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 0000000..87f38d9 Binary files /dev/null and b/app/client/assets/sprites/playerLife2_orange@2x.png differ diff --git a/app/client/assets/sprites/shield1@2x.png b/app/client/assets/sprites/shield1@2x.png new file mode 100644 index 0000000..3ff74f0 Binary files /dev/null and b/app/client/assets/sprites/shield1@2x.png differ