wires stroke fighter to rowing metrics

This commit is contained in:
Lars Berning 2022-02-14 20:47:45 +01:00
parent 700e569a42
commit 9beca6cf72
No known key found for this signature in database
GPG Key ID: 028E73C9E1D8A0B3
16 changed files with 154 additions and 53 deletions

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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`
<performance-dashboard
.appState=${this.appState}
.metrics=${this.metrics}
></performance-dashboard>
`
}
renderRowingGames () {
return html`
<game-component></game-component>
<game-component
.appState=${this.appState}
></game-component>
`
}

View File

@ -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': {

View File

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

View File

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

View File

@ -5,5 +5,9 @@
"checkJs": true,
"esModuleInterop": true,
"experimentalDecorators": true
}
},
"editor.codeActionsOnSave": {
"source.organizeImports": false
},
"exclude": ["node_modules", "**/node_modules/*"]
}