Browse Source

The start of voice channels and enemy behaviors

- VoiceChannel and GameVoiceChannels for managing character dialog (vox)
- the start of enemy behaviors to drive enemy sprites
- Removed character-specific GameEnemy variants
develop
Rob Colbert 2 years ago
parent
commit
595aa59941
  1. BIN
      game/assets/audio/vox/tex-notreading.wav
  2. BIN
      game/assets/audio/vox/tex-quotepost.wav
  3. BIN
      game/assets/audio/vox/torba-controversy.wav
  4. BIN
      game/assets/audio/vox/torba-feminine.wav
  5. BIN
      game/assets/audio/vox/torba-stirthepot.wav
  6. BIN
      game/assets/audio/vox/torba-talkwithnick.wav
  7. 42
      game/js/game-app.js
  8. 94
      game/js/lib/game-enemies.js
  9. 51
      game/js/lib/game-enemy-beardson.js
  10. 50
      game/js/lib/game-enemy-behaviors.js
  11. 37
      game/js/lib/game-enemy-formation.js
  12. 46
      game/js/lib/game-enemy-groyper.js
  13. 46
      game/js/lib/game-enemy-torba.js
  14. 13
      game/js/lib/game-enemy.js
  15. 33
      game/js/lib/game-player.js
  16. 89
      game/js/lib/game-voice-channels.js

BIN
game/assets/audio/vox/tex-notreading.wav

Binary file not shown.

BIN
game/assets/audio/vox/tex-quotepost.wav

Binary file not shown.

BIN
game/assets/audio/vox/torba-controversy.wav

Binary file not shown.

BIN
game/assets/audio/vox/torba-feminine.wav

Binary file not shown.

BIN
game/assets/audio/vox/torba-stirthepot.wav

Binary file not shown.

BIN
game/assets/audio/vox/torba-talkwithnick.wav

Binary file not shown.

42
game/js/game-app.js

