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 7d441b2..0000000 Binary files a/app/client/assets/sprites/ufoGreen.png and /dev/null differ diff --git a/app/client/components/AppDialog.js b/app/client/components/AppDialog.js index eab5d2e..1392268 100644 --- a/app/client/components/AppDialog.js +++ b/app/client/components/AppDialog.js @@ -5,9 +5,9 @@ Component that renders a html dialog */ -import { AppElement, html, css } from './AppElement.js' import { customElement, property } from 'lit/decorators.js' -import { ref, createRef } from 'lit/directives/ref.js' +import { createRef, ref } from 'lit/directives/ref.js' +import { AppElement, css, html } from './AppElement.js' @customElement('app-dialog') export class AppDialog extends AppElement { @@ -96,14 +96,17 @@ export class AppDialog extends AppElement { } firstUpdated () { + // @ts-ignore this.dialog.value.showModal() } updated (changedProperties) { if (changedProperties.has('dialogOpen')) { if (this.dialogOpen) { + // @ts-ignore this.dialog.value.showModal() } else { + // @ts-ignore this.dialog.value.close() } } diff --git a/app/client/components/BatteryIcon.js b/app/client/components/BatteryIcon.js index db8c34a..0d2d8d0 100644 --- a/app/client/components/BatteryIcon.js +++ b/app/client/components/BatteryIcon.js @@ -5,8 +5,8 @@ Component that renders a battery indicator */ -import { AppElement, svg, css } from './AppElement.js' import { customElement, property } from 'lit/decorators.js' +import { AppElement, css, svg } from './AppElement.js' @customElement('battery-icon') export class DashboardMetric extends AppElement { diff --git a/app/client/components/DashboardActions.js b/app/client/components/DashboardActions.js index 458a16e..9f9ee73 100644 --- a/app/client/components/DashboardActions.js +++ b/app/client/components/DashboardActions.js @@ -5,10 +5,10 @@ Component that renders the action buttons of the dashboard */ -import { AppElement, html, css } from './AppElement.js' import { customElement, state } from 'lit/decorators.js' -import { icon_undo, icon_expand, icon_compress, icon_poweroff, icon_bluetooth, icon_upload, icon_gamepad } from '../lib/icons.js' +import { icon_bluetooth, icon_compress, icon_expand, icon_gamepad, icon_poweroff, icon_undo, icon_upload } from '../lib/icons.js' import './AppDialog.js' +import { AppElement, css, html } from './AppElement.js' @customElement('dashboard-actions') export class DashboardActions extends AppElement { @@ -58,7 +58,7 @@ export class DashboardActions extends AppElement { } ` - @state({ type: Object }) + @state() dialog render () { @@ -148,6 +148,7 @@ export class DashboardActions extends AppElement { function dialogClosed (event) { this.dialog = undefined if (event.detail === 'confirm') { + // @ts-ignore this.sendEvent('triggerAction', { command: 'uploadTraining' }) } } @@ -163,6 +164,7 @@ export class DashboardActions extends AppElement { function dialogClosed (event) { this.dialog = undefined if (event.detail === 'confirm') { + // @ts-ignore this.sendEvent('triggerAction', { command: 'shutdown' }) } } diff --git a/app/client/components/DashboardMetric.js b/app/client/components/DashboardMetric.js index 185c89f..a02a873 100644 --- a/app/client/components/DashboardMetric.js +++ b/app/client/components/DashboardMetric.js @@ -5,8 +5,8 @@ Component that renders a metric of the dashboard */ -import { AppElement, html, css } from './AppElement.js' import { customElement, property } from 'lit/decorators.js' +import { AppElement, css, html } from './AppElement.js' @customElement('dashboard-metric') export class DashboardMetric extends AppElement { diff --git a/app/client/components/GameComponent.js b/app/client/components/GameComponent.js index 177d89f..f62f8c7 100644 --- a/app/client/components/GameComponent.js +++ b/app/client/components/GameComponent.js @@ -5,9 +5,9 @@ Wrapper for the Open Rowing Monitor rowing games */ -import { AppElement, html, css } from './AppElement.js' import { customElement } from 'lit/decorators.js' import { createRowingGames } from '../arcade/RowingGames.js' +import { AppElement, css, html } from './AppElement.js' @customElement('game-component') export class GameComponent extends AppElement { @@ -28,6 +28,15 @@ export class GameComponent extends AppElement { firstUpdated () { const canvas = this.renderRoot.querySelector('#arcade') - createRowingGames(canvas, canvas.clientWidth, canvas.clientHeight) + // @ts-ignore + this.rowingGames = createRowingGames(canvas, canvas.clientWidth, canvas.clientHeight) + } + + updated (changedProperties) { + if (changedProperties.has('appState')) { + if (this.rowingGames !== undefined) { + this.rowingGames.appState(changedProperties.get('appState')) + } + } } } diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js index 0d04013..d75afea 100644 --- a/app/client/components/PerformanceDashboard.js +++ b/app/client/components/PerformanceDashboard.js @@ -5,13 +5,13 @@ Component that renders the dashboard */ -import { AppElement, html, css } from './AppElement.js' -import { APP_STATE } from '../store/appState.js' import { customElement, property } from 'lit/decorators.js' -import './DashboardMetric.js' -import './DashboardActions.js' +import { icon_bolt, icon_clock, icon_fire, icon_heartbeat, icon_paddle, icon_route, icon_stopwatch } from '../lib/icons.js' +import { APP_STATE } from '../store/appState.js' +import { AppElement, css, html } from './AppElement.js' import './BatteryIcon.js' -import { icon_route, icon_stopwatch, icon_bolt, icon_paddle, icon_heartbeat, icon_fire, icon_clock } from '../lib/icons.js' +import './DashboardActions.js' +import './DashboardMetric.js' @customElement('performance-dashboard') export class PerformanceDashboard extends AppElement { @@ -45,9 +45,6 @@ export class PerformanceDashboard extends AppElement { } ` - @property({ type: Object }) - metrics - @property({ type: Object }) appState = APP_STATE diff --git a/app/client/index.js b/app/client/index.js index 153e18f..698a655 100644 --- a/app/client/index.js +++ b/app/client/index.js @@ -5,21 +5,18 @@ Main Initialization Component of the Web Component App */ -import { LitElement, html } from 'lit' +import { html, LitElement } from 'lit' import { customElement, state } from 'lit/decorators.js' -import { APP_STATE } from './store/appState.js' -import { createApp } from './lib/app.js' -import './components/PerformanceDashboard.js' import './components/GameComponent.js' +import './components/PerformanceDashboard.js' +import { createApp } from './lib/app.js' +import { APP_STATE } from './store/appState.js' @customElement('web-app') export class App extends LitElement { @state() appState = APP_STATE - @state() - metrics - constructor () { super() @@ -33,23 +30,31 @@ export class App extends LitElement { // once any child component sends this CustomEvent we update the global state according // to the changes that were passed to us this.addEventListener('appStateChanged', (event) => { + // @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/*"] }