adds start screen to stroke fighter, refactores some code
This commit is contained in:
parent
d662e6e71e
commit
1f77de07ab
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]) : ''
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {},
|
||||
|
|
|
|||
Loading…
Reference in New Issue