adds start screen to stroke fighter, refactores some code

This commit is contained in:
Lars Berning 2022-02-18 19:34:07 +01:00
parent d662e6e71e
commit 1f77de07ab
No known key found for this signature in database
GPG Key ID: 028E73C9E1D8A0B3
8 changed files with 175 additions and 60 deletions

View File

@ -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)

View File

@ -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')
])

View File

@ -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
}
}

View File

@ -49,7 +49,7 @@ export class DashboardMetric extends AppElement {
return html`
<div class="label">${this.icon}</div>
<div class="content">
<span class="metric-value">${this.value !== undefined ? this.value : '--'}</span>
<span class="metric-value">${this.value}</span>
<span class="metric-unit">${this.unit}</span>
</div>
<slot></slot>

View File

@ -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 {
<canvas id="arcade"></canvas>
<div id="container">
<div id="widget">
<!-- todo: should use the same calculations as PerformanceDashboard -->
<div>${icon_route}${Math.round(metrics.distanceTotal)}<span class="metric-unit">m</span></div>
<div>${icon_stopwatch}${metrics.splitFormatted}<span class="metric-unit">/500m</span></div>
<div>${icon_bolt}${Math.round(metrics.powerRaw)}<span class="metric-unit">watt</span></div>
<div>${icon_paddle}${Math.round(metrics.strokesPerMinute)}<span class="metric-unit">/min</span></div>
${metrics?.heartrate ? html`<div>${icon_heartbeat}${Math.round(metrics.heartrate)}<span class="metric-unit">bpm</span></div>` : ''}
<div>${icon_bolt}${metrics.instantaneousTorque.toFixed(2)}<span class="metric-unit">trq</span></div>
<div>${icon_bolt}${metrics.powerRatio.toFixed(2)}<span class="metric-unit">ratio</span></div>
<div>${icon_bolt}${metrics.strokeState}</div>
<div>${icon_route}${metricValue(metrics, 'distanceTotal')}<span class="metric-unit">${metricUnit(metrics, 'distanceTotal')}</span></div>
<div>${icon_stopwatch}${metricValue(metrics, 'splitFormatted')}<span class="metric-unit">/500m</span></div>
<div>${icon_bolt}${metricValue(metrics, 'powerRaw')}<span class="metric-unit">watt</span></div>
<div>${icon_paddle}${metricValue(metrics, 'strokesPerMinute')}<span class="metric-unit">/min</span></div>
${metrics?.heartrate
? html`<div>${icon_heartbeat}${metricValue(metrics, 'heartrate')}<span class="metric-unit">bpm</span></div>`
: ''}
<div>${icon_bolt}${metricValue(metrics, 'instantaneousTorque')}<span class="metric-unit">trq</span></div>
<div>${icon_bolt}${metricValue(metrics, 'powerRatio')}<span class="metric-unit">ratio</span></div>
<div>${icon_bolt}${metricValue(metrics, 'strokeState')}</div>
<div id='buttons'>
<button @click=${this.openDashboard}>${icon_exit}</button>

View File

@ -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`
<dashboard-metric .icon=${icon_route} .unit=${metrics?.distanceTotal?.unit || 'm'} .value=${metrics?.distanceTotal?.value}></dashboard-metric>
<dashboard-metric .icon=${icon_stopwatch} unit="/500m" .value=${metrics?.splitFormatted?.value}></dashboard-metric>
<dashboard-metric .icon=${icon_bolt} unit="watt" .value=${metrics?.power?.value}></dashboard-metric>
<dashboard-metric .icon=${icon_paddle} unit="/min" .value=${metrics?.strokesPerMinute?.value}></dashboard-metric>
<dashboard-metric .icon=${icon_route} .unit=${metricUnit(metrics, 'distanceTotal')} .value=${metricValue(metrics, 'distanceTotal')}></dashboard-metric>
<dashboard-metric .icon=${icon_stopwatch} unit="/500m" .value=${metricValue(metrics, 'splitFormatted')}></dashboard-metric>
<dashboard-metric .icon=${icon_bolt} unit="watt" .value=${metricValue(metrics, 'power')}></dashboard-metric>
<dashboard-metric .icon=${icon_paddle} unit="/min" .value=${metricValue(metrics, 'strokesPerMinute')}></dashboard-metric>
${metrics?.heartrate?.value
? html`
<dashboard-metric .icon=${icon_heartbeat} unit="bpm" .value=${metrics?.heartrate?.value}>
<dashboard-metric .icon=${icon_heartbeat} unit="bpm" .value=${metricValue(metrics, 'heartrate')}>
${metrics?.heartrateBatteryLevel?.value
? html`
<battery-icon .batteryLevel=${metrics?.heartrateBatteryLevel?.value}></battery-icon>
@ -67,39 +68,10 @@ export class PerformanceDashboard extends AppElement {
: ''
}
</dashboard-metric>`
: html`<dashboard-metric .icon=${icon_paddle} unit="total" .value=${metrics?.strokesTotal?.value}></dashboard-metric>`}
<dashboard-metric .icon=${icon_fire} unit="kcal" .value=${metrics?.caloriesTotal?.value}></dashboard-metric>
<dashboard-metric .icon=${icon_clock} .value=${metrics?.durationTotalFormatted?.value}></dashboard-metric>
: html`<dashboard-metric .icon=${icon_paddle} unit="total" .value=${metricValue(metrics, 'strokesTotal')}></dashboard-metric>`}
<dashboard-metric .icon=${icon_fire} unit="kcal" .value=${metricValue(metrics, 'caloriesTotal')}></dashboard-metric>
<dashboard-metric .icon=${icon_clock} .value=${metricValue(metrics, 'durationTotalFormatted')}></dashboard-metric>
<dashboard-actions .appState=${this.appState}></dashboard-actions>
`
}
// 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
}
}

View File

@ -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]) : ''
}

View File

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