From 156591fb51af596c61d0e3ee6d24d387d9e8399a Mon Sep 17 00:00:00 2001 From: Lars Berning <151194+laberning@users.noreply.github.com> Date: Mon, 21 Feb 2022 20:22:19 +0100 Subject: [PATCH] implements stroke fighter insane mode --- app/client/arcade/RowingGames.js | 7 ++-- app/client/arcade/StrokeFighterBattleScene.js | 24 ++++++++---- app/client/arcade/StrokeFighterEndScene.js | 38 ++++++++++++++++--- app/client/arcade/StrokeFighterStartScene.js | 17 +++++---- app/client/components/GameComponent.js | 8 ++-- 5 files changed, 66 insertions(+), 28 deletions(-) diff --git a/app/client/arcade/RowingGames.js b/app/client/arcade/RowingGames.js index e102b63..557c7c7 100644 --- a/app/client/arcade/RowingGames.js +++ b/app/client/arcade/RowingGames.js @@ -28,7 +28,7 @@ export function createRowingGames (rootComponent, clientWidth, clientHeight) { font: 'sinko' }) - // todo: once there are multiple games, asset loadingshould be moved to the individual games + // todo: once there are multiple games, asset loading should be moved to the individual games const assets = '/assets' const sprites = ['enemyBlack1', 'enemyBlue2', 'enemyGreen3', 'enemyRed4', 'enemyRed5', 'playerShip2_orange', 'playerLife2_orange', 'spaceShips_004', 'spaceShips_006', 'spaceShips_007', 'spaceShips_009', 'star1', 'star2', @@ -56,8 +56,9 @@ export function createRowingGames (rootComponent, clientWidth, clientHeight) { } } - // todo: currently we move to an empty scene to dispose the game as there does not seem to be - // a mechanism in kaboom to dispose the instance. + /** + * clean up the game resources + */ function dispose () { k.quit() } diff --git a/app/client/arcade/StrokeFighterBattleScene.js b/app/client/arcade/StrokeFighterBattleScene.js index afed1d1..ea7e505 100644 --- a/app/client/arcade/StrokeFighterBattleScene.js +++ b/app/client/arcade/StrokeFighterBattleScene.js @@ -8,7 +8,7 @@ import addSpaceBackground from './SpaceBackground.js' /** - * Creates the main scene of Storke Fighter + * Creates the main scene of Stroke Fighter * @param {import('kaboom').KaboomCtx} k Kaboom Context */ export default function StrokeFighterBattleScene (k, args) { @@ -21,7 +21,7 @@ export default function StrokeFighterBattleScene (k, args) { // strokes per minute at end of training const SPM_END = 28 const BULLET_SPEED = 1200 - const ENEMY_SPEED = 60 + const ENEMY_SPEED = 50 const PLAYER_SPEED = 480 const PLAYER_LIFES = 3 const SPRITE_WIDTH = 90 @@ -72,13 +72,10 @@ export default function StrokeFighterBattleScene (k, args) { k.area(), k.opacity(0.4), k.pos(player.pos), + k.follow(player), k.origin('center') ]) - shield.onUpdate(() => { - shield.pos = player.pos - }) - shield.onCollide('enemy', (enemy) => { k.destroy(enemy) k.shake(4) @@ -331,8 +328,10 @@ export default function StrokeFighterBattleScene (k, args) { function scheduleNextEnemy () { const percentTrainingFinished = trainingTime / TARGET_TIME - const currentSPM = SPM_START + (SPM_END - SPM_START) * percentTrainingFinished + // linearly increase the SPM over time + let currentSPM = SPM_START + (SPM_END - SPM_START) * percentTrainingFinished let maxEnemyHealth = 1 + let minEnemyHealth = 1 if (percentTrainingFinished < 0.4) { maxEnemyHealth = 1 } else if (percentTrainingFinished < 0.8) { @@ -340,7 +339,16 @@ export default function StrokeFighterBattleScene (k, args) { } else { maxEnemyHealth = 3 } - spawnEnemy(k.choose(ENEMIES.filter((enemy) => enemy.health <= maxEnemyHealth))) + // insane mode (keep on rowing after winning) + if (percentTrainingFinished > 1) { + // cap SPM at 20% above SPM_END (for insane mode) + currentSPM = Math.max(currentSPM, SPM_END * 1.2) + minEnemyHealth = 2 + if (percentTrainingFinished > 1.3) { + minEnemyHealth = 3 + } + } + spawnEnemy(k.choose(ENEMIES.filter((enemy) => enemy.health >= minEnemyHealth && enemy.health <= maxEnemyHealth))) k.wait(60 / currentSPM, scheduleNextEnemy) } diff --git a/app/client/arcade/StrokeFighterEndScene.js b/app/client/arcade/StrokeFighterEndScene.js index 1c79209..0330ec6 100644 --- a/app/client/arcade/StrokeFighterEndScene.js +++ b/app/client/arcade/StrokeFighterEndScene.js @@ -39,33 +39,61 @@ export default function StrokeFighterEndScene (k, args) { k.pos(k.width() / 2, 320), k.origin('center') ]) + const restartButton = k.add([ + k.rect(300, 60), + k.area(), + k.pos(k.width() / 2, 440), + k.scale(1), + k.origin('center'), + k.outline(2, k.rgb(255, 255, 255)), + k.color(54, 80, 128) + ]) + const restartText = k.add([ k.text('Restart', { size: 40 }), k.area({ cursor: 'pointer' }), k.pos(k.width() / 2, 440), - k.origin('center') + k.scale(1), + k.origin('center'), + k.color(255, 255, 255) ]) if (args?.overtimePossible) { if (args?.gameState === 'LOST') { k.add([ k.text('or keep rowing to continue your workout', { size: 18 }), - k.pos(k.width() / 2, 550), + k.pos(k.width() / 2, 650), k.origin('center') ]) } else { k.add([ k.text('or keep rowing for an insane challenge', { size: 18 }), - k.pos(k.width() / 2, 550), + k.pos(k.width() / 2, 650), k.origin('center') ]) } } - restartButton.onClick(() => { - console.log('click') k.go('strokeFighterStart') }) + restartButton.onUpdate(() => { + if (restartButton.isHovering()) { + k.cursor('pointer') + restartButton.scale = k.vec2(1.2) + restartText.scale = k.vec2(1.2) + const t = k.time() * 10 + restartButton.color = k.rgb( + k.wave(0, 255, t), + k.wave(0, 255, t + 2), + k.wave(0, 255, t + 4) + ) + } else { + k.cursor('default') + restartButton.scale = k.vec2(1) + restartText.scale = k.vec2(1) + restartButton.color = k.rgb(54, 80, 128) + } + }) let motionDetectionEnabled = false if (args?.overtimePossible) { diff --git a/app/client/arcade/StrokeFighterStartScene.js b/app/client/arcade/StrokeFighterStartScene.js index a535e22..8a96d52 100644 --- a/app/client/arcade/StrokeFighterStartScene.js +++ b/app/client/arcade/StrokeFighterStartScene.js @@ -8,7 +8,7 @@ import addSpaceBackground from './SpaceBackground.js' /** - * Creates the main scene of Storke Fighter + * Creates the main scene of Stroke Fighter * @param {import('kaboom').KaboomCtx} k Kaboom Context */ export default function StrokeFighterStartScene (k, args) { @@ -19,13 +19,8 @@ export default function StrokeFighterStartScene (k, args) { k.pos(k.width() / 2, 50), k.origin('center') ]) - k.add([ - k.text('start rowing...', { size: 40 }), - k.pos(k.width() / 2, 110), - k.origin('center') - ]) - const shipsPos = k.vec2(450, 260) + const shipsPos = k.vec2(520, 240) const ship1 = k.add([ k.sprite('playerShip2_orange'), k.scale(0.5), @@ -53,7 +48,7 @@ export default function StrokeFighterStartScene (k, args) { addBullet(ship3.pos.sub(20, 40)) addBullet(ship3.pos.sub(-20, 40)) - const explainPos = k.vec2(40, 260) + const explainPos = k.vec2(60, 240) k.add([ k.text('light stroke = ', { size: 28 }), k.pos(explainPos), @@ -79,6 +74,12 @@ export default function StrokeFighterStartScene (k, args) { ]) } + k.add([ + k.text('start rowing to charge lasers', { size: 28 }), + k.pos(k.width() / 2, 650), + k.origin('center') + ]) + let motionDetectionEnabled = false k.wait(5, () => { motionDetectionEnabled = true diff --git a/app/client/components/GameComponent.js b/app/client/components/GameComponent.js index 3120aff..efce4a6 100644 --- a/app/client/components/GameComponent.js +++ b/app/client/components/GameComponent.js @@ -119,7 +119,7 @@ export class GameComponent extends AppElement { // depending on the resolution // using a square screen has the advantage that it works well on portrait and landscape screens // for now will set it to a fixed square resolution and let css take care of scaling it - const gameSize = 600 + const gameSize = 700 const arcade = this.renderRoot.querySelector('#arcade') // this.rowingGames = createRowingGames(arcade, canvas.clientWidth, canvas.clientHeight) // @ts-ignore @@ -130,10 +130,10 @@ export class GameComponent extends AppElement { // change notifiers available in this component), then the state changes will be processed by the // game with a certain delay. This is pretty weird, since they are processed by this component at // the correct time. Also when we look at timestamps in the games callback, then it seems that they - // are called completely in sync with the event and without dely. - // This problem only occures, when the update events are created from a web request (i.e. by receiving + // are called completely in sync with the event and without delay. + // This problem only occurs, when the update events are created from a web request (i.e. by receiving // new rowing metrics via web socket). - // By delivering the app state updates directly here from index.js, this problem does not occure. + // By delivering the app state updates directly here from index.js, this problem does not occur. this.sendEvent('setGameStateUpdater', (appState) => { this.gameAppState(appState) }) }