From 4cd8d9c699b831905e5d54389f9d09dd221397cd Mon Sep 17 00:00:00 2001 From: Lars Berning <151194+laberning@users.noreply.github.com> Date: Sun, 20 Feb 2022 20:39:32 +0100 Subject: [PATCH] adds a game over screen --- app/client/arcade/RowingGames.js | 13 +-- app/client/arcade/StrokeFighterBattleScene.js | 58 ++++++++++++- .../arcade/StrokeFighterGameOverScene.js | 78 ++++++++++++++++++ app/client/arcade/StrokeFighterStartScene.js | 21 +++-- .../assets/sprites/playerLife2_orange@2x.png | Bin 0 -> 1384 bytes app/client/assets/sprites/shield1@2x.png | Bin 0 -> 3684 bytes 6 files changed, 154 insertions(+), 16 deletions(-) create mode 100644 app/client/arcade/StrokeFighterGameOverScene.js create mode 100644 app/client/assets/sprites/playerLife2_orange@2x.png create mode 100644 app/client/assets/sprites/shield1@2x.png diff --git a/app/client/arcade/RowingGames.js b/app/client/arcade/RowingGames.js index 088994b..3847d3d 100644 --- a/app/client/arcade/RowingGames.js +++ b/app/client/arcade/RowingGames.js @@ -8,6 +8,7 @@ import kaboom from 'kaboom' import StrokeFighterBattleScene from './StrokeFighterBattleScene.js' import StrokeFighterStartScene from './StrokeFighterStartScene.js' +import StrokeFighterGameOverScene from './StrokeFighterGameOverScene.js' /** * creates and initializes the rowing games @@ -23,7 +24,8 @@ export function createRowingGames (rootComponent, canvasElement, clientWidth, cl root: rootComponent, crisp: false, width: clientWidth, - height: clientHeight + height: clientHeight, + font: 'sinko' }) // for now show debug infos all the time // k.debug.inspect = true @@ -31,8 +33,8 @@ export function createRowingGames (rootComponent, canvasElement, clientWidth, cl // todo: once there are multiple games, asset loadingshould be moved to the individual games const assets = '/assets' const sprites = ['enemyBlack1', 'enemyBlue2', 'enemyGreen3', 'enemyRed4', 'enemyRed5', 'playerShip2_orange', - 'spaceShips_004', 'spaceShips_006', 'spaceShips_007', 'spaceShips_009', 'star1', 'star2', - 'laserRed01', 'laserRed09'] + 'playerLife2_orange', 'spaceShips_004', 'spaceShips_006', 'spaceShips_007', 'spaceShips_009', 'star1', 'star2', + 'laserRed01', 'laserRed09', 'shield1'] for (const sprite of sprites) { k.loadSprite(sprite, `${assets}/sprites/${sprite}@2x.png`) @@ -42,8 +44,9 @@ export function createRowingGames (rootComponent, canvasElement, clientWidth, cl // todo: check if there is some kaboomish way to get the active scene let activeScene - k.scene('strokeFighterBattle', () => { activeScene = StrokeFighterBattleScene(k) }) - k.scene('strokeFighterStart', () => { activeScene = StrokeFighterStartScene(k) }) + k.scene('strokeFighterBattle', (args) => { activeScene = StrokeFighterBattleScene(k, args) }) + k.scene('strokeFighterStart', (args) => { activeScene = StrokeFighterStartScene(k, args) }) + k.scene('strokeFighterGameOver', (args) => { activeScene = StrokeFighterGameOverScene(k, args) }) k.go('strokeFighterStart') diff --git a/app/client/arcade/StrokeFighterBattleScene.js b/app/client/arcade/StrokeFighterBattleScene.js index 741d4b6..7666545 100644 --- a/app/client/arcade/StrokeFighterBattleScene.js +++ b/app/client/arcade/StrokeFighterBattleScene.js @@ -11,7 +11,7 @@ import addSpaceBackground from './SpaceBackground.js' * Creates the main scene of Storke Fighter * @param {import('kaboom').KaboomCtx} k Kaboom Context */ -export default function StrokeFighterBattleScene (k) { +export default function StrokeFighterBattleScene (k, args) { // how much stroke power is needed to fire high power lasers const THRESHOLD_POWER = 180 // training duration in seconds @@ -23,6 +23,7 @@ export default function StrokeFighterBattleScene (k) { const BULLET_SPEED = 1200 const ENEMY_SPEED = 60 const PLAYER_SPEED = 480 + const PLAYER_LIFES = 3 const SPRITE_WIDTH = 90 const ENEMIES = [ { sprite: 'enemyBlack1', health: 1 }, @@ -36,7 +37,8 @@ export default function StrokeFighterBattleScene (k) { { sprite: 'spaceShips_009', health: 2 } ] - let trainingTime = 0 + let trainingTime = args?.trainingTime || 0 + let playerLifes = args?.gameOver ? 0 : PLAYER_LIFES const ui = k.add([ k.fixed(), @@ -63,6 +65,31 @@ export default function StrokeFighterBattleScene (k) { k.origin('center') ]) + if (args?.gameOver) { + const shield = k.add([ + k.sprite('shield1'), + k.scale(0.5), + k.area(), + k.opacity(0.4), + k.pos(player.pos), + k.origin('center') + ]) + + shield.onUpdate(() => { + shield.pos = player.pos + }) + + shield.onCollide('enemy', (enemy) => { + k.destroy(enemy) + k.shake(4) + k.play('hit', { + detune: -1200, + volume: 0.6, + speed: k.rand(0.5, 2) + }) + }) + } + function moveLeft () { player.move(-PLAYER_SPEED, 0) if (player.pos.x < 0) { @@ -80,11 +107,19 @@ export default function StrokeFighterBattleScene (k) { player.onCollide('enemy', (enemy) => { k.destroy(enemy) k.shake(4) + background.redflash() k.play('hit', { detune: -1200, volume: 0.6, speed: k.rand(0.5, 2) }) + playerLifes -= 1 + drawPlayerLifes() + if (playerLifes <= 0) { + k.go('strokeFighterGameOver', { + trainingTime + }) + } }) player.onUpdate(() => { @@ -142,7 +177,6 @@ export default function StrokeFighterBattleScene (k) { spawnBullet(player.pos.sub(20, 40)) spawnBullet(player.pos.sub(-20, 40)) } else { - background.redflash() spawnBullet(player.pos.sub(0, 65)) spawnBullet(player.pos.sub(20, 40)) spawnBullet(player.pos.sub(-20, 40)) @@ -185,7 +219,7 @@ export default function StrokeFighterBattleScene (k) { }) const timer = ui.add([ - k.text('00:00', { size: 25, font: 'sinko' }), + k.text('00:00', { size: 25 }), k.pos(10, 10), k.fixed() ]) @@ -200,6 +234,21 @@ export default function StrokeFighterBattleScene (k) { } }) + function drawPlayerLifes () { + k.destroyAll('playerLife') + + // todo: would want to draw these on the "ui", but not sure on how to delete them then... + for (let i = 1; i <= playerLifes; i++) { + k.add([ + k.sprite('playerLife2_orange'), + k.scale(0.5), + k.pos(k.width() - i * 40, 10), + k.z(100), + 'playerLife' + ]) + } + } + // converts a timestamp in seconds to a human readable hh:mm:ss format function secondsToTimeString (secondsTimeStamp) { if (secondsTimeStamp === Infinity) return '∞' @@ -260,6 +309,7 @@ export default function StrokeFighterBattleScene (k) { k.wait(60 / currentSPM, scheduleNextEnemy) } + drawPlayerLifes() scheduleNextEnemy() return { diff --git a/app/client/arcade/StrokeFighterGameOverScene.js b/app/client/arcade/StrokeFighterGameOverScene.js new file mode 100644 index 0000000..3932668 --- /dev/null +++ b/app/client/arcade/StrokeFighterGameOverScene.js @@ -0,0 +1,78 @@ +'use strict' +/* + Open Rowing Monitor, https://github.com/laberning/openrowingmonitor + + Implements the Start Screen of the Stroke Fighter Game +*/ + +import addSpaceBackground from './SpaceBackground.js' + +/** + * Creates the main scene of Storke Fighter + * @param {import('kaboom').KaboomCtx} k Kaboom Context + */ +export default function StrokeFighterGameOverScene (k, args) { + addSpaceBackground(k) + + k.add([ + k.text('Stroke Fighter', { size: 50 }), + k.pos(k.width() / 2, 50), + k.origin('center') + ]) + k.add([ + k.text('Game Over', { size: 40 }), + k.pos(k.width() / 2, 180), + k.origin('center') + ]) + k.add([ + k.sprite('playerShip2_orange'), + k.scale(0.5), + k.pos(k.width() / 2, 320), + k.origin('center') + ]) + const restartButton = k.add([ + k.text('Restart', { size: 40 }), + k.area({ cursor: 'pointer' }), + k.pos(k.width() / 2, 440), + k.origin('center') + ]) + k.add([ + k.text('... or keep rowing to finish your workout', { size: 18 }), + k.pos(k.width() / 2, 550), + k.origin('center') + ]) + restartButton.onClick(() => { + console.log('click') + k.go('strokeFighterStart') + }) + + let motionDetectionEnabled = false + k.wait(5, () => { + motionDetectionEnabled = true + }) + + let lastStrokeState = 'DRIVING' + function appState (appState) { + if (!motionDetectionEnabled) { + return + } + if (appState?.metrics.strokeState === undefined) { + return + } + if (lastStrokeState === 'DRIVING' && appState.metrics.strokeState === 'RECOVERY') { + driveFinished(appState.metrics) + } + lastStrokeState = appState.metrics.strokeState + } + + function driveFinished (metrics) { + k.go('strokeFighterBattle', { + gameOver: true, + trainingTime: args?.trainingTime + }) + } + + return { + appState + } +} diff --git a/app/client/arcade/StrokeFighterStartScene.js b/app/client/arcade/StrokeFighterStartScene.js index e66f4fd..a535e22 100644 --- a/app/client/arcade/StrokeFighterStartScene.js +++ b/app/client/arcade/StrokeFighterStartScene.js @@ -11,16 +11,16 @@ import addSpaceBackground from './SpaceBackground.js' * Creates the main scene of Storke Fighter * @param {import('kaboom').KaboomCtx} k Kaboom Context */ -export default function StrokeFighterStartScene (k) { +export default function StrokeFighterStartScene (k, args) { addSpaceBackground(k) k.add([ - k.text('Stroke Fighter', { size: 50, font: 'sinko' }), + k.text('Stroke Fighter', { size: 50 }), k.pos(k.width() / 2, 50), k.origin('center') ]) k.add([ - k.text('start rowing...', { size: 40, font: 'sinko' }), + k.text('start rowing...', { size: 40 }), k.pos(k.width() / 2, 110), k.origin('center') ]) @@ -55,17 +55,17 @@ export default function StrokeFighterStartScene (k) { const explainPos = k.vec2(40, 260) k.add([ - k.text('light stroke = ', { size: 28, font: 'sinko' }), + k.text('light stroke = ', { size: 28 }), k.pos(explainPos), k.origin('left') ]) k.add([ - k.text('normal stroke = ', { size: 28, font: 'sinko' }), + k.text('normal stroke = ', { size: 28 }), k.pos(explainPos.add(0, 140)), k.origin('left') ]) k.add([ - k.text('heavy stroke = ', { size: 28, font: 'sinko' }), + k.text('heavy stroke = ', { size: 28 }), k.pos(explainPos.add(0, 280)), k.origin('left') ]) @@ -79,8 +79,15 @@ export default function StrokeFighterStartScene (k) { ]) } + let motionDetectionEnabled = false + k.wait(5, () => { + motionDetectionEnabled = true + }) let lastStrokeState = 'DRIVING' function appState (appState) { + if (!motionDetectionEnabled) { + return + } if (appState?.metrics.strokeState === undefined) { return } @@ -91,7 +98,7 @@ export default function StrokeFighterStartScene (k) { } function driveFinished (metrics) { - k.wait(2, () => { k.go('strokeFighterBattle') }) + k.go('strokeFighterBattle') } return { diff --git a/app/client/assets/sprites/playerLife2_orange@2x.png b/app/client/assets/sprites/playerLife2_orange@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..87f38d9fe798267bcf449c0d132ed0eaebba330f GIT binary patch literal 1384 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 y0K*emGuQcaqp?b{Dc{