Browse Source

first "playable"

- collision is off by the distance of a sprite's registration point
- doesn't keep score
- speaks with every collision (testing)
- only spawns Beardsons
develop
Rob Colbert 2 years ago
parent
commit
2b9d9f8b98
  1. BIN
      dist/assets/audio/sfx/impact-001.wav
  2. BIN
      dist/assets/audio/sfx/impact-002.wav
  3. BIN
      dist/assets/audio/sfx/impact-003.wav
  4. BIN
      dist/assets/audio/sfx/impact-004.wav
  5. BIN
      dist/assets/audio/sfx/impact-005.wav
  6. BIN
      dist/assets/audio/sfx/impact-006.wav
  7. BIN
      dist/assets/audio/sfx/impact-007.wav
  8. BIN
      dist/assets/audio/sfx/throw-egg-001.wav
  9. BIN
      dist/assets/audio/vox/tex-biggerfish.wav
  10. BIN
      dist/assets/audio/vox/tex-shutuptwo.wav
  11. BIN
      dist/assets/audio/vox/tex-stayfresh.wav
  12. BIN
      dist/assets/img/fuentes.boss.png
  13. BIN
      dist/assets/img/rainbow-dildo.large.png
  14. BIN
      dist/assets/img/rainbow-dildo.png
  15. 135
      dist/js/cyberegg2077.js
  16. 41
      dist/js/lib/game-background.js
  17. 112
      dist/js/lib/game-egg-simulator.js
  18. 109
      dist/js/lib/game-enemies.js
  19. 44
      dist/js/lib/game-enemy-beardson.js
  20. 32
      dist/js/lib/game-player.js
  21. 139
      dist/js/lib/nice-audio.js
  22. 70
      dist/js/lib/nice-game.js
  23. 25
      dist/js/lib/nice-image.js
  24. 163
      dist/js/lib/nice-input.js
  25. 125
      dist/js/lib/nice-log.js
  26. 27
      dist/js/lib/nice-sprite.js
  27. 2
      minigame-engine.js
  28. 1
      package.json
  29. 38
      views/index.pug
  30. 14
      views/layout.pug
  31. 6
      yarn.lock

BIN
dist/assets/audio/sfx/impact-001.wav

Binary file not shown.

BIN
dist/assets/audio/sfx/impact-002.wav

Binary file not shown.

BIN
dist/assets/audio/sfx/impact-003.wav

Binary file not shown.

BIN
dist/assets/audio/sfx/impact-004.wav

Binary file not shown.

BIN
dist/assets/audio/sfx/impact-005.wav

Binary file not shown.

BIN
dist/assets/audio/sfx/impact-006.wav

Binary file not shown.

BIN
dist/assets/audio/sfx/impact-007.wav

Binary file not shown.

BIN
dist/assets/audio/sfx/throw-egg-001.wav

Binary file not shown.

BIN
dist/assets/audio/vox/tex-biggerfish.wav

Binary file not shown.

BIN
dist/assets/audio/vox/tex-shutuptwo.wav

Binary file not shown.

BIN
dist/assets/audio/vox/tex-stayfresh.wav

Binary file not shown.

BIN
dist/assets/img/fuentes.boss.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
dist/assets/img/rainbow-dildo.large.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
dist/assets/img/rainbow-dildo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

135
dist/js/cyberegg2077.js

