266 lines
6.7 KiB
JavaScript
266 lines
6.7 KiB
JavaScript
'use strict'
|
|
/*
|
|
Open Rowing Monitor, https://github.com/laberning/openrowingmonitor
|
|
|
|
Implements the Battle Action 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 StrokeFighterBattleScene (k) {
|
|
// how much stroke power is needed to fire high power lasers
|
|
const THRESHOLD_POWER = 180
|
|
// training duration in seconds
|
|
const TARGET_TIME = 10 * 60
|
|
// strokes per minute at start of training
|
|
const SPM_START = 18
|
|
// strokes per minute at end of training
|
|
const SPM_END = 28
|
|
const BULLET_SPEED = 1200
|
|
const ENEMY_SPEED = 60
|
|
const PLAYER_SPEED = 480
|
|
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 }
|
|
]
|
|
|
|
let trainingTime = 0
|
|
|
|
const ui = k.add([
|
|
k.fixed(),
|
|
k.z(100)
|
|
])
|
|
|
|
const background = addSpaceBackground(k)
|
|
|
|
function grow (rate) {
|
|
return {
|
|
update () {
|
|
const n = rate * k.dt()
|
|
this.scale.x += n
|
|
this.scale.y += n
|
|
}
|
|
}
|
|
}
|
|
|
|
const player = k.add([
|
|
k.sprite('playerShip2_orange'),
|
|
k.area(),
|
|
k.pos(k.width() / 2, k.height() - 64),
|
|
k.origin('center')
|
|
])
|
|
|
|
function moveLeft () {
|
|
player.move(-PLAYER_SPEED, 0)
|
|
if (player.pos.x < 0) {
|
|
player.pos.x = k.width()
|
|
}
|
|
}
|
|
|
|
function moveRight () {
|
|
player.move(PLAYER_SPEED, 0)
|
|
if (player.pos.x > k.width()) {
|
|
player.pos.x = 0
|
|
}
|
|
}
|
|
|
|
player.onCollide('enemy', (enemy) => {
|
|
k.destroy(enemy)
|
|
k.shake(4)
|
|
k.play('hit', {
|
|
detune: -1200,
|
|
volume: 0.6,
|
|
speed: k.rand(0.5, 2)
|
|
})
|
|
})
|
|
|
|
player.onUpdate(() => {
|
|
const tolerance = 10
|
|
const closestEnemy = k.get('enemy').reduce((prev, enemy) => { return enemy?.pos.y > prev?.pos.y ? enemy : prev }, { pos: { y: 0 } })
|
|
if (closestEnemy?.pos?.x) {
|
|
if (closestEnemy.pos.x > player.pos.x + tolerance) {
|
|
moveRight()
|
|
} else if (closestEnemy.pos.x < player.pos.x - tolerance) {
|
|
moveLeft()
|
|
}
|
|
}
|
|
})
|
|
|
|
function addLaserHit (pos, n) {
|
|
for (let i = 0; i < n; i++) {
|
|
k.wait(k.rand(n * 0.1), () => {
|
|
for (let i = 0; i < 2; i++) {
|
|
k.add([
|
|
k.sprite('laserRed09'),
|
|
k.pos(pos.sub(0, 10)),
|
|
k.scale(k.vec2(0.5)),
|
|
k.lifespan(0.1),
|
|
grow(k.rand(0.5, 2)),
|
|
k.origin('center')
|
|
])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
function spawnBullet (pos) {
|
|
k.add([
|
|
k.sprite('laserRed01'),
|
|
k.area(),
|
|
k.pos(pos),
|
|
k.origin('center'),
|
|
k.move(k.UP, BULLET_SPEED),
|
|
k.cleanup(),
|
|
'bullet'
|
|
])
|
|
}
|
|
|
|
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, 65))
|
|
} else if (destructivePower <= 2) {
|
|
spawnBullet(player.pos.sub(20, 40))
|
|
spawnBullet(player.pos.sub(-20, 40))
|
|
} else {
|
|
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,
|
|
detune: k.rand(-1200, 1200)
|
|
})
|
|
}
|
|
|
|
function spawnEnemy (enemy) {
|
|
k.add([
|
|
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.origin('bot'),
|
|
'enemy',
|
|
{ speed: k.rand(ENEMY_SPEED * 0.5, ENEMY_SPEED * 1.5) }
|
|
])
|
|
}
|
|
|
|
k.on('death', 'enemy', (enemy) => {
|
|
k.destroy(enemy)
|
|
k.every('bullet', (bullet) => {
|
|
addLaserHit(bullet.pos, 1)
|
|
k.destroy(bullet)
|
|
})
|
|
k.shake(2)
|
|
})
|
|
|
|
k.on('hurt', 'enemy', (enemy) => {
|
|
k.shake(1)
|
|
k.play('hit', {
|
|
detune: k.rand(-1200, 1200),
|
|
volume: 0.3,
|
|
speed: k.rand(0.2, 2)
|
|
})
|
|
})
|
|
|
|
const timer = ui.add([
|
|
k.text('00:00', { size: 25, font: 'sinko' }),
|
|
k.pos(10, 10),
|
|
k.fixed()
|
|
])
|
|
|
|
let trainingTimeRounded = 0
|
|
timer.onUpdate(() => {
|
|
trainingTime += k.dt()
|
|
const newTrainingTimeRounded = Math.round(trainingTime)
|
|
if (trainingTimeRounded !== newTrainingTimeRounded) {
|
|
timer.text = `${secondsToTimeString(newTrainingTimeRounded)} / ${k.debug.fps()}fps`
|
|
trainingTimeRounded = newTrainingTimeRounded
|
|
}
|
|
})
|
|
|
|
// converts a timestamp in seconds to a human readable hh:mm:ss format
|
|
function secondsToTimeString (secondsTimeStamp) {
|
|
if (secondsTimeStamp === Infinity) return '∞'
|
|
const hours = Math.floor(secondsTimeStamp / 60 / 60)
|
|
const minutes = Math.floor(secondsTimeStamp / 60) - (hours * 60)
|
|
const seconds = Math.floor(secondsTimeStamp % 60)
|
|
let timeString = hours > 0 ? ` ${hours.toString().padStart(2, '0')}:` : ''
|
|
timeString += `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
|
return timeString
|
|
}
|
|
|
|
k.onCollide('bullet', 'enemy', (bullet, enemy) => {
|
|
k.destroy(bullet)
|
|
enemy.hurt(1)
|
|
addLaserHit(bullet.pos, 1)
|
|
})
|
|
|
|
k.onUpdate('enemy', (enemy) => {
|
|
enemy.move(0, enemy.speed)
|
|
if (enemy.pos.y - enemy.height > k.height()) {
|
|
k.destroy(enemy)
|
|
}
|
|
})
|
|
|
|
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.powerRaw < THRESHOLD_POWER * 0.75) {
|
|
fireWeapons(1)
|
|
} else if (metrics.powerRaw < THRESHOLD_POWER) {
|
|
fireWeapons(2)
|
|
} else {
|
|
fireWeapons(3)
|
|
}
|
|
}
|
|
|
|
function scheduleNextEnemy () {
|
|
const percentTrainingFinished = trainingTime / TARGET_TIME
|
|
const currentSPM = SPM_START + (SPM_END - SPM_START) * percentTrainingFinished
|
|
let maxEnemyHealth = 1
|
|
if (percentTrainingFinished < 0.4) {
|
|
maxEnemyHealth = 1
|
|
} else if (percentTrainingFinished < 0.8) {
|
|
maxEnemyHealth = 2
|
|
} else {
|
|
maxEnemyHealth = 3
|
|
}
|
|
spawnEnemy(k.choose(ENEMIES.filter((enemy) => enemy.health <= maxEnemyHealth)))
|
|
k.wait(60 / currentSPM, scheduleNextEnemy)
|
|
}
|
|
|
|
scheduleNextEnemy()
|
|
|
|
return {
|
|
appState
|
|
}
|
|
}
|