Browse Source

wip: refactoring to Nice Arcade SDK

NiceImage, NiceSprite, NiceInput, NiceAudio, NiceGame and related.
develop
Rob Colbert 2 years ago
parent
commit
c572bb9fa5
  1. 111
      dist/js/cyberegg2077.js
  2. 137
      dist/js/lib/nice-audio.js
  3. 54
      dist/js/lib/nice-game.js
  4. 68
      dist/js/lib/nice-input.js
  5. 4
      dist/js/lib/nice-sprite.js
  6. 123
      dist/js/nice-app.js
  7. 1
      minigame-engine.js
  8. 8
      views/index.pug

111
dist/js/cyberegg2077.js

@ -0,0 +1,111 @@
// cyberegg2077.js
// Copyright (C) 2022 Rob Colbert @[email protected]
// License: Apache-2.0
'use strict';
const DTP_COMPONENT_NAME = 'nice-game';
import { NiceImage, NiceSprite, NiceGame } from '/dist/js/lib/nice-game.js';
class Tex extends NiceSprite {
constructor ( ) {
super();
this.input = {
moveLeft: false,
moveRight: false,
};
this.moveSpeed = 2;
}
async load ( ) { return super.load('/dist/assets/img/big-baja-tex.png', 82, 128); }
update ( ) {
if (this.input.moveLeft) {
this.position.x -= this.moveSpeed;
}
if (this.input.moveRight) {
this.position.x += this.moveSpeed;
}
}
}
class Egg {
constructor (image) {
this.image = image;
this.position = { x: 0, y: 0 };
}
render (ctx) {
ctx.drawImage(
this.sprite.image,
this.position.x - (this.sprite.image.width / 2),
this.position.y - (this.sprite.image.height / 2),
);
}
}
class CyberEgg2077 extends NiceGame {
constructor ( ) {
super(DTP_COMPONENT_NAME);
this.startButton = document.getElementById('start-button');
this.startButton.addEventListener('click', this.onStartGame.bind(this));
}
async run ( ) {
await this.startGameEngine(this.onUpdateDisplay.bind(this));
this.input.addKey('moveLeft', 'LeftArrow');
this.input.addKey('moveRight', 'RightArrow');
this.input.addKey('throwEgg', 'Space');
this.input.addButton('moveLeft', 'btn-move-left');
this.input.addButton('moveRight', 'btn-move-right');
this.input.addButton('throwEgg', 'btn-throw-egg');
await this.audio.setMusicStream('/dist/assets/audio/cyber_pulse.ogg');
}
async onStartGame (/* event */) {
this.audio.music.play();
this.startButton.setAttribute('hidden', '');
this.tex.moveSpeed = 2;
}
onUpdateDisplay (ctx) {
/*
* Update game objects, run the logic, woo.
*/
this.tex.update();
/*
* Render the game scene using painter's algorithm (back to front)
*/
this.background.draw(ctx, 0, 0);
this.tex.render(ctx);
}
async loadGameAssets ( ) {
this.background = new NiceImage(960, 540);
await this.background.load('/dist/assets/img/bg-001.jpg');
this.tex = new Tex();
this.tex.position.x = 480;
this.tex.position.y = 470;
await this.tex.load();
}
}
window.addEventListener('load', async ( ) => {
window.game = new CyberEgg2077();
window.game.run();
});

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

