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/*"]
}