You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
380 lines
10 KiB
380 lines
10 KiB
// game-app.js
|
|
// Copyright (C) 2022 Rob Colbert @[email protected]
|
|
// License: Apache-2.0
|
|
|
|
'use strict';
|
|
|
|
const DTP_COMPONENT_NAME = 'game-app';
|
|
import '../less/style.less';
|
|
|
|
import {
|
|
NiceColor,
|
|
NiceEasing,
|
|
NiceGame,
|
|
NiceInputTools,
|
|
NiceMath,
|
|
NiceVector2d,
|
|
} from 'dtp-nice-game';
|
|
|
|
import GameBackground from './lib/game-background.js';
|
|
import GamePlayer from './lib/game-player.js';
|
|
|
|
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();
|
|
document.addEventListener('ngsdk:game-boot', this.onGameBoot.bind(this));
|
|
|
|
this.eggs = [ ];
|
|
this.playfield = {
|
|
width: 960,
|
|
height: 540,
|
|
margin: 120,
|
|
colors: {
|
|
margin: {
|
|
fill: new NiceColor(0, 0, 0, 0.8),
|
|
stroke: new NiceColor(32,32,64, 1.0),
|
|
},
|
|
},
|
|
};
|
|
|
|
this.voiceChannels = new GameVoiceChannels(this);
|
|
}
|
|
|
|
async run ( ) {
|
|
await this.startGameEngine(
|
|
this.onGameUpdate.bind(this),
|
|
this.onGameRender.bind(this),
|
|
);
|
|
}
|
|
|
|
async onGameBoot (event) {
|
|
NiceInputTools.cancelEvent(event);
|
|
|
|
this.gameDisplay.appendChild(this.overlays.mainMenu);
|
|
|
|
/*
|
|
* Initialize the inputs this game needs
|
|
*/
|
|
|
|
this.input.addKey('moveLeft', 'ArrowLeft');
|
|
this.input.addKey('moveRight', 'ArrowRight');
|
|
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');
|
|
|
|
this.input.addGamepadButton('throwEgg', 0);
|
|
this.input.addGamepadButton('moveLeft', 14);
|
|
this.input.addGamepadButton('moveRight', 15);
|
|
|
|
this.startMusicStream('/dist/assets/audio/music/blueberries');
|
|
|
|
await this.loadGameAssets();
|
|
|
|
this.mode = 'menu';
|
|
this.background.setMode('story');
|
|
}
|
|
|
|
async onStartGame (event) {
|
|
NiceInputTools.cancelEvent(event);
|
|
await this.startNewGame();
|
|
}
|
|
|
|
async startMusicStream (url) {
|
|
await this.audio.setMusicStream(url);
|
|
this.audio.playMusicStream();
|
|
this.audio.musicVolume = 0.4;
|
|
}
|
|
|
|
async startNewGame ( ) {
|
|
this.mode = 'loading';
|
|
|
|
this.gameDisplay.removeChild(this.overlays.mainMenu);
|
|
this.gameDisplay.appendChild(this.overlays.gameControls);
|
|
|
|
this.startMusicStream('/dist/assets/audio/music/idra');
|
|
await this.loadAudio();
|
|
|
|
this.player.moveSpeed = 2;
|
|
|
|
this.eggs.clear();
|
|
this.oldWantsThrowEgg = false;
|
|
|
|
this.mode = 'game';
|
|
this.level = 1;
|
|
this.startLevel();
|
|
|
|
this.background.setMode('play');
|
|
}
|
|
|
|
startLevel ( ) {
|
|
const now = performance.now();
|
|
|
|
this.spawnRandomEnemy();
|
|
this.nextSpawnInterval = 1800 * 2;
|
|
this.nextSpawnTime = now + this.nextSpawnInterval;
|
|
|
|
this.formations = [ ];
|
|
}
|
|
|
|
onGameUpdate (elapsed, now) {
|
|
this.background.update(elapsed, now);
|
|
|
|
switch (this.mode) {
|
|
case 'loading':
|
|
break;
|
|
case 'menu':
|
|
return this.updateMenu(elapsed, now);
|
|
case 'game':
|
|
return this.updateGame(elapsed, now);
|
|
}
|
|
}
|
|
|
|
onGameRender (ctx) {
|
|
switch (this.mode) {
|
|
case 'loading':
|
|
break;
|
|
case 'menu':
|
|
return this.renderMenu(ctx);
|
|
case 'game':
|
|
return this.renderGame(ctx);
|
|
}
|
|
}
|
|
|
|
updateMenu (/* elapsed, now */) {
|
|
if (this.input.isInputPressed('system:start')) {
|
|
this.startNewGame();
|
|
}
|
|
}
|
|
|
|
renderMenu (ctx) {
|
|
this.background.render(ctx);
|
|
this.player.render(ctx);
|
|
}
|
|
|
|
updateGame (elapsed, now) {
|
|
this.player.update(elapsed, now);
|
|
|
|
/*
|
|
* 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;
|
|
|
|
this.eggs.update(elapsed, now);
|
|
this.enemies.update(elapsed, now);
|
|
|
|
/*
|
|
* Spawn an enemy from the groyper army at scheduled intervals.
|
|
*/
|
|
if (now >= this.nextSpawnTime) {
|
|
this.nextSpawnTime += this.nextSpawnInterval;
|
|
this.spawnRandomEnemy();
|
|
}
|
|
|
|
/*
|
|
* Process any current formations
|
|
*/
|
|
for (const formation of this.formations) {
|
|
formation.update();
|
|
}
|
|
}
|
|
|
|
spawnRandomEnemy ( ) {
|
|
switch (NiceMath.randomRangeInt(1, 4)) {
|
|
case 1:
|
|
this.enemies.spawnBeardson();
|
|
break;
|
|
|
|
case 2:
|
|
this.enemies.spawnGroyper();
|
|
break;
|
|
|
|
case 3:
|
|
this.enemies.spawnTorba();
|
|
break;
|
|
|
|
case 4:
|
|
this.enemies.spawnFuentesFagFace();
|
|
break;
|
|
}
|
|
}
|
|
|
|
renderGame (ctx) {
|
|
this.background.render(ctx);
|
|
|
|
this.enemies.render(ctx);
|
|
for (const formation of this.formations) {
|
|
formation.render(ctx);
|
|
}
|
|
|
|
this.renderPlayfield(ctx);
|
|
|
|
this.eggs.render(ctx);
|
|
this.player.render(ctx);
|
|
}
|
|
|
|
renderPlayfield (ctx) {
|
|
ctx.fillStyle = this.playfield.colors.margin.fill.toCssString();
|
|
ctx.fillRect(0, 0, this.playfield.margin, this.playfield.height);
|
|
ctx.fillRect(this.playfield.width - this.playfield.margin, 0, this.playfield.margin, this.playfield.height);
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.moveTo(this.playfield.margin, 0);
|
|
ctx.lineTo(this.playfield.margin, this.playfield.height);
|
|
|
|
ctx.moveTo(this.playfield.width - this.playfield.margin, 0);
|
|
ctx.lineTo(this.playfield.width - this.playfield.margin, this.playfield.height);
|
|
|
|
ctx.strokeStyle = this.playfield.colors.margin.stroke.toCssString();
|
|
ctx.lineWidth = 3.0;
|
|
ctx.stroke();
|
|
}
|
|
|
|
throwEgg ( ) {
|
|
const moveSpeed = 4 + (Math.random() * 2);
|
|
this.log.info('throwEgg', 'throwing egg');
|
|
this.eggs.throwEgg(new NiceVector2d(
|
|
this.player.position.x,
|
|
this.player.position.y - 48,
|
|
), moveSpeed);
|
|
|
|
this.audio.playSound('throw-egg-001');
|
|
}
|
|
|
|
spawnFormation ( ) {
|
|
const formation = new GameEnemyFormation(this, new NiceVector2d(480, 20));
|
|
formation.addTier(3);
|
|
formation.addTier(7);
|
|
}
|
|
|
|
flashBorder (color) {
|
|
this
|
|
.createTween(
|
|
this.playfield.colors.margin.stroke,
|
|
{
|
|
r: color.r,
|
|
g: color.g,
|
|
b: color.b,
|
|
a: color.a,
|
|
},
|
|
{
|
|
r: this.playfield.colors.margin.stroke.r,
|
|
g: this.playfield.colors.margin.stroke.g,
|
|
b: this.playfield.colors.margin.stroke.b,
|
|
a: this.playfield.colors.margin.stroke.a,
|
|
},
|
|
NiceEasing.Bounce.InOut,
|
|
)
|
|
.duration(1000)
|
|
.run()
|
|
; // end createTween
|
|
}
|
|
|
|
getRandomPlayfieldX (padding = 0) {
|
|
const fieldWidth = this.playfield.width - (this.playfield.margin * 2) - (padding * 2);
|
|
return this.playfield.margin + padding + (Math.random() * fieldWidth);
|
|
}
|
|
|
|
initMenuOverlays ( ) {
|
|
/*
|
|
* Attach to each expected menu overlay
|
|
*/
|
|
this.addOverlay('debugDisplay', '.menu-overlay#debug-display');
|
|
this.addOverlay('mainMenu', '.menu-overlay#main-menu');
|
|
this.addOverlay('gameControls', '.menu-overlay#game-controls');
|
|
|
|
document.addEventListener('ngsdk:gamepad-connected', this.onGamepadConnected.bind(this));
|
|
|
|
this.startButton = this.overlays.mainMenu.querySelector('button#start-button');
|
|
this.startButton.onclick = this.onStartGame.bind(this);
|
|
|
|
this.gameDisplay.removeChild(this.overlays.mainMenu);
|
|
this.overlays.mainMenu.removeAttribute('hidden');
|
|
|
|
this.gameDisplay.removeChild(this.overlays.gameControls);
|
|
this.overlays.gameControls.removeAttribute('hidden');
|
|
|
|
this.overlays.systemMenu.removeAttribute('hidden');
|
|
}
|
|
|
|
async onGamepadConnected (event) {
|
|
const { gamepad } = event.detail;
|
|
|
|
this.log.info('onGamepadConnected', 'gamepad connected', { gamepad });
|
|
const gamepadFeedback = this.overlays.systemMenu.querySelector('span#gamepad-feedback');
|
|
gamepadFeedback.textContent = 'CONNECTED';
|
|
}
|
|
|
|
async loadGameAssets ( ) {
|
|
const jobs = [ ];
|
|
|
|
this.background = new GameBackground(this);
|
|
jobs.push(this.background.load());
|
|
|
|
this.player = new GamePlayer(this);
|
|
jobs.push(this.player.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-fagface', '/dist/assets/img/fuentes-fagface.png', 49, 64));
|
|
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);
|
|
}
|
|
|
|
async loadAudio ( ) {
|
|
const jobs = [ ];
|
|
|
|
jobs.push(this.audio.loadSound('enemy-spawn', '/dist/assets/audio/sfx/spawn.ogg'));
|
|
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('beardson-touchdown', '/dist/assets/audio/sfx/pew-pew-pew.wav'));
|
|
|
|
return Promise.all(jobs);
|
|
}
|
|
}
|
|
|
|
window.addEventListener('load', async ( ) => {
|
|
window.game = new GameApp();
|
|
window.game.run();
|
|
});
|