diff --git a/app/client/arcade/RowingGames.js b/app/client/arcade/RowingGames.js
index 1b31ba2..0989c3c 100644
--- a/app/client/arcade/RowingGames.js
+++ b/app/client/arcade/RowingGames.js
@@ -7,6 +7,7 @@
import { createGameEngine } from './GameEngine.js'
import StrokeFighterBattleScene from './StrokeFighterBattleScene.js'
+import StrokeFighterStartScene from './StrokeFighterStartScene.js'
/**
* creates and initializes the rowing games
@@ -41,10 +42,13 @@ export function createRowingGames (canvasElement, clientWidth, clientHeight) {
// 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('disposed', () => { })
- k.go('strokeFighterBattle')
+ k.go('strokeFighterStart')
+ // on changes, forward the appState to the active scene, used to monitor the rowing metrics
+ // from the game scene
function appState (appState) {
if (activeScene?.appState) {
activeScene.appState(appState)
diff --git a/app/client/arcade/StrokeFighterBattleScene.js b/app/client/arcade/StrokeFighterBattleScene.js
index 63f53e6..566829c 100644
--- a/app/client/arcade/StrokeFighterBattleScene.js
+++ b/app/client/arcade/StrokeFighterBattleScene.js
@@ -136,15 +136,15 @@ export default function StrokeFighterBattleScene (k) {
*/
function fireWeapons (destructivePower) {
if (destructivePower <= 1) {
- spawnBullet(player.pos.sub(0, 20))
+ spawnBullet(player.pos.sub(0, 65))
} else if (destructivePower <= 2) {
- spawnBullet(player.pos.sub(16, 15))
- spawnBullet(player.pos.add(16, -15))
+ spawnBullet(player.pos.sub(20, 40))
+ spawnBullet(player.pos.sub(-20, 40))
} else {
- background.redify()
- spawnBullet(player.pos.sub(0, 20))
- spawnBullet(player.pos.sub(16, 15))
- spawnBullet(player.pos.add(16, -15))
+ background.redflash()
+ spawnBullet(player.pos.sub(0, 65))
+ spawnBullet(player.pos.sub(20, 40))
+ spawnBullet(player.pos.sub(-20, 40))
}
k.play('shoot', {
volume: 0.6,
@@ -183,9 +183,8 @@ export default function StrokeFighterBattleScene (k) {
})
const timer = k.add([
- k.text('00:00'),
- k.scale(0.8),
- k.pos(12, 32),
+ k.text('00:00', { size: 25, font: 'sinko' }),
+ k.pos(10, 10),
k.fixed(),
k.layer('ui')
])
diff --git a/app/client/arcade/StrokeFighterStartScene.js b/app/client/arcade/StrokeFighterStartScene.js
new file mode 100644
index 0000000..2e08864
--- /dev/null
+++ b/app/client/arcade/StrokeFighterStartScene.js
@@ -0,0 +1,101 @@
+'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 StrokeFighterStartScene (k) {
+ k.layers([
+ 'background',
+ 'ui'
+ ], 'ui')
+
+ addSpaceBackground(k)
+
+ k.add([
+ k.text('Stroke Fighter', { size: 50, font: 'sinko' }),
+ k.pos(k.width() / 2, 50),
+ k.origin('center')
+ ])
+ k.add([
+ k.text('start rowing...', { size: 40, font: 'sinko' }),
+ k.pos(k.width() / 2, 110),
+ k.origin('center')
+ ])
+
+ const shipsPos = k.vec2(450, 260)
+ const ship1 = k.add([
+ k.sprite('playerShip2_orange'),
+ k.pos(shipsPos),
+ k.origin('center')
+ ])
+ addBullet(ship1.pos.sub(0, 65))
+
+ const ship2 = k.add([
+ k.sprite('playerShip2_orange'),
+ k.pos(shipsPos.add(0, 140)),
+ k.origin('center')
+ ])
+ addBullet(ship2.pos.sub(20, 40))
+ addBullet(ship2.pos.sub(-20, 40))
+
+ const ship3 = k.add([
+ k.sprite('playerShip2_orange'),
+ k.pos(shipsPos.add(0, 280)),
+ k.origin('center')
+ ])
+ addBullet(ship3.pos.sub(0, 65))
+ addBullet(ship3.pos.sub(20, 40))
+ addBullet(ship3.pos.sub(-20, 40))
+
+ const explainPos = k.vec2(40, 260)
+ k.add([
+ k.text('light stroke = ', { size: 28, font: 'sinko' }),
+ k.pos(explainPos),
+ k.origin('left')
+ ])
+ k.add([
+ k.text('normal stroke = ', { size: 28, font: 'sinko' }),
+ k.pos(explainPos.add(0, 140)),
+ k.origin('left')
+ ])
+ k.add([
+ k.text('heavy stroke = ', { size: 28, font: 'sinko' }),
+ k.pos(explainPos.add(0, 280)),
+ k.origin('left')
+ ])
+
+ function addBullet (pos) {
+ k.add([
+ k.sprite('laserRed01'),
+ k.pos(pos),
+ k.origin('center')
+ ])
+ }
+
+ 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) {
+ k.wait(2, () => { k.go('strokeFighterBattle') })
+ }
+
+ return {
+ appState
+ }
+}
diff --git a/app/client/components/DashboardMetric.js b/app/client/components/DashboardMetric.js
index b6e91ca..e4d9387 100644
--- a/app/client/components/DashboardMetric.js
+++ b/app/client/components/DashboardMetric.js
@@ -49,7 +49,7 @@ export class DashboardMetric extends AppElement {
return html`
${this.icon}
- ${this.value !== undefined ? this.value : '--'}
+ ${this.value}
${this.unit}
diff --git a/app/client/components/GameComponent.js b/app/client/components/GameComponent.js
index f182977..fe803aa 100644
--- a/app/client/components/GameComponent.js
+++ b/app/client/components/GameComponent.js
@@ -8,6 +8,7 @@
import { customElement } from 'lit/decorators.js'
import { createRowingGames } from '../arcade/RowingGames.js'
import { icon_bolt, icon_exit, icon_heartbeat, icon_paddle, icon_route, icon_stopwatch } from '../lib/icons.js'
+import { metricValue, metricUnit } from '../lib/helper.js'
import { buttonStyles } from '../lib/styles.js'
import { AppElement, css, html } from './AppElement.js'
@customElement('game-component')
@@ -73,15 +74,16 @@ export class GameComponent extends AppElement {
-
-
${icon_route}${Math.round(metrics.distanceTotal)}m
-
${icon_stopwatch}${metrics.splitFormatted}/500m
-
${icon_bolt}${Math.round(metrics.powerRaw)}watt
-
${icon_paddle}${Math.round(metrics.strokesPerMinute)}/min
- ${metrics?.heartrate ? html`
${icon_heartbeat}${Math.round(metrics.heartrate)}bpm
` : ''}
-
${icon_bolt}${metrics.instantaneousTorque.toFixed(2)}trq
-
${icon_bolt}${metrics.powerRatio.toFixed(2)}ratio
-
${icon_bolt}${metrics.strokeState}
+
${icon_route}${metricValue(metrics, 'distanceTotal')}${metricUnit(metrics, 'distanceTotal')}
+
${icon_stopwatch}${metricValue(metrics, 'splitFormatted')}/500m
+
${icon_bolt}${metricValue(metrics, 'powerRaw')}watt
+
${icon_paddle}${metricValue(metrics, 'strokesPerMinute')}/min
+ ${metrics?.heartrate
+ ? html`
${icon_heartbeat}${metricValue(metrics, 'heartrate')}bpm
`
+ : ''}
+
${icon_bolt}${metricValue(metrics, 'instantaneousTorque')}trq
+
${icon_bolt}${metricValue(metrics, 'powerRatio')}ratio
+
${icon_bolt}${metricValue(metrics, 'strokeState')}
diff --git a/app/client/components/PerformanceDashboard.js b/app/client/components/PerformanceDashboard.js
index 027ab53..c6060ea 100644
--- a/app/client/components/PerformanceDashboard.js
+++ b/app/client/components/PerformanceDashboard.js
@@ -6,6 +6,7 @@
*/
import { customElement, property } from 'lit/decorators.js'
+import { metricUnit, metricValue } from '../lib/helper.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'
@@ -51,15 +52,15 @@ export class PerformanceDashboard extends AppElement {
appState = APP_STATE
render () {
- const metrics = this.calculateFormattedMetrics(this.appState.metrics)
+ const metrics = this.appState.metrics
return html`
-
-
-
-
+
+
+
+
${metrics?.heartrate?.value
? html`
-
+
${metrics?.heartrateBatteryLevel?.value
? html`
@@ -67,39 +68,10 @@ export class PerformanceDashboard extends AppElement {
: ''
}
`
- : html``}
-
-
+ : html``}
+
+
`
}
-
- // todo: so far this is just a port of the formatter from the initial proof of concept client
- // we could split this up to make it more readable and testable
- calculateFormattedMetrics (metrics) {
- const fieldFormatter = {
- distanceTotal: (value) => value >= 10000
- ? { value: (value / 1000).toFixed(1), unit: 'km' }
- : { value: Math.round(value), unit: 'm' },
- caloriesTotal: (value) => Math.round(value),
- power: (value) => Math.round(value),
- strokesPerMinute: (value) => Math.round(value)
- }
-
- const formattedMetrics = {}
- for (const [key, value] of Object.entries(metrics)) {
- const valueFormatted = fieldFormatter[key] ? fieldFormatter[key](value) : value
- if (valueFormatted.value !== undefined && valueFormatted.unit !== undefined) {
- formattedMetrics[key] = {
- value: valueFormatted.value,
- unit: valueFormatted.unit
- }
- } else {
- formattedMetrics[key] = {
- value: valueFormatted
- }
- }
- }
- return formattedMetrics
- }
}
diff --git a/app/client/lib/helper.js b/app/client/lib/helper.js
index 448491d..2972541 100644
--- a/app/client/lib/helper.js
+++ b/app/client/lib/helper.js
@@ -19,3 +19,40 @@ export function filterObjectByKeys (object, keys) {
return obj
}, {})
}
+
+/**
+ * Picks a metric from the metrics object and presents its value in a human readable format
+ * @param {Object} metrics raw metrics object
+ * @param {String} metric selected metric
+ * @returns String value of metric in human readable format
+ */
+export function metricValue (metrics, metric) {
+ const formatMap = {
+ distanceTotal: (value) => value >= 10000
+ ? (value / 1000).toFixed(1)
+ : Math.round(value),
+ caloriesTotal: (value) => Math.round(value),
+ power: (value) => Math.round(value),
+ powerRaw: (value) => Math.round(value),
+ strokesPerMinute: (value) => Math.round(value),
+ instantaneousTorque: (value) => value.toFixed(2),
+ powerRatio: (value) => value.toFixed(2)
+ }
+ if (metrics[metric] === undefined) {
+ return '--'
+ }
+ return formatMap[metric] ? formatMap[metric](metrics[metric]) : metrics[metric]
+}
+
+/**
+ * Picks a metric from the metrics object and presents its unit in a human readable format
+ * @param {Object} metrics raw metrics object
+ * @param {String} metric selected metric
+ * @returns String value of metric unit in human readable format
+ */
+export function metricUnit (metrics, metric) {
+ const unitMap = {
+ distanceTotal: (value) => value >= 10000 ? 'km' : 'm'
+ }
+ return unitMap[metric] ? unitMap[metric](metrics[metric]) : ''
+}
diff --git a/app/client/store/appState.js b/app/client/store/appState.js
index 5541e6c..d73758d 100644
--- a/app/client/store/appState.js
+++ b/app/client/store/appState.js
@@ -8,7 +8,7 @@
export const APP_STATE = {
// currently can be STANDALONE (Mobile Home Screen App), KIOSK (Raspberry Pi deployment) or '' (default)
appMode: '',
- // currently can be DASHBOARD or 'ROWINGGAMES' (default)
+ // currently can be DASHBOARD or 'ROWINGGAMES'
activeRoute: 'DASHBOARD',
// contains all the rowing metrics that are delivered from the backend
metrics: {},