From 9beca6cf7272d9222ffc3260db4d3572de948f9f Mon Sep 17 00:00:00 2001 From: Lars Berning <151194+laberning@users.noreply.github.com> Date: Mon, 14 Feb 2022 20:47:45 +0100 Subject: [PATCH] wires stroke fighter to rowing metrics --- app/client/arcade/GameEngine.js | 5 ++ app/client/arcade/RowingGames.js | 19 ++++- app/client/arcade/SpaceBackground.js | 4 + app/client/arcade/StrokeFighterBattleScene.js | 79 ++++++++++++++---- app/client/assets/sprites/ufoGreen.png | Bin 3049 -> 0 bytes app/client/components/AppDialog.js | 7 +- app/client/components/BatteryIcon.js | 2 +- app/client/components/DashboardActions.js | 8 +- app/client/components/DashboardMetric.js | 2 +- app/client/components/GameComponent.js | 13 ++- app/client/components/PerformanceDashboard.js | 13 ++- app/client/index.js | 32 ++++--- app/client/lib/app.js | 7 +- app/client/lib/helper.js | 7 +- app/client/lib/helper.test.js | 3 +- jsconfig.json | 6 +- 16 files changed, 154 insertions(+), 53 deletions(-) delete mode 100644 app/client/assets/sprites/ufoGreen.png diff --git a/app/client/arcade/GameEngine.js b/app/client/arcade/GameEngine.js index 46ad990..eab0a45 100644 --- a/app/client/arcade/GameEngine.js +++ b/app/client/arcade/GameEngine.js @@ -7,6 +7,11 @@ import kaboom from 'kaboom' +/** + * + * @param {import('kaboom').KaboomOpt} options Kaboom Options + * @returns KaboomCtx Kaboom Context + */ export function createGameEngine (options) { return kaboom(options) } diff --git a/app/client/arcade/RowingGames.js b/app/client/arcade/RowingGames.js index 3dd44a0..e8e88d2 100644 --- a/app/client/arcade/RowingGames.js +++ b/app/client/arcade/RowingGames.js @@ -10,7 +10,7 @@ import StrokeFighterBattleScene from './StrokeFighterBattleScene.js' /** * creates and initializes the rowing games - * @param {Element} canvasElement + * @param {HTMLCanvasElement} canvasElement * @param {number} clientWidth * @param {number} clientHeight */ @@ -29,7 +29,7 @@ export function createRowingGames (canvasElement, clientWidth, clientHeight) { // 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', 'ufoGreen', + 'spaceShips_004', 'spaceShips_006', 'spaceShips_007', 'spaceShips_009', 'star1', 'star2', 'laserRed01', 'laserRed09'] for (const sprite of sprites) { @@ -38,7 +38,20 @@ export function createRowingGames (canvasElement, clientWidth, clientHeight) { k.loadSound('hit', `${assets}/sounds/explosionCrunch_000.ogg`) k.loadSound('shoot', `${assets}/sounds/laserSmall_001.ogg`) - k.scene('strokeFighterBattle', () => { StrokeFighterBattleScene(k) }) + // todo: check if there is some kaboomish way to get the active scene + let activeScene + k.scene('strokeFighterBattle', () => { activeScene = StrokeFighterBattleScene(k) }) k.go('strokeFighterBattle') + + function appState (appState) { + if (activeScene?.appState) { + activeScene.appState(appState) + } + } + + return { + k, + appState + } } diff --git a/app/client/arcade/SpaceBackground.js b/app/client/arcade/SpaceBackground.js index 3bb8018..5498340 100644 --- a/app/client/arcade/SpaceBackground.js +++ b/app/client/arcade/SpaceBackground.js @@ -9,6 +9,10 @@ const STAR_SPEED = 20 const STAR_SPRITE_NAMES = ['star1', 'star2'] const STAR_NUM = 10 +/** + * adds a scrolling space background to the background layer + * @param {import('kaboom').KaboomCtx} k Kaboom Context + */ export default function addSpaceBackground (k) { k.add([ k.rect(k.width() + 50, k.height() + 50), diff --git a/app/client/arcade/StrokeFighterBattleScene.js b/app/client/arcade/StrokeFighterBattleScene.js index ee5f4b7..329f565 100644 --- a/app/client/arcade/StrokeFighterBattleScene.js +++ b/app/client/arcade/StrokeFighterBattleScene.js @@ -2,20 +2,31 @@ /* Open Rowing Monitor, https://github.com/laberning/openrowingmonitor - Implements the Battle Actions of the Stroke Fighter Game + Implements the Battle Action of the Stroke Fighter Game */ import addSpaceBackground from './SpaceBackground.js' -const ENEMY_SPRITE_NAMES = ['enemyBlack1', 'enemyBlue2', 'enemyGreen3', 'enemyRed4', 'enemyRed5', - 'spaceShips_004', 'spaceShips_006', 'spaceShips_007', 'spaceShips_009', 'ufoGreen'] - +/** + * Creates the main scene of Storke Fighter + * @param {import('kaboom').KaboomCtx} k Kaboom Context + */ export default function StrokeFighterBattleScene (k) { const BULLET_SPEED = 1200 const ENEMY_SPEED = 60 const PLAYER_SPEED = 480 - const ENEMY_HEALTH = 4 const SPRITE_WIDTH = 90 + const ENEMIES = [ + { sprite: 'enemyBlack1', health: 1 }, + { sprite: 'enemyBlue2', health: 1 }, + { sprite: 'enemyGreen3', health: 1 }, + { sprite: 'enemyRed4', health: 1 }, + { sprite: 'enemyRed5', health: 1 }, + { sprite: 'spaceShips_004', health: 3 }, + { sprite: 'spaceShips_006', health: 2 }, + { sprite: 'spaceShips_007', health: 3 }, + { sprite: 'spaceShips_009', health: 2 } + ] k.layers([ 'background', @@ -107,23 +118,36 @@ export default function StrokeFighterBattleScene (k) { ]) } - // todo: this should be triggered by a finished rowing drive phase - k.onKeyPress('space', () => { - spawnBullet(player.pos.sub(16, 15)) - spawnBullet(player.pos.add(16, -15)) + k.onKeyPress('space', () => { fireWeapons(2) }) + + /** + * fires the weapons of our spaceship + * @param {number} destructivePower the deadliness the weapon + */ + function fireWeapons (destructivePower) { + if (destructivePower <= 1) { + spawnBullet(player.pos.sub(0, 20)) + } else if (destructivePower <= 2) { + spawnBullet(player.pos.sub(16, 15)) + spawnBullet(player.pos.add(16, -15)) + } else { + spawnBullet(player.pos.sub(0, 20)) + spawnBullet(player.pos.sub(16, 15)) + spawnBullet(player.pos.add(16, -15)) + } k.play('shoot', { volume: 0.3, detune: k.rand(-1200, 1200) }) - }) + } function spawnEnemy () { - const name = k.choose(ENEMY_SPRITE_NAMES) + const enemy = k.choose(ENEMIES) k.add([ - k.sprite(name), + k.sprite(enemy.sprite), k.area(), k.pos(k.rand(0 + SPRITE_WIDTH / 2, k.width() - SPRITE_WIDTH / 2), 0), - k.health(ENEMY_HEALTH), + k.health(enemy.health), k.origin('bot'), 'enemy', { speed: k.rand(ENEMY_SPEED * 0.5, ENEMY_SPEED * 1.5) } @@ -133,6 +157,7 @@ export default function StrokeFighterBattleScene (k) { k.on('death', 'enemy', (e) => { k.destroy(e) + k.every('bullet', (bullet) => { k.destroy(bullet) }) k.shake(2) }) @@ -146,7 +171,7 @@ export default function StrokeFighterBattleScene (k) { }) const timer = k.add([ - k.text(0), + k.text('0'), k.pos(12, 32), k.fixed(), k.layer('ui'), @@ -170,5 +195,31 @@ export default function StrokeFighterBattleScene (k) { k.destroy(sprite) } }) + + let lastStrokeState = 'DRIVING' + function appState (appState) { + if (appState?.metrics.strokeState === undefined) { + return + } + if (lastStrokeState === 'DRIVING' && appState.metrics.strokeState === 'RECOVERY') { + driveFinished(appState.metrics) + } + lastStrokeState = appState.metrics.strokeState + } + + function driveFinished (metrics) { + if (metrics.power < 120) { + fireWeapons(1) + } else if (metrics.power < 180) { + fireWeapons(2) + } else { + fireWeapons(3) + } + } + spawnEnemy() + + return { + appState + } } diff --git a/app/client/assets/sprites/ufoGreen.png b/app/client/assets/sprites/ufoGreen.png deleted file mode 100644 index 7d441b2c5ff5f42d6ea61ad9503fb3f237f925f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3049 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 { + // @ts-ignore this.updateState(event.detail) }) // notify the app about the triggered action this.addEventListener('triggerAction', (event) => { + // @ts-ignore this.app.handleAction(event.detail) }) } - // the global state is updated by replacing the appState with a copy of the new state - // todo: maybe it is more convenient to just pass the state elements that should be changed? - // i.e. do something like this.appState = { ..this.appState, ...newState } + /** + * the global state is updated by replacing the appState with a copy of the new state + * todo: maybe it is more convenient to just pass the state elements that should be changed? + * i.e. do something like this.appState = { ..this.appState, ...newState } + * @param {Object} newState the new state of the application + */ updateState = (newState) => { this.appState = { ...newState } } - // return a deep copy of the state to other components to minimize risk of side effects + /** + * return a deep copy of the state to other components to minimize risk of side effects + * @returns Object + */ getState = () => { // could use structuredClone once the browser support is wider // https://developer.mozilla.org/en-US/docs/Web/API/structuredClone @@ -65,14 +70,15 @@ export class App extends LitElement { return html` ` } renderRowingGames () { return html` - + ` } diff --git a/app/client/lib/app.js b/app/client/lib/app.js index 34e973f..1bfd483 100644 --- a/app/client/lib/app.js +++ b/app/client/lib/app.js @@ -9,7 +9,7 @@ import NoSleep from 'nosleep.js' import { filterObjectByKeys } from './helper.js' const rowingMetricsFields = ['strokesTotal', 'distanceTotal', 'caloriesTotal', 'power', 'heartrate', - 'heartrateBatteryLevel', 'splitFormatted', 'strokesPerMinute', 'durationTotalFormatted'] + 'heartrateBatteryLevel', 'splitFormatted', 'strokesPerMinute', 'durationTotalFormatted', 'strokeState'] export function createApp (app) { const urlParameters = new URLSearchParams(window.location.search) @@ -60,7 +60,6 @@ export function createApp (app) { }, 1000) }) - // todo: we should use different types of messages to make processing easier socket.addEventListener('message', (event) => { try { const message = JSON.parse(event.data) @@ -122,6 +121,10 @@ export function createApp (app) { app.updateState(appState) } + /** + * triggers handling of action + * @param {Object} action type of action + */ function handleAction (action) { switch (action.command) { case 'switchPeripheralMode': { diff --git a/app/client/lib/helper.js b/app/client/lib/helper.js index 16bae13..448491d 100644 --- a/app/client/lib/helper.js +++ b/app/client/lib/helper.js @@ -5,7 +5,12 @@ Helper functions */ -// Filters an object so that it only contains the attributes that are defined in a list +/** + * Filters an object so that it only contains the attributes that are defined in a list + * @param {Object} object + * @param {Array} keys List of allowed attributs + * @returns Object + */ export function filterObjectByKeys (object, keys) { return Object.keys(object) .filter(key => keys.includes(key)) diff --git a/app/client/lib/helper.test.js b/app/client/lib/helper.test.js index 42fd02c..396b8fa 100644 --- a/app/client/lib/helper.test.js +++ b/app/client/lib/helper.test.js @@ -4,10 +4,9 @@ */ import { test } from 'uvu' import * as assert from 'uvu/assert' - import { filterObjectByKeys } from './helper.js' -test('filterd list should only contain the elements specified', () => { +test('filtered list should only contain the elements specified', () => { const object1 = { a: ['a1', 'a2'], b: 'b' diff --git a/jsconfig.json b/jsconfig.json index d2e2610..9bae86f 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -5,5 +5,9 @@ "checkJs": true, "esModuleInterop": true, "experimentalDecorators": true - } + }, + "editor.codeActionsOnSave": { + "source.organizeImports": false + }, + "exclude": ["node_modules", "**/node_modules/*"] }