@ -4,9 +4,142 @@
'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 {
async load ( ) {
this.music = new Audio('/dist/assets/audio/cyber_pulse.ogg');
constructor ( ) {
this.log = new NiceLog(DTP_COMPONENT_NAME);
this._musicVolume = 0.7;
}
start ( ) {
this.ctx = new AudioContext();
this.masterVolume = this.ctx.createGain();
this.masterVolume.connect(this.ctx.destination);
}
/**
* 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);
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.
*/
async setMusicStream (url) {
if (this.music) {
this.log.debug('setMusicStream', 'stopping current music');
this.music.pause();
delete this.music;
}
this.log.debug('setMusicStream', 'setting new music stream', { url });
this.music = new Audio(url);
this.music.volume = this.musicVolume;
this.musicGain = this.ctx.createGain();
this.musicGain.value = this._musicVolume;
this.musicGain.connect(this.masterVolume);
this.musicSource = this.ctx.createMediaElementSource(this.music);
this.musicSource.connect(this.musicGain);
this.music.play();
}
async playMusicStream ( ) {
if (!this.musicSource || !this.music.paused) {
return;
}
this.log.debug('playMusicStream', 'starting music stream playback');
this.musicGain.value = this.musicVolume;
this.music.play();
}
async pauseMusicStream ( ) {
if (!this.musicSource || this.music.paused) {
return;
}
this.log.debug('pauseMusicStream', 'stopping music stream playback');
this.music.pause();
this.musicGain.value = 0;
}
get musicVolume ( ) { return this._musicVolume; }
set musicVolume (volume) {
this._musicVolume = volume;
if (this.musicGain) {
this.musicGain.value = volume;
}
}
get haveMusicStream ( ) {
return !!this.music &&
!!this.musicSource &&
!!this.musicGain
;
}
get isMusicPaused ( ) {
if (!this.music) {
return true;
}
return this.music.paused;
}
}

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

@ -0,0 +1,54 @@
// 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 { NiceInput } from '/dist/js/lib/nice-input.js';
import NiceImage from './nice-image.js';
import NiceSprite from './nice-sprite.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();
}
window.requestAnimationFrame(this.onUpdateDisplay);
}
}
export { NiceLog, NiceAudio, NiceInput, NiceImage, NiceSprite, NiceGame };

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