@ -6,36 +6,24 @@
const DTP_COMPONENT_NAME = 'nice-game';
import { NiceImage, NiceSprite, NiceGame } from '/dist/js/lib/nice-game.js';
import { NiceGame } from '/dtp-nice-game/nice-game.js';
class Tex 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;
}
}
}
import GameBackground from './lib/game-background.js';
import GameEggSimulator from './lib/game-egg-simulator.js';
import GamePlayer from './lib/game-player.js';
import GameEnemies from './lib/game-enemies.js';
class CyberEgg2077 extends NiceGame {
constructor ( ) {
super(DTP_COMPONENT_NAME);
this.startButton = document.getElementById('start-button');
this.startButton.addEventListener('click', this.onStartGame.bind(this));
this.mode = 'loading';
this.eggs = [ ];
}
async run ( ) {
@ -56,11 +44,29 @@ class CyberEgg2077 extends NiceGame {
}
async onStartGame (/* event */) {
this.mode = 'loading';
this.audio.start();
this.loadAudio();
this.audio.music.play();
this.startButton.setAttribute('hidden', '');
this.tex.moveSpeed = 2;
this.eggs.clear();
this.oldWantsThrowEgg = false;
this.mode = 'game';
this.level = 1;
this.startLevel();
}
startLevel ( ) {
const NOW = new Date();
this.nextSpawnInterval = 1000 * 5;
this.nextSpawnTime = NOW.valueOf() + this.nextSpawnInterval;
}
onUpdateDisplay (ctx) {
@ -80,20 +86,97 @@ class CyberEgg2077 extends NiceGame {
}
updateGame (ctx) {
const NOW = new Date();
this.background.update();
this.tex.update();
/*
* Use this.oldWantsThrowEgg to gate button presses and only throw the
* egg when the button is first pressed (not every frame while pressed)
*/
const wantsThrowEgg = this.input.isInputPressed('throwEgg');
if (wantsThrowEgg && !this.oldWantsThrowEgg) {
this.throwEgg();
}
this.oldWantsThrowEgg = wantsThrowEgg;
/*
* Update the enemies, then the eggs.
*/
this.enemies.update();
if (NOW.valueOf() >= this.nextSpawnTime) {
this.nextSpawnTime += this.nextSpawnInterval;
this.enemies.spawnBeardson();
}
this.eggs.update();
/*
* See if any eggs have hit any enemies
*/
/*
* Render "back to front" (painter's algorithm)
*/
this.background.draw(ctx, 0, 0);
this.enemies.render(ctx);
this.eggs.render(ctx);
this.tex.render(ctx);
}
throwEgg ( ) {
const moveSpeed = 4 + (Math.random() * 2);
this.log.info('throwEgg', 'throwing egg');
this.eggs.throwEgg({
x: this.tex.position.x,
y: this.tex.position.y - 16,
}, moveSpeed);
this.audio.playSound('throw-egg-001');
}
async loadGameAssets ( ) {
this.background = new NiceImage(960, 540);
await this.background.load('/dist/assets/img/bg-001.jpg');
const jobs = [ ];
this.tex = new Tex(this);
this.background = new GameBackground(960, 540);
jobs.push(this.background.load('/dist/assets/img/bg-001.jpg'));
this.tex = new GamePlayer(this);
this.tex.position.x = 480;
this.tex.position.y = 470;
await this.tex.load();
this.tex.position.y = 460;
jobs.push(this.tex.load());
this.eggs = new GameEggSimulator(this);
jobs.push(this.eggs.load());
this.enemies = new GameEnemies(this);
jobs.push(this.enemies.load());
await Promise.all(jobs);
}
async loadAudio ( ) {
const jobs = [ ];
jobs.push(this.audio.loadSound('throw-egg-001', '/dist/assets/audio/sfx/throw-egg-001.wav'));
jobs.push(this.audio.loadSound('impact-001', '/dist/assets/audio/sfx/impact-001.wav'));
jobs.push(this.audio.loadSound('impact-002', '/dist/assets/audio/sfx/impact-002.wav'));
jobs.push(this.audio.loadSound('impact-003', '/dist/assets/audio/sfx/impact-003.wav'));
jobs.push(this.audio.loadSound('impact-004', '/dist/assets/audio/sfx/impact-004.wav'));
jobs.push(this.audio.loadSound('impact-005', '/dist/assets/audio/sfx/impact-005.wav'));
jobs.push(this.audio.loadSound('impact-006', '/dist/assets/audio/sfx/impact-006.wav'));
jobs.push(this.audio.loadSound('impact-007', '/dist/assets/audio/sfx/impact-007.wav'));
jobs.push(this.audio.loadSound('tex-stayfresh', '/dist/assets/audio/vox/tex-stayfresh.wav'));
jobs.push(this.audio.loadSound('tex-biggerfish', '/dist/assets/audio/vox/tex-biggerfish.wav'));
jobs.push(this.audio.loadSound('tex-shutuptwo', '/dist/assets/audio/vox/tex-shutuptwo.wav'));
await Promise.all(jobs);
}
}

41
dist/js/lib/game-background.js

@ -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"
*/
}
}

112
dist/js/lib/game-egg-simulator.js

@ -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);
}
}
}

109
dist/js/lib/game-enemies.js

@ -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)];
}
}

44
dist/js/lib/game-enemy-beardson.js

@ -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;
}
}
}

32
dist/js/lib/game-player.js

@ -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;
}
}
}

139
dist/js/lib/nice-audio.js

@ -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;
}
}

70
dist/js/lib/nice-game.js

@ -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,
};

25
dist/js/lib/nice-image.js

@ -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);
}
}

163
dist/js/lib/nice-input.js

@ -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');
}
}
}

125
dist/js/lib/nice-log.js

@ -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,
);
}
}
}

27
dist/js/lib/nice-sprite.js

@ -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),
);
}
}

2
minigame-engine.js

@ -27,6 +27,7 @@ module.getHomeView = async (req, res) => {
module.app.set('view engine', 'pug');
module.app.set('views', path.join(__dirname, 'views'));
module.app.use('/dtp-nice-game', express.static(path.join(__dirname, 'node_modules', 'dtp-nice-game', 'lib')));
module.app.use('/dist', express.static(path.join(__dirname, 'dist')));
module.app.get('/', module.getHomeView);
@ -34,4 +35,5 @@ module.getHomeView = async (req, res) => {
module.app.listen(3000, ( ) => {
console.log('CyberEgg 2077 is alive');
});
})();

1
package.json

@ -11,6 +11,7 @@
"license": "Apache-2.0",
"private": false,
"dependencies": {
"dtp-nice-game": "https://git.digitaltelepresence.com/digital-telepresence/dtp-nice-game.git",
"express": "^4.17.3",
"pug": "^3.0.2"
},

38
views/index.pug

@ -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

14
views/layout.pug

@ -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")

6
yarn.lock

@ -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…
Cancel
Save