Browse Source
- collision is off by the distance of a sprite's registration point - doesn't keep score - speaks with every collision (testing) - only spawns Beardsonsdevelop
Rob Colbert
2 years ago
31 changed files with 484 additions and 599 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 124 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 4.2 KiB |
@ -0,0 +1,41 @@ |
|||
// lib/game-background.js
|
|||
// Copyright (C) 2022 Rob Colbert @[email protected]
|
|||
// License: Apache-2.0
|
|||
|
|||
'use strict'; |
|||
|
|||
import NiceImage from '/dtp-nice-game/nice-image.js'; |
|||
|
|||
/** |
|||
* GameBackground provides an image to use as the backdrop, an update method for |
|||
* updating animated and other dynamic components, and a draw method, which can |
|||
* all be implemented to provide all sorts of dynamic effects on the background. |
|||
* |
|||
* ...which just happens to be a NiceImage. |
|||
*/ |
|||
export default class GameBackground extends NiceImage { |
|||
|
|||
constructor (game) { |
|||
super(); |
|||
this.game = game; |
|||
} |
|||
|
|||
update ( ) { |
|||
/* |
|||
* TODO: dynamic backgrounds that can animate on their own |
|||
* Yes, this is being called now. It just does nothing for now. |
|||
*/ |
|||
} |
|||
|
|||
draw (ctx, x, y) { |
|||
super.draw(ctx, x, y); |
|||
|
|||
/* |
|||
* Free to render anything we want over that here, and I will... |
|||
* |
|||
* Yes, this is being called now. I'm just not adding anything, yet. |
|||
* |
|||
* Think rain, snowflakes, moving water, clouds, and maybe "live events" |
|||
*/ |
|||
} |
|||
} |
@ -0,0 +1,112 @@ |
|||
// lib/game-egg-simulator.js
|
|||
// Copyright (C) 2022 Rob Colbert @[email protected]
|
|||
// License: Apache-2.0
|
|||
|
|||
'use strict'; |
|||
|
|||
import NiceImage from '/dtp-nice-game/nice-image.js'; |
|||
import NiceSprite from '/dtp-nice-game/nice-sprite.js'; |
|||
|
|||
/** |
|||
* GameEggSimulator manages the eggs in the scene. As Tex throws them, they are |
|||
* spawned into this manager. The manager is then updated once per frame, then |
|||
* asked if any eggs collide with meaningful objects. This drives a lot of game |
|||
* play. |
|||
* |
|||
* The GameEggSimulator will simply call methods on this.game to have whatever |
|||
* impact it wants to have based on frame-by-frame updates. |
|||
*/ |
|||
export default class GameEggSimulator extends NiceSprite { |
|||
|
|||
constructor (game) { |
|||
super(); |
|||
|
|||
this.game = game; |
|||
|
|||
this.eggImages = [ ]; |
|||
this.eggSprites = [ ]; |
|||
} |
|||
|
|||
async load ( ) { |
|||
const jobs = [ ]; |
|||
let image; |
|||
|
|||
image = new NiceImage(); |
|||
this.eggImages.push(image); |
|||
jobs.push(image.load('/dist/assets/img/egg-001.png', 12, 16)); |
|||
|
|||
image = new NiceImage(); |
|||
this.eggImages.push(image); |
|||
jobs.push(image.load('/dist/assets/img/egg-002.png', 13, 16)); |
|||
|
|||
image = new NiceImage(); |
|||
this.eggImages.push(image); |
|||
jobs.push(image.load('/dist/assets/img/egg-003.png', 13, 16)); |
|||
|
|||
await Promise.all(jobs); |
|||
} |
|||
|
|||
clear ( ) { |
|||
this.eggSprites = [ ]; |
|||
} |
|||
|
|||
throwEgg (position, moveSpeed = 2) { |
|||
const index = Math.floor(Math.random() * this.eggImages.length); |
|||
this.game.log.info('throwEgg', 'throwing new egg', { index }); |
|||
|
|||
const egg = new NiceSprite(this.game, position); |
|||
egg.image = this.eggImages[index]; |
|||
egg.moveSpeed = moveSpeed; |
|||
egg.rotationSpeed = Math.random() * 0.2; |
|||
if (Math.random() > 0.5) { |
|||
egg.rotationSpeed = -egg.rotationSpeed; |
|||
} |
|||
|
|||
this.eggSprites.push(egg); |
|||
|
|||
return egg; |
|||
} |
|||
|
|||
removeEgg (egg) { |
|||
const index = this.eggSprites.indexOf(egg); |
|||
if (index !== -1) { |
|||
this.eggSprites.splice(index, 1); |
|||
} |
|||
} |
|||
|
|||
update ( ) { |
|||
const expiredEggs = [ ]; |
|||
for (const egg of this.eggSprites) { |
|||
egg.position.y -= egg.moveSpeed; |
|||
egg.rotation += egg.rotationSpeed; |
|||
if (egg.position.y < -egg.image.height) { |
|||
expiredEggs.push(egg); |
|||
} |
|||
} |
|||
for (const egg of expiredEggs) { |
|||
this.removeEgg(egg); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Tests if an enemy (NiceSprite) collides with any egg(s) in the simulation. |
|||
* @param {GameEnemy} enemy The enemy to be tested for collision with any |
|||
* egg(s). |
|||
*/ |
|||
collidesWith (enemy) { |
|||
const matchedEggs = this.eggSprites.filter((egg) => enemy.collidesWithAABB(egg)); |
|||
if (matchedEggs.length > 0) { |
|||
for (const egg of matchedEggs) { |
|||
this.removeEgg(egg); |
|||
} |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
render (ctx) { |
|||
for (const egg of this.eggSprites) { |
|||
egg.render(ctx); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,109 @@ |
|||
// lib/game-enemies.js
|
|||
// Copyright (C) 2022 Rob Colbert @[email protected]
|
|||
// License: Apache-2.0
|
|||
|
|||
'use strict'; |
|||
|
|||
import NiceImage from '/dtp-nice-game/nice-image.js'; |
|||
import NiceSprite from '/dtp-nice-game/nice-sprite.js'; |
|||
|
|||
import GameEnemyBeardson from './game-enemy-beardson.js'; |
|||
|
|||
/** |
|||
* GameEnemies manages the instances of "enemny" characters in play. They are |
|||
* spawned into this object, which then performs their "AI" during each frame's |
|||
* update processing. |
|||
*/ |
|||
export default class GameEnemies extends NiceSprite { |
|||
|
|||
constructor (game) { |
|||
super(); |
|||
|
|||
this.game = game; |
|||
this.moveSpeed = 1.0; |
|||
|
|||
this.enemyImages = { }; |
|||
this.enemies = [ ]; |
|||
} |
|||
|
|||
async load ( ) { |
|||
const jobs = [ ]; |
|||
|
|||
this.enemyImages.groyper = new NiceImage(); |
|||
jobs.push(this.enemyImages.groyper.load('/dist/assets/img/groyper.png', 64, 64)); |
|||
|
|||
this.enemyImages.groyperLarge = new NiceImage(); |
|||
jobs.push(this.enemyImages.groyperLarge.load('/dist/assets/img/groyper.large.png', 180, 180)); |
|||
|
|||
this.enemyImages.beardson = new NiceImage(); |
|||
jobs.push(this.enemyImages.beardson.load('/dist/assets/img/beardson.png', 38, 64)); |
|||
|
|||
this.enemyImages.beardsonLarge = new NiceImage(); |
|||
jobs.push(this.enemyImages.beardsonLarge.load('/dist/assets/img/beardson.large.png', 108, 180)); |
|||
|
|||
this.enemyImages.fuentesPuppy = new NiceImage(); |
|||
jobs.push(this.enemyImages.fuentesPuppy.load('/dist/assets/img/fuentes-puppy-mode.png', 64, 64)); |
|||
|
|||
this.enemyImages.fuentesPuppyLarge = new NiceImage(); |
|||
jobs.push(this.enemyImages.fuentesPuppyLarge.load('/dist/assets/img/fuentes-puppy-mode.large.png', 180, 180)); |
|||
|
|||
await Promise.all(jobs); |
|||
} |
|||
|
|||
update ( ) { |
|||
const eliminatedEnemies = this.enemies.filter((enemy) => { |
|||
enemy.update(); |
|||
if (!this.game.eggs.collidesWith(enemy)) { |
|||
return false; |
|||
} |
|||
this.game.audio.playSound(this.getRandomImpactSound()); |
|||
this.game.audio.playSound(this.getRandomTexQuote()); |
|||
return true; |
|||
}); |
|||
for (const enemy of eliminatedEnemies) { |
|||
this.removeEnemy(enemy); |
|||
} |
|||
} |
|||
|
|||
render (ctx) { |
|||
for (const enemy of this.enemies) { |
|||
enemy.render(ctx); |
|||
} |
|||
} |
|||
|
|||
spawnBeardson ( ) { |
|||
this.game.log.debug('spawnBeardson', 'a wild Beardson approaches from the north'); |
|||
const beardson = new GameEnemyBeardson( |
|||
this.game, |
|||
this.enemyImages.beardson, |
|||
this.getRandomSpawnPoint(), |
|||
{ |
|||
x: this.moveSpeed * (1 + (Math.random() * 3)), |
|||
y: this.moveSpeed * (0.25 + (Math.random() * 0.75)), |
|||
}, |
|||
); |
|||
this.enemies.push(beardson); |
|||
} |
|||
|
|||
getRandomSpawnPoint ( ) { |
|||
const x = 80 + (800 * Math.random()); |
|||
return { x, y: 32 }; |
|||
} |
|||
|
|||
removeEnemy (enemy) { |
|||
const index = this.enemies.indexOf(enemy); |
|||
if (index !== -1) { |
|||
this.enemies.splice(index, 1); |
|||
} |
|||
} |
|||
|
|||
getRandomImpactSound ( ) { |
|||
const impactSounds = ['impact-001','impact-002','impact-003','impact-004','impact-005','impact-006','impact-007']; |
|||
return impactSounds[Math.floor(Math.random() * impactSounds.length)]; |
|||
} |
|||
|
|||
getRandomTexQuote ( ) { |
|||
const texQuotes = ['tex-stayfresh', 'tex-biggerfish', 'tex-shutuptwo']; |
|||
return texQuotes[Math.floor(Math.random() * texQuotes.length)]; |
|||
} |
|||
} |
@ -0,0 +1,44 @@ |
|||
// lib/game-enemy-beardson.js
|
|||
// Copyright (C) 2022 Rob Colbert @[email protected]
|
|||
// License: Apache-2.0
|
|||
|
|||
'use strict'; |
|||
|
|||
import NiceSprite from '/dtp-nice-game/nice-sprite.js'; |
|||
|
|||
/** |
|||
* Implements a Beardson. The Beardson moves toward the bottom of the game play |
|||
* area. When he reaches the bottom, he pulls out a rainbow-colored dildo and |
|||
* chases Tex. When he catches Tex, Tex loses a life. |
|||
*/ |
|||
export default class GameEnemyBeardson extends NiceSprite { |
|||
|
|||
constructor (game, image, position, moveSpeed) { |
|||
super(game, position); |
|||
this.image = image; |
|||
this.game = game; |
|||
this.targetX = position.x; |
|||
this.moveSpeed = { |
|||
x: moveSpeed.x, |
|||
y: moveSpeed.y, |
|||
}; |
|||
} |
|||
|
|||
update ( ) { |
|||
if (this.position.y < 480) { |
|||
this.position.y += this.moveSpeed.y; |
|||
} |
|||
|
|||
if (Math.random() > 0.998) { |
|||
this.targetX = 80 + (Math.floor(800 * Math.random())); |
|||
} |
|||
|
|||
if (this.position.x > this.targetX) { |
|||
this.position.x -= this.moveSpeed.x; |
|||
} |
|||
|
|||
if (this.position.x < this.targetX) { |
|||
this.position.x += this.moveSpeed.x; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,32 @@ |
|||
// lib/game-player.js
|
|||
// Copyright (C) 2022 Rob Colbert @[email protected]
|
|||
// License: Apache-2.0
|
|||
|
|||
'use strict'; |
|||
|
|||
import NiceSprite from '/dtp-nice-game/nice-sprite.js'; |
|||
|
|||
/** |
|||
* GamePlayer is a simple 2D NiceSprite-based object that knows how to consider |
|||
* player input during an update loop to move around and do some things. |
|||
*/ |
|||
export default class GamePlayer extends NiceSprite { |
|||
|
|||
constructor (game) { |
|||
super(); |
|||
this.game = game; |
|||
this.moveSpeed = 2; |
|||
} |
|||
|
|||
async load ( ) { return super.load('/dist/assets/img/big-baja-tex.png', 82, 128); } |
|||
|
|||
update ( ) { |
|||
const { input } = this.game; |
|||
if (input.buttons.moveLeft.isPressed || input.keys.moveLeft.isPressed) { |
|||
this.position.x -= this.moveSpeed; |
|||
} |
|||
if (input.buttons.moveRight.isPressed || input.keys.moveRight.isPressed) { |
|||
this.position.x += this.moveSpeed; |
|||
} |
|||
} |
|||
} |
@ -1,139 +0,0 @@ |
|||
// lib/nice-audio.js
|
|||
// Copyright (C) 2022 Rob Colbert @[email protected]
|
|||
// License: Apache-2.0
|
|||
|
|||
'use strict'; |
|||
|
|||
const DTP_COMPONENT_NAME = 'nice-audio'; |
|||
|
|||
import NiceLog from './nice-log.js'; |
|||
|
|||
const AudioContext = window.AudioContext || window.webkitAudioContext; |
|||
|
|||
/** |
|||
* NiceAudio is the audio engine for the Nice Arcade SDK. It can load audio |
|||
* files into audio buffers, and play the buffers. It can also stream larger |
|||
* audio files (ex: background music). |
|||
*/ |
|||
export default class NiceAudio { |
|||
|
|||
constructor ( ) { |
|||
this.log = new NiceLog(DTP_COMPONENT_NAME); |
|||
} |
|||
|
|||
start ( ) { |
|||
this.ctx = new AudioContext(); |
|||
this.masterVolume = this.ctx.createGain(); |
|||
this.masterVolume.connect(this.ctx.destination); |
|||
|
|||
this.musicGain = this.ctx.createGain(); |
|||
this.musicGain.value = 0.7; |
|||
this.musicGain.connect(this.masterVolume); |
|||
} |
|||
|
|||
/** |
|||
* Loads an audio resource from a specified URL, decodes that audio data into |
|||
* an Audio Buffer, and resolves the buffer. This is commonly used for small |
|||
* audio files to be used as sound effects. Music, multi-megabyte, and multi- |
|||
* minute audio should be streamed, not loaded into memory. |
|||
* @param {URL} url The audio resource to be loaded into an in-memory audio buffer. |
|||
* @returns Promise that resolves the audio buffer or rejects with an error. |
|||
*/ |
|||
loadAudioBuffer (url) { |
|||
return new Promise(async (resolve, reject) => { |
|||
const response = await fetch(url); |
|||
const audioData = await response.arrayBuffer(); |
|||
this.ctx.decodeAudioData(audioData, resolve, reject); |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* Creates a buffer source object, sets the specified buffer as the data |
|||
* source, creates a gain node, configures both as specified, plus the buffer |
|||
* source into the gain node, plugs the gain node into masterVolume. |
|||
* |
|||
* If you hold onto the `source` or `gain` objects returned, you may create a |
|||
* memory leak and/or overwhelm audio resources on the player's device. |
|||
* |
|||
* @param {AudioBuffer} buffer |
|||
* @param {Object} options Allows specification of `volume` (default: 1.0) and `loop` (default: false). |
|||
* @returns An object containing a `source` and `gain` object that can be used for additional routing, effects, etc. |
|||
*/ |
|||
playAudioBuffer (buffer, options = { }) { |
|||
options = Object.assign({ |
|||
volume: 1.0, |
|||
loop: false, |
|||
}, options); |
|||
|
|||
const source = this.ctx.createBufferSource(); |
|||
source.buffer = buffer; |
|||
source.loop = options.loop; |
|||
|
|||
const gain = this.ctx.createGain(); |
|||
gain.value = options.volume; |
|||
|
|||
source.connect(gain); |
|||
gain.connect(this.masterVolume); |
|||
|
|||
source.start(); |
|||
|
|||
return { gain, source }; |
|||
} |
|||
|
|||
/** |
|||
* Will stop any current playing music, destroy the current audio source and |
|||
* Audio element, then create a new stack to support the new resource. |
|||
* @param {URL} url The URL of the resource to be set as the new music stream |
|||
* for the audio engine. |
|||
*/ |
|||
setMusicStream (url) { |
|||
this.stopMusicStream(); |
|||
this.log.debug('setMusicStream', 'setting new music stream', { url }); |
|||
this.music = new Audio(url); |
|||
} |
|||
|
|||
playMusicStream ( ) { |
|||
if (this.musicSource) { |
|||
return; |
|||
} |
|||
|
|||
this.musicSource = this.ctx.createMediaElementSource(this.music); |
|||
this.musicSource.connect(this.musicGain); |
|||
|
|||
this.log.debug('playMusicStream', 'starting music stream playback'); |
|||
this.music.play(); |
|||
} |
|||
|
|||
stopMusicStream ( ) { |
|||
if (!this.musicSource) { |
|||
return; |
|||
} |
|||
|
|||
this.log.debug('pauseMusicStream', 'stopping music stream playback'); |
|||
this.music.pause(); |
|||
|
|||
this.musicGain.value = 0; |
|||
|
|||
this.musicSource.disconnect(this.musicGain); |
|||
delete this.musicSource; |
|||
} |
|||
|
|||
get musicVolume ( ) { return this.musicGain.value; } |
|||
set musicVolume (volume) { |
|||
this.musicGain.value = volume; |
|||
} |
|||
|
|||
get haveMusicStream ( ) { |
|||
return !!this.music && |
|||
!!this.musicSource && |
|||
!!this.musicGain |
|||
; |
|||
} |
|||
|
|||
get isMusicPaused ( ) { |
|||
if (!this.music) { |
|||
return true; |
|||
} |
|||
return this.music.paused; |
|||
} |
|||
} |
@ -1,70 +0,0 @@ |
|||
// lib/nice-game.js
|
|||
// Copyright (C) 2022 Rob Colbert
|
|||
// License: Apache-2.0
|
|||
|
|||
'use strict'; |
|||
|
|||
import NiceLog from '/dist/js/lib/nice-log.js'; |
|||
import NiceAudio from '/dist/js/lib/nice-audio.js'; |
|||
import NiceImage from './nice-image.js'; |
|||
import NiceSprite from './nice-sprite.js'; |
|||
|
|||
import { |
|||
NiceInputTools, |
|||
NiceInputButton, |
|||
NiceInputKey, |
|||
NiceInput, |
|||
} from '/dist/js/lib/nice-input.js'; |
|||
|
|||
class NiceGame { |
|||
|
|||
constructor (componentName) { |
|||
this.componentName = componentName; |
|||
|
|||
this.log = new NiceLog(this.componentName); |
|||
this.log.enable(true); |
|||
|
|||
this.sprites = { }; |
|||
this.eggs = [ ]; |
|||
} |
|||
|
|||
async startGameEngine (onGameUpdate) { |
|||
this.gameDisplayCanvas = document.getElementById('game-display'); |
|||
if (!this.gameDisplayCanvas) { |
|||
this.log.error('startup', 'failed to find game display canvas'); |
|||
throw new Error('failed to find game display canvas'); |
|||
} |
|||
|
|||
this.onGameUpdate = onGameUpdate; |
|||
|
|||
this.input = new NiceInput(); |
|||
this.audio = new NiceAudio(); |
|||
|
|||
this.log.info('startup', 'starting game update loop'); |
|||
this.gameDisplayCanvas.width = 960; |
|||
this.gameDisplayCanvas.height = 540; |
|||
|
|||
this.gameDisplayCtx = this.gameDisplayCanvas.getContext('2d'); |
|||
this.onUpdateDisplay = this.updateDisplay.bind(this); |
|||
window.requestAnimationFrame(this.onUpdateDisplay); |
|||
} |
|||
|
|||
updateDisplay ( ) { |
|||
if (this.onGameUpdate && (typeof this.onGameUpdate === 'function')) { |
|||
this.onGameUpdate(this.gameDisplayCtx); |
|||
} |
|||
window.requestAnimationFrame(this.onUpdateDisplay); |
|||
} |
|||
} |
|||
|
|||
export { |
|||
NiceAudio, |
|||
NiceGame, |
|||
NiceImage, |
|||
NiceInputButton, |
|||
NiceInputKey, |
|||
NiceInputTools, |
|||
NiceInput, |
|||
NiceLog, |
|||
NiceSprite, |
|||
}; |
@ -1,25 +0,0 @@ |
|||
// lib/nice-image.js
|
|||
// Copyright (C) 2022 Rob Colbert @[email protected]
|
|||
// License: Apache-2.0
|
|||
|
|||
'use strict'; |
|||
|
|||
export default class NiceImage { |
|||
|
|||
async load (url, width, height) { |
|||
this.image = new Image(width, height); |
|||
return new Promise((resolve, reject) => { |
|||
this.image.onload = ( ) => resolve(this); |
|||
this.image.onerror = reject; |
|||
this.image.src = url; |
|||
}); |
|||
} |
|||
|
|||
get width ( ) { return this.image.width; } |
|||
get height ( ) { return this.image.height; } |
|||
|
|||
draw (ctx, x, y) { |
|||
if (!this.image) { return; } |
|||
ctx.drawImage(this.image, x, y); |
|||
} |
|||
} |
@ -1,163 +0,0 @@ |
|||
// lib/nice-input.js
|
|||
// Copyright (C) 2022 Rob Colbert @[email protected]
|
|||
// License: Apache-2.0
|
|||
|
|||
'use strict'; |
|||
|
|||
const DTP_COMPONENT_NAME = 'nice-input'; |
|||
|
|||
import NiceLog from './nice-log.js'; |
|||
|
|||
/** |
|||
* Static support methods used across the variety of Input classes. |
|||
*/ |
|||
export class NiceInputTools { |
|||
|
|||
/** |
|||
* Entirely cancels and stops all default behavior on the given event. This is |
|||
* useful for disabling long-press actions on buttons in a game that expects |
|||
* the player to hold a button down to move in a direction. |
|||
* |
|||
* @param {Event} event The DOM input event to be canceled like as if Reddit |
|||
* got involved. |
|||
*/ |
|||
static cancelEvent (event) { |
|||
event = event || window.event; |
|||
event.preventDefault(); |
|||
event.stopPropagation(); |
|||
event.cancelBubble = true; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* The NiceInputButton class implements the glue between the Nice engine and the |
|||
* browser's various methods and events for working with a button. |
|||
*/ |
|||
export class NiceInputButton { |
|||
|
|||
constructor (manager, actionName, buttonId) { |
|||
this.manager = manager; |
|||
this.log = manager.log; |
|||
|
|||
this.actionName = actionName; |
|||
this.isPressed = false; |
|||
|
|||
this.element = document.getElementById(buttonId); |
|||
if (!this.element) { |
|||
throw new Error(`button element ${buttonId} does not exist in view`); |
|||
} |
|||
|
|||
this.log.debug('NiceInputButton', 'registering event callbacks', { element: this.element }); |
|||
|
|||
/* |
|||
* Events that mean a button is currently "down" |
|||
*/ |
|||
this.element.addEventListener('touchstart', this.onInputStart.bind(this)); |
|||
this.element.addEventListener('mousedown', this.onInputStart.bind(this)); |
|||
|
|||
/* |
|||
* Events that mean a button is no longer "down" |
|||
*/ |
|||
this.element.addEventListener('touchend', this.onInputEnd.bind(this)); |
|||
this.element.addEventListener('touchcancel', this.onInputEnd.bind(this)); |
|||
this.element.addEventListener('mouseup', this.onInputEnd.bind(this)); |
|||
this.element.addEventListener('mouseleave', this.onInputEnd.bind(this)); |
|||
|
|||
/* |
|||
* Ignored, but also needs to be canceled |
|||
*/ |
|||
this.element.addEventListener('touchmove', NiceInputTools.cancelEvent); |
|||
} |
|||
|
|||
async onInputStart (event) { |
|||
NiceInputTools.cancelEvent(event); |
|||
this.log.debug('onInputStart', 'button down', { action: this.actionName }); |
|||
this.element.classList.add('active'); |
|||
this.isPressed = true; |
|||
} |
|||
|
|||
async onInputEnd (event) { |
|||
NiceInputTools.cancelEvent(event); |
|||
this.log.debug('onInputEnd', 'button up', { action: this.actionName }); |
|||
this.element.classList.remove('active'); |
|||
this.isPressed = false; |
|||
} |
|||
} |
|||
|
|||
export class NiceInputKey { |
|||
|
|||
constructor (actionName, keyCode) { |
|||
this.actionName = actionName; |
|||
this.keyCode = keyCode; |
|||
this.isPressed = false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* The NiceInput class manages interaction with the user through the browser and |
|||
* DOM events. You don't have to worry about those. You simply add an input by |
|||
* name, specify the ID of the control in the HTML View, and NiceInput will |
|||
* handle all interaction with that control. |
|||
* |
|||
* The game programmer can then check the state of their inputs during normal |
|||
* game logic using one consistent interface: NiceInput. |
|||
*/ |
|||
export class NiceInput { |
|||
|
|||
constructor ( ) { |
|||
this.log = new NiceLog(DTP_COMPONENT_NAME); |
|||
this.log.enable(true); |
|||
|
|||
this.buttons = { }; |
|||
this.keys = { }; |
|||
|
|||
document.addEventListener('keydown', this.onKeyDown.bind(this)); |
|||
document.addEventListener('keyup', this.onKeyUp.bind(this)); |
|||
} |
|||
|
|||
addButton (actionName, buttonId) { |
|||
const button = new NiceInputButton(this, actionName, buttonId); |
|||
this.buttons[actionName] = button; |
|||
} |
|||
|
|||
addKey (actionName, keyCode) { |
|||
const key = new NiceInputKey (actionName, keyCode); |
|||
this.keys[actionName] = key; |
|||
} |
|||
|
|||
async onKeyDown (event) { |
|||
this.log.debug('onKeyDown', 'key pressed', { event }); |
|||
|
|||
const actionName = Object |
|||
.keys(this.keys) |
|||
.find((key) => this.keys[key].keyCode === event.code) |
|||
; |
|||
if (!actionName) { return; } |
|||
|
|||
const key = this.keys[actionName]; |
|||
key.isPressed = true; |
|||
|
|||
const button = this.buttons[actionName]; |
|||
if (button) { |
|||
button.element.classList.add('active'); |
|||
} |
|||
} |
|||
|
|||
async onKeyUp (event) { |
|||
this.log.debug('onKeyUp', 'key released', { event }); |
|||
|
|||
const actionName = Object |
|||
.keys(this.keys) |
|||
.find((key) => this.keys[key].keyCode === event.code) |
|||
; |
|||
if (!actionName) { return; } |
|||
|
|||
const key = this.keys[actionName]; |
|||
key.isPressed = false; |
|||
|
|||
const button = this.buttons[actionName]; |
|||
if (button) { |
|||
button.element.classList.remove('active'); |
|||
} |
|||
} |
|||
} |
@ -1,125 +0,0 @@ |
|||
// lib/nice-log.js
|
|||
// Copyright (C) 2022 Rob Colbert @[email protected]
|
|||
// License: Apache-2.0
|
|||
|
|||
'use strict'; |
|||
|
|||
export default class NiceLog { |
|||
|
|||
constructor (componentName, options) { |
|||
this.componentName = componentName; |
|||
this.options = Object.assign({ |
|||
color: { |
|||
debug: '#808080', |
|||
info: '#249324', |
|||
warn: '#AC7C37', |
|||
error: '#A74949', |
|||
}, |
|||
size: { |
|||
label: 120, |
|||
}, |
|||
}, options); |
|||
|
|||
this.css = { |
|||
debug: { |
|||
label: ` |
|||
display: inline-block; |
|||
background-color: ${this.options.color.debug}; |
|||
color: white; |
|||
width: ${this.options.size.label}px; |
|||
padding: 2px 4px; |
|||
border-radius: 4px; |
|||
`,
|
|||
message: ` |
|||
color: ${this.options.color.debug}; |
|||
`,
|
|||
}, |
|||
info: { |
|||
label: ` |
|||
background-color: ${this.options.color.info}; |
|||
color: white; |
|||
width: ${this.options.size.label}px; |
|||
padding: 2px 4px; |
|||
border-radius: 4px; |
|||
`,
|
|||
message: ` |
|||
color: ${this.options.color.info}; |
|||
`,
|
|||
}, |
|||
warn: { |
|||
label: ` |
|||
background-color: ${this.options.color.warn}; |
|||
color: white; |
|||
width: ${this.options.size.label}px; |
|||
padding: 2px 4px; |
|||
border-radius: 4px; |
|||
`,
|
|||
message: ` |
|||
color: ${this.options.color.warn}; |
|||
`,
|
|||
}, |
|||
error: { |
|||
label: ` |
|||
background-color: ${this.options.color.error}; |
|||
color: white; |
|||
width: ${this.options.size.label}px; |
|||
padding: 2px 4px; |
|||
border-radius: 4px; |
|||
`,
|
|||
message: ` |
|||
color: ${this.options.color.error}; |
|||
`,
|
|||
}, |
|||
}; |
|||
|
|||
const env = document.querySelector('body').getAttribute('data-dtp-env'); |
|||
if (env === 'local') { |
|||
this.enable(); |
|||
} |
|||
} |
|||
|
|||
enable (enabled = true) { |
|||
this.enabled = enabled; |
|||
} |
|||
|
|||
debug (event, msg, data) { |
|||
this.write('debug', this.css.debug, event, msg, data); |
|||
} |
|||
|
|||
log (event, msg, data) { this.info(event, msg, data); } |
|||
|
|||
info (event, msg, data) { |
|||
this.write('log', this.css.info, event, msg, data); |
|||
} |
|||
|
|||
warn (event, msg, data) { // alias for warning
|
|||
this.warning(event, msg, data); |
|||
} |
|||
|
|||
warning (event, msg, data) { |
|||
this.write('warn', this.css.warn, event, msg, data); |
|||
} |
|||
|
|||
error (event, msg, data) { |
|||
this.write('error', this.css.error, event, msg, data); |
|||
if (data && data.error) { |
|||
console.error(data.error); |
|||
} |
|||
} |
|||
|
|||
write (method, css, event, msg, data) { |
|||
if (!this.enabled) { return; } |
|||
if (data) { |
|||
console[method]('%c%s%c: %s', |
|||
css.label, `${this.componentName}.${event}`, |
|||
css.message, msg, |
|||
data, |
|||
); |
|||
} else { |
|||
console[method]('%c%s%c: %s', |
|||
css.label, `${this.componentName}.${event}`, |
|||
css.message, msg, |
|||
); |
|||
} |
|||
} |
|||
} |
@ -1,27 +0,0 @@ |
|||
// lib/nice-sprite.js
|
|||
// Copyright (C) 2022 Rob Colbert @[email protected]
|
|||
// License: Apache-2.0
|
|||
|
|||
'use strict'; |
|||
|
|||
import NiceImage from './nice-image.js'; |
|||
|
|||
export default class NiceSprite { |
|||
|
|||
constructor ( ) { |
|||
this.position = { x: 0, y: 0 }; |
|||
} |
|||
|
|||
async load (url, width, height) { |
|||
this.image = new NiceImage(); |
|||
return this.image.load(url, width, height); |
|||
} |
|||
|
|||
render (ctx) { |
|||
this.image.draw( |
|||
ctx, |
|||
this.position.x - (this.image.width / 2), |
|||
this.position.y - (this.image.height / 2), |
|||
); |
|||
} |
|||
} |
@ -1,28 +1,18 @@ |
|||
<!DOCTYPE html> |
|||
html(lang="en") |
|||
head |
|||
meta(charset="utf-8") |
|||
meta(name="viewport" content="width=device-width, initial-scale=1") |
|||
title CyberEgg 2077 |
|||
extends layout |
|||
block game-view |
|||
|
|||
link(rel="stylesheet" href=`/dist/css/style.css?v=${pkg.version}`) |
|||
section.text-center |
|||
|
|||
body |
|||
.container |
|||
section.text-center |
|||
.game-title-block(hidden) |
|||
.game-title CyberEgg 2077 |
|||
.game-subtitle Defend Earth From Cybersoy Invasion |
|||
|
|||
.game-title-block(hidden) |
|||
.game-title CyberEgg 2077 |
|||
.game-subtitle Defend Earth From Cybersoy Invasion |
|||
.margin-block |
|||
.game-display-wrapper |
|||
canvas(id="game-display" width="960" height="540").game-display |
|||
button(type="button").start-button#start-button Start Game |
|||
button(type="button").direction-button#btn-move-left < |
|||
button(type="button").direction-button#btn-move-right > |
|||
button(type="button").action-button#btn-throw-egg 🥚 |
|||
|
|||
.margin-block |
|||
.game-display-wrapper |
|||
canvas(id="game-display" width="960" height="540").game-display |
|||
button(type="button").start-button#start-button Start Game |
|||
button(type="button").direction-button#btn-move-left < |
|||
button(type="button").direction-button#btn-move-right > |
|||
button(type="button").action-button#btn-throw-egg 🥚 |
|||
|
|||
.notice a #[a(href="https://nicecrew.digital") #nicecrew] exclusive |
|||
|
|||
script(src=`${gameModuleUrl}?v=${pkg.version}`, type="module") |
|||
.notice a #[a(href="https://nicecrew.digital") #nicecrew] exclusive |
@ -0,0 +1,14 @@ |
|||
<!DOCTYPE html> |
|||
html(lang="en") |
|||
head |
|||
meta(charset="utf-8") |
|||
meta(name="viewport" content="width=device-width, initial-scale=1") |
|||
title CyberEgg 2077 |
|||
|
|||
link(rel="stylesheet" href=`/dist/css/style.css?v=${pkg.version}`) |
|||
|
|||
body |
|||
.container |
|||
block game-view |
|||
|
|||
script(src=`${gameModuleUrl}?v=${pkg.version}`, type="module") |
@ -219,6 +219,12 @@ [email protected]: |
|||
dom-serializer "0" |
|||
domelementtype "1" |
|||
|
|||
"dtp-nice-game@https://git.digitaltelepresence.com/digital-telepresence/dtp-nice-game.git": |
|||
version "0.1.4" |
|||
resolved "https://git.digitaltelepresence.com/digital-telepresence/dtp-nice-game.git#e42e3efc5c7bb3cbe0ef3effd97ca25b481ee16c" |
|||
dependencies: |
|||
jshint "^2.13.4" |
|||
|
|||
[email protected]: |
|||
version "1.1.1" |
|||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" |
|||
|
Loading…
Reference in new issue