@ -31,8 +31,8 @@ export class NiceInputTools {
*/
export class NiceInputButton {
constructor (name, buttonId) {
this.name = name;
constructor (actionName, buttonId) {
this.actionName = actionName;
this.isPressed = false;
this.element = document.getElementById(buttonId);
@ -73,6 +73,15 @@ export class NiceInputButton {
}
}
export class NiceInputKey {
constructor (actionName, keyName) {
this.actionName = actionName;
this.keyName = keyName;
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
@ -86,47 +95,46 @@ export class NiceInput {
constructor ( ) {
this.buttons = { };
this.inputs = { };
// moveLeft: document.getElementById('btn-move-left'),
// moveRight: document.getElementById('btn-move-right'),
// throwEgg: document.getElementById('btn-throw-egg'),
}
addButton (name, buttonId) {
const button = new NiceInputButton(name, buttonId);
this.buttons[name] = button;
}
this.keys = { };
initInput ( ) {
document.addEventListener('keydown', this.onKeyDown.bind(this));
document.addEventListener('keyup', this.onKeyUp.bind(this));
}
addButton (actionName, buttonId) {
const button = new NiceInputButton(actionName, buttonId);
this.buttons[actionName] = button;
}
addKey (actionName, keyName) {
const key = new NiceInputKey (actionName, keyName);
this.keys[actionName] = key;
}
async onKeyDown (event) {
this.log.debug('onKeyDown', 'key pressed', { event });
switch (event.key) {
case 'ArrowRight':
this.buttons.moveRight.classList.add('active');
this.tex.input.moveRight = true;
break;
case 'ArrowLeft':
this.buttons.moveLeft.classList.add('active');
this.tex.input.moveLeft = true;
break;
const key = this.keys[event.key];
if (!key) { return; }
key.isPressed = true;
const button = this.buttons[key.actionName];
if (button) {
button.classList.add('active');
}
}
async onKeyUp (event) {
this.log.debug('onKeyUp', 'key released', { event });
switch (event.key) {
case 'ArrowRight':
this.buttons.moveRight.classList.remove('active');
this.tex.input.moveRight = false;
break;
case 'ArrowLeft':
this.buttons.moveLeft.classList.remove('active');
this.tex.input.moveLeft = false;
break;
const key = this.keys[event.key];
if (!key) { return; }
key.isPressed = false;
const button = this.buttons[key.actionName];
if (button) {
button.classList.remove('active');
}
}
}

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

@ -4,9 +4,9 @@
'use strict';
import NiceImage from '/dist/js/lib/nice-image.js';
import NiceImage from './nice-image.js';
export default class WebSprite {
export default class NiceSprite {
constructor ( ) {
this.position = { x: 0, y: 0 };

123
dist/js/nice-app.js

@ -1,123 +0,0 @@
// nice-app.js
// Copyright (C) 2022 Rob Colbert
// License: Apache-2.0
'use strict';
const DTP_COMPONENT_NAME = 'nice-app';
import NiceLog from '/dist/js/lib/nice-log.js';
import NiceAudio from '/dist/js/lib/nice-audio.js';
import NiceSprite from '/dist/js/lib/nice-sprite.js';
import NiceImage from '/dist/js/lib/nice-image.js';
class Tex extends NiceSprite {
constructor ( ) {
super();
this.input = {
moveLeft: false,
moveRight: false,
};
this.moveSpeed = 2;
}
async load ( ) { return super.load('/dist/assets/img/big-baja-tex.png', 82, 128); }
update ( ) {
if (this.input.moveLeft) {
this.position.x -= this.moveSpeed;
}
if (this.input.moveRight) {
this.position.x += this.moveSpeed;
}
}
}
class Egg {
constructor (image) {
this.image = image;
this.position = { x: 0, y: 0 };
}
render (ctx) {
ctx.drawImage(
this.sprite.image,
this.position.x - (this.sprite.image.width / 2),
this.position.y - (this.sprite.image.height / 2),
);
}
}
class NiceApp {
constructor ( ) {
this.log = new NiceLog(DTP_COMPONENT_NAME);
this.log.enable(true);
this.sprites = { };
this.eggs = [ ];
this.startButton = document.getElementById('start-button');
this.log.info('constructor', 'CyberEgg 2077 online');
}
async startGameEngine ( ) {
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.initInput();
await this.loadGameAssets();
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);
}
async startGame ( ) {
this.audio.music.play();
this.startButton.setAttribute('hidden', '');
this.tex.moveSpeed = 2;
}
updateDisplay ( ) {
this.tex.update();
this.background.draw(this.gameDisplayCtx, 0, 0);
this.tex.render(this.gameDisplayCtx);
window.requestAnimationFrame(this.onUpdateDisplay);
}
async loadGameAssets ( ) {
this.audio = new NiceAudio();
await this.audio.load();
this.background = new NiceImage(960, 540);
await this.background.load('/dist/assets/img/bg-001.jpg');
this.tex = new Tex();
this.tex.position.x = 480;
this.tex.position.y = 470;
await this.tex.load();
}
}
window.addEventListener('load', async ( ) => {
console.log('window:load');
window.app = new NiceApp();
await window.app.startGameEngine();
});

1
minigame-engine.js

@ -13,6 +13,7 @@ module.config = {
};
module.getHomeView = async (req, res) => {
res.locals.gameModuleUrl = '/dist/js/cyberegg2077.js';
res.render('index');
};

8
views/index.pug

@ -7,8 +7,6 @@ html(lang="en")
link(rel="stylesheet" href=`/dist/css/style.css?v=${pkg.version}`)
script(src=`/dist/js/app.js?v=${pkg.version}`, type="module")
body
.container
section.text-center
@ -20,9 +18,11 @@ html(lang="en")
.margin-block
.game-display-wrapper
canvas(id="game-display" width="960" height="540").game-display
button(type="button", onclick="return app.startGame(event);").start-button#start-button Start Game
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
.notice a #[a(href="https://nicecrew.digital") #nicecrew] exclusive
script(src=`${gameModuleUrl}?v=${pkg.version}`, type="module")
Loading…
Cancel
Save