@ -23,18 +23,18 @@ import GameEggSimulator from './lib/game-egg-simulator.js';
import GameEnemies from './lib/game-enemies.js';
import GameEnemyFormation from './lib/game-enemy-formation.js';
import { GameVoiceChannels } from './lib/game-voice-channels.js';
export default class GameApp extends NiceGame {
constructor ( ) {
super(DTP_COMPONENT_NAME);
this.mode = 'starting';
this.initMenuOverlays();
this.mode = 'starting';
document.addEventListener('ngsdk:game-boot', this.onGameBoot.bind(this));
this.eggs = [ ];
this.playfield = {
width: 960,
height: 540,
@ -47,7 +47,7 @@ export default class GameApp extends NiceGame {
},
};
document.addEventListener('ngsdk:game-boot', this.onGameBoot.bind(this));
this.voiceChannels = new GameVoiceChannels(this);
}
async run ( ) {
@ -74,8 +74,6 @@ export default class GameApp extends NiceGame {
this.input.addButton('moveRight', '#btn-move-right');
this.input.addButton('throwEgg', '#btn-throw-egg');
this.input.addGamepadButton('system:start', 9);
this.input.addGamepadButton('system:back', 8);
this.input.addGamepadButton('throwEgg', 0);
this.input.addGamepadButton('moveLeft', 14);
this.input.addGamepadButton('moveRight', 15);
@ -122,6 +120,7 @@ export default class GameApp extends NiceGame {
startLevel ( ) {
const now = performance.now();
this.nextSpawnInterval = 1000 * 2;
this.nextSpawnTime = now + this.nextSpawnInterval;
this.formations = [ ];
@ -188,11 +187,13 @@ export default class GameApp extends NiceGame {
case 1:
this.enemies.spawnBeardson();
break;
case 2:
this.enemies.spawnTorba();
this.enemies.spawnGroyper();
break;
case 3:
this.enemies.spawnGroyper();
this.enemies.spawnTorba();
break;
}
}
@ -277,7 +278,7 @@ export default class GameApp extends NiceGame {
; // end createTween
}
getRandomPlayfieldX (padding) {
getRandomPlayfieldX (padding = 0) {
const fieldWidth = this.playfield.width - (this.playfield.margin * 2) - (padding * 2);
return this.playfield.margin + padding + (Math.random() * fieldWidth);
}
@ -321,14 +322,23 @@ export default class GameApp extends NiceGame {
this.player = new GamePlayer(this);
jobs.push(this.player.load());
this.eggs = new GameEggSimulator(this);
jobs.push(this.eggs.load());
this.enemies = new GameEnemies(this);
jobs.push(this.enemies.load());
this.eggs = new GameEggSimulator(this);
jobs.push(this.eggs.load());
jobs.push(this.loadImage('groyper', '/dist/assets/img/groyper.png', 64, 64));
jobs.push(this.loadImage('groyper.large', '/dist/assets/img/groyper.large.png', 180, 180));
jobs.push(this.loadImage('beardson', '/dist/assets/img/beardson.png', 38, 64));
jobs.push(this.loadImage('beardson.large', '/dist/assets/img/beardson.large.png', 108, 180));
jobs.push(this.loadImage('fuentes-puppy', '/dist/assets/img/fuentes-puppy-mode.png', 64, 64));
jobs.push(this.loadImage('fuentes-puppy.large', '/dist/assets/img/fuentes-puppy-mode.large.png', 180, 180));
jobs.push(this.loadImage('torba', '/dist/assets/img/torba.png', 52, 64));
jobs.push(this.loadImage('torba.large', '/dist/assets/img/torba.large.png', 146, 180));
await Promise.all(jobs);
}
@ -347,14 +357,6 @@ export default class GameApp extends NiceGame {
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-biggerfish', '/dist/assets/audio/vox/tex-biggerfish.wav'));
jobs.push(this.audio.loadSound('tex-candy', '/dist/assets/audio/vox/tex-candy.wav'));
jobs.push(this.audio.loadSound('tex-discontent', '/dist/assets/audio/vox/tex-discontent.wav'));
jobs.push(this.audio.loadSound('tex-not-reading','/dist/assets/audio/vox/tex-not-reading.wav'));
jobs.push(this.audio.loadSound('tex-quote-post', '/dist/assets/audio/vox/tex-quote-post.wav'));
jobs.push(this.audio.loadSound('tex-shutuptwo', '/dist/assets/audio/vox/tex-shutuptwo.wav'));
jobs.push(this.audio.loadSound('tex-stayfresh', '/dist/assets/audio/vox/tex-stayfresh.wav'));
jobs.push(this.audio.loadSound('beardson-touchdown', '/dist/assets/audio/sfx/pew-pew-pew.wav'));
await Promise.all(jobs);

94
game/js/lib/game-enemies.js

@ -6,11 +6,12 @@
const DTP_COMPONENT_NAME = 'game-enemies';
import { NiceLog, NiceImage, NiceColor, NiceEasing, NiceVector2d } from 'dtp-nice-game';
import { NiceLog, NiceColor, NiceVector2d } from 'dtp-nice-game';
import GameEnemyGroyper from './game-enemy-groyper.js';
import GameEnemyBeardson from './game-enemy-beardson.js';
import GameEnemyTorba from './game-enemy-torba.js';
import GameEnemy from './game-enemy.js';
import { DescendToSurface } from './game-enemy-behaviors.js';
const QUIP_PROBABILITY = 0.75;
/**
* GameEnemies manages the instances of "enemny" characters in play. They are
@ -26,34 +27,22 @@ export default class GameEnemies {
this.game = game;
this.moveSpeed = 1.0;
this.enemyImages = { };
this.enemies = [ ];
this.tweens = { };
this.voiceChannels = { };
this.voiceChannels.torba = this.game.voiceChannels.addChannel('torba');
}
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));
jobs.push(this.voiceChannels.torba.loadQuip('torba-stirthepot'));
jobs.push(this.voiceChannels.torba.loadQuip('torba-controversy'));
jobs.push(this.voiceChannels.torba.loadQuip('torba-talkwithnick'));
jobs.push(this.voiceChannels.torba.loadQuip('torba-feminine'));
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);
return Promise.all(jobs);
}
update ( ) {
@ -63,8 +52,8 @@ export default class GameEnemies {
return false;
}
this.game.audio.playSound(this.getRandomImpactSound());
if (Math.random() > 0.6) {
this.game.player.playRandomQuip();
if (Math.random() > QUIP_PROBABILITY) {
this.game.player.voiceChannel.playRandomQuip();
}
return true;
});
@ -81,55 +70,42 @@ export default class GameEnemies {
spawnBeardson ( ) {
this.log.debug('spawnBeardson', 'a wild Beardson approaches from the north');
const beardson = new GameEnemyBeardson(
this.game,
this.enemyImages.beardson,
this.getRandomSpawnPoint(this.enemyImages.beardson.width / 2),
{
x: this.moveSpeed * (1 + (Math.random() * 3)),
y: this.moveSpeed * (0.25 + (Math.random() * 0.75)),
},
);
this.enemies.push(beardson);
this.game.audio.playSound('enemy-spawn');
this.spawnDescender(this.game.images.beardson);
this.game.audio.playSound('enemy-spawn');
this.game.flashBorder(new NiceColor(255, 0, 0, 1.0));
}
spawnTorba ( ) {
this.log.debug('spawnTorba', 'a wild Torba approaches from the north');
const torba = new GameEnemyTorba(
this.game,
this.game.images.torba,
this.getRandomSpawnPoint(this.game.images.torba.width / 2),
{
x: this.moveSpeed * (1 + (Math.random() * 3)),
y: this.moveSpeed * (0.25 + (Math.random() * 0.75)),
},
);
this.enemies.push(torba);
this.game.audio.playSound('enemy-spawn');
this.spawnDescender(this.game.images.torba);
if (Math.random() > QUIP_PROBABILITY) {
this.voiceChannels.torba.playRandomQuip();
} else {
this.game.audio.playSound('enemy-spawn');
}
this.game.flashBorder(new NiceColor(255, 255, 0, 1.0));
}
spawnGroyper ( ) {
this.log.debug('spawnGroyper', 'a wild groyper approaches from the north');
const groyper = new GameEnemyGroyper(
this.game,
this.game.images.groyper,
this.getRandomSpawnPoint(this.game.images.groyper.width / 2),
{
x: this.moveSpeed * (1 + (Math.random() * 3)),
y: this.moveSpeed * (0.25 + (Math.random() * 0.75)),
},
);
this.enemies.push(groyper);
this.game.audio.playSound('enemy-spawn');
this.spawnDescender(this.game.images.groyper);
this.game.audio.playSound('enemy-spawn');
this.game.flashBorder(new NiceColor(0, 255, 0, 1.0));
}
spawnDescender (image) {
const enemy = new GameEnemy(this.game, image, this.getRandomSpawnPoint(image.width / 2));
enemy.addBehavior(new DescendToSurface(this.game, enemy, 0.25 + (Math.random() * 0.75)));
this.enemies.push(enemy);
return enemy;
}
getRandomSpawnPoint (padding = 0) {
return new NiceVector2d(this.game.getRandomPlayfieldX(padding), 32);
}

51
game/js/lib/game-enemy-beardson.js

@ -1,51 +0,0 @@
// lib/game-enemy-beardson.js
// Copyright (C) 2022 Rob Colbert @[email protected]
// License: Apache-2.0
'use strict';
import GameEnemy from './game-enemy.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 GameEnemy {
constructor (game, image, position, moveSpeed) {
super(game, image, position);
this.targetX = position.x;
this.moveSpeed = {
x: moveSpeed.x,
y: moveSpeed.y,
};
this.isDescending = true;
}
update ( ) {
if (this.isDescending) {
if (this.position.y < 480) {
this.position.y += this.moveSpeed.y;
} else {
this.position.y = 480;
this.isDescending = false;
this.game.audio.playSound('beardson-touchdown');
}
}
if (Math.random() > 0.998) {
this.targetX = this.game.getRandomPlayfieldX();
}
if (this.position.x > this.targetX) {
this.position.x -= this.moveSpeed.x;
}
if (this.position.x < this.targetX) {
this.position.x += this.moveSpeed.x;
}
}
}

50
game/js/lib/game-enemy-behaviors.js

@ -0,0 +1,50 @@
// lib/game-enemy-behaviors.js
// Copyright (C) 2022 Rob Colbert @[email protected]
// License: Apache-2.0
'use strict';
export class DescendToSurface {
constructor (game, enemy, moveSpeed) {
this.game = game;
this.enemy = enemy;
this.moveSpeed = moveSpeed;
this.isDescending = true;
this.targetX = this.enemy.position.x;
}
update (/* elapsed, now */) {
if (this.isDescending) {
if (this.enemy.position.y < 480) {
this.enemy.position.y += this.moveSpeed;
} else {
this.enemy.position.y = 480;
this.isDescending = false;
this.game.audio.playSound('beardson-touchdown');
return false; // end of behavior
}
}
if (Math.random() > 0.998) {
this.targetX = this.game.getRandomPlayfieldX(this.enemy.image.width / 2);
}
if (this.enemy.position.x > this.targetX) {
this.enemy.position.x -= this.moveSpeed;
if (this.enemy.position.x < this.targetX) {
this.enemy.position.x = this.targetX;
}
}
if (this.enemy.position.x < this.targetX) {
this.enemy.position.x += this.moveSpeed;
if (this.enemy.position.x > this.targetX) {
this.enemy.position.x = this.targetX;
}
}
return true; // keep running
}
}

37
game/js/lib/game-enemy-formation.js

@ -7,48 +7,17 @@
// import NiceVector2d from 'dtp-nice-game/lib/nice-vector-2d.js';
/**
* A formation is a collection of enemies with tiers, and enemies are
* added to the tiers. They then render as a "formation" in the view,
* and can "pop out" of formation to execute a "run" then arrive back to
* the formation.
*/
export default class GameEnemyFormation {
constructor (game, position) {
this.game = game;
this.position = position;
this.moveSpeed = 2;
this.tiers = [ ];
this.enemies = [ ];
}
/**
*
* @param {*} tierHeight
* @returns
*/
addTier (tierHeight = 64) {
const idx = this.tiers.length;
this.tiers.push({ height: tierHeight, enemies: [ ] });
return idx;
}
/**
* Adds an enemy object to the specified tier at the specified position
* offset. This is where the enemy will reside while in formation.
* @param {Number} tierIndex the index of the tier to which the enemy should be
* added
* @param {NiceSprite} enemy The enemy to be added to the specified tier
* @param {NiceVector2d} offset The positional offset from the formation's origin at
* which the enemy should reside while in formation
* @returns the index of the enemy added within the tier
*/
addEnemyToTier (tierIndex, enemy, offset) {
const idx = this.tiers[tierIndex].enemies.length;
this.tiers[tierIndex].enemies.push({
enemy, offset,
isInFormation: true,
});
return idx;
addEnemy (enemy, position) {
this.enemies = enemy;
}
update ( ) {

46
game/js/lib/game-enemy-groyper.js

@ -1,46 +0,0 @@
// lib/game-enemy-groyper.js
// Copyright (C) 2022 Rob Colbert @[email protected]
// License: Apache-2.0
'use strict';
import GameEnemy from './game-enemy.js';
export default class GameEnemyGroyper extends GameEnemy {
constructor (game, image, position, moveSpeed) {
super(game, image, position);
this.targetX = position.x;
this.moveSpeed = {
x: moveSpeed.x,
y: moveSpeed.y,
};
this.isDescending = true;
}
update ( ) {
if (this.isDescending) {
if (this.position.y < 480) {
this.position.y += this.moveSpeed.y;
} else {
this.position.y = 480;
this.isDescending = false;
this.game.audio.playSound('beardson-touchdown');
}
}
if (Math.random() > 0.998) {
this.targetX = this.game.getRandomPlayfieldX();
}
if (this.position.x > this.targetX) {
this.position.x -= this.moveSpeed.x;
}
if (this.position.x < this.targetX) {
this.position.x += this.moveSpeed.x;
}
}
}

46
game/js/lib/game-enemy-torba.js

@ -1,46 +0,0 @@
// lib/game-enemy-torba.js
// Copyright (C) 2022 Rob Colbert @[email protected]
// License: Apache-2.0
'use strict';
import GameEnemy from './game-enemy.js';
export default class GameEnemyTorba extends GameEnemy {
constructor (game, image, position, moveSpeed) {
super(game, image, position);
this.targetX = position.x;
this.moveSpeed = {
x: moveSpeed.x,
y: moveSpeed.y,
};
this.isDescending = true;
}
update ( ) {
if (this.isDescending) {
if (this.position.y < 480) {
this.position.y += this.moveSpeed.y;
} else {
this.position.y = 480;
this.isDescending = false;
this.game.audio.playSound('beardson-touchdown');
}
}
if (Math.random() > 0.998) {
this.targetX = this.game.getRandomPlayfieldX();
}
if (this.position.x > this.targetX) {
this.position.x -= this.moveSpeed.x;
}
if (this.position.x < this.targetX) {
this.position.x += this.moveSpeed.x;
}
}
}

13
game/js/lib/game-enemy.js

@ -11,6 +11,17 @@ export default class GameEnemy extends NiceSprite {
constructor (game, image, position) {
super(game, position);
this.image = image;
this.game = game;
this.behaviors = [ ];
}
addBehavior (behavior) {
this.behaviors.push(behavior);
}
update (elapsed, now) {
const behavior = this.behaviors[0];
if (behavior && !behavior.update(elapsed, now)) {
this.behaviors.shift();
}
}
}

33
game/js/lib/game-player.js

@ -27,11 +27,7 @@ export default class GamePlayer extends NiceSprite {
this.margin = CELL_WIDTH / 2;
this.quips = [ ];
}
async loadQuip (id) {
this.quips.push(id);
return this.game.audio.loadSound(id, `/dist/assets/audio/vox/${id}.wav`);
this.voiceChannel = this.game.voiceChannels.addChannel('player');
}
async load ( ) {
@ -39,13 +35,13 @@ export default class GamePlayer extends NiceSprite {
jobs.push(super.load('/dist/assets/img/big-baja-tex.png', CELL_WIDTH, CELL_HEIGHT));
jobs.push(this.loadQuip('tex-biggerfish'));
jobs.push(this.loadQuip('tex-candy'));
jobs.push(this.loadQuip('tex-discontent'));
jobs.push(this.loadQuip('tex-not-reading'));
jobs.push(this.loadQuip('tex-quote-post'));
jobs.push(this.loadQuip('tex-shutuptwo'));
jobs.push(this.loadQuip('tex-stayfresh'));
jobs.push(this.voiceChannel.loadQuip('tex-biggerfish'));
jobs.push(this.voiceChannel.loadQuip('tex-candy'));
jobs.push(this.voiceChannel.loadQuip('tex-discontent'));
jobs.push(this.voiceChannel.loadQuip('tex-notreading'));
jobs.push(this.voiceChannel.loadQuip('tex-quotepost'));
jobs.push(this.voiceChannel.loadQuip('tex-shutuptwo'));
jobs.push(this.voiceChannel.loadQuip('tex-stayfresh'));
return Promise.all(jobs);
}
@ -63,17 +59,4 @@ export default class GamePlayer extends NiceSprite {
}
}
}
playRandomQuip ( ) {
if (this.playingQuip) {
return;
}
const { source } = this.game.audio.playSound(this.getRandomQuipId());
source.addEventListener('ended', ( ) => { this.playingQuip = false; });
this.playingQuip = true;
}
getRandomQuipId ( ) {
return this.quips[Math.floor(Math.random() * this.quips.length)];
}
}

89
game/js/lib/game-voice-channels.js

@ -0,0 +1,89 @@
// lib/game-voice-channels.js
// Copyright (C) 2022 Rob Colbert @[email protected]
// License: Apache-2.0
'use strict';
import { NiceLog } from "dtp-nice-game";
export class VoiceChannel {
constructor (game, name) {
this.game = game;
this.name = name;
this.log = new NiceLog(`VoiceChannel:${name}`);
this.log.enable();
this.playing = false;
this.quips = [ ];
}
async loadQuip (soundId) {
if (this.quips.includes(soundId)) {
throw new Error(`Quip ${soundId} has already been loaded`);
}
this.quips.push(soundId);
return this.game.audio.loadSound(soundId, `/dist/assets/audio/vox/${soundId}.wav`);
}
playRandomQuip ( ) {
this.log.debug('playRandomQuip', 'request to play random quipt', { playing: this.playing });
if (this.playing) {
return;
}
const soundId = this.getRandomQuipSoundId();
this.log.debug('playRandomQuip', 'playing random voiceover sound', { soundId });
const { source } = this.game.audio.playSound(soundId);
source.addEventListener('ended', ( ) => {
this.log.debug('playRandomQuip', 'quip ending', { soundId });
this.playing = false;
});
this.playing = true;
}
getRandomQuipSoundId ( ) {
if (!Array.isArray(this.quips) || (this.quips.length === 0)) {
throw new Error(`No valid quips loaded on channel "${this.name}"`);
}
const quipIdx = Math.floor(Math.random() * (this.quips.length - 1));
const quipSoundId = this.quips[quipIdx];
this.log.debug('getRandomQuipSoundId', 'selected a random quip', { quipIdx, quipSoundId });
return quipSoundId;
}
}
/**
* GameVoiceChannels tracks audio playing in named channels and will prevent the
* game from playing a sound on a channel if a sound is currently playing on
* that channel.
*/
export class GameVoiceChannels {
constructor (game) {
this.game = game;
this.channels = { };
}
addChannel (channelName) {
const channel = new VoiceChannel(this.game, channelName);
this.channels[channelName] = channel;
return channel;
}
getChannel (channelName) {
const channel = this.channels[channelName];
if (!channel) {
throw new Error(`Invalid voice channel "${channelName}"`);
}
return channel;
}
playSound (channelName, soundId) {
const channel = this.getChannel(channelName);
channel.playSound(soundId);
}
}
Loading…
Cancel
Save