wires stroke fighter to rowing metrics
This commit is contained in:
parent
700e569a42
commit
9beca6cf72
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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': {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -5,5 +5,9 @@
|
|||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true
|
||||
}
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": false
|
||||
},
|
||||
"exclude": ["node_modules", "**/node_modules/*"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue