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: {},