feat: add level time limits with countdown timer
- Add `timeLimit` configuration to levels 1-4 - Implement countdown timer that starts when throttle is pressed - Display remaining time in HUD, turning red when under 10s - Add 'time-up' failure condition and message in LevelFailedScene
This commit is contained in:
parent
0c80e4774e
commit
17fe6efd10
|
|
@ -4,6 +4,7 @@ export default {
|
||||||
name: "GoFundMe",
|
name: "GoFundMe",
|
||||||
description: "Get the Money!",
|
description: "Get the Money!",
|
||||||
kidsAboard: 4,
|
kidsAboard: 4,
|
||||||
|
timeLimit: 60,
|
||||||
startPosition: { x: 200, y: 700 },
|
startPosition: { x: 200, y: 700 },
|
||||||
startAngle: 0,
|
startAngle: 0,
|
||||||
terrain: [
|
terrain: [
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export default {
|
||||||
name: 'Big Air',
|
name: 'Big Air',
|
||||||
description: 'Clear the gap and land level, or the kids pay for it.',
|
description: 'Clear the gap and land level, or the kids pay for it.',
|
||||||
kidsAboard: 3,
|
kidsAboard: 3,
|
||||||
|
timeLimit: 20,
|
||||||
startPosition: { x: 100 * WORLD_SCALE, y: 350 * WORLD_SCALE },
|
startPosition: { x: 100 * WORLD_SCALE, y: 350 * WORLD_SCALE },
|
||||||
startAngle: 0,
|
startAngle: 0,
|
||||||
terrain: [{ points: runway }, { points: landing }],
|
terrain: [{ points: runway }, { points: landing }],
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ export default {
|
||||||
name: 'Tight Squeeze',
|
name: 'Tight Squeeze',
|
||||||
description: 'A steep valley and a long run of sharp bumps - lean early, lean often.',
|
description: 'A steep valley and a long run of sharp bumps - lean early, lean often.',
|
||||||
kidsAboard: 4,
|
kidsAboard: 4,
|
||||||
|
timeLimit: 20,
|
||||||
startPosition: { x: 100 * WORLD_SCALE, y: 350 * WORLD_SCALE },
|
startPosition: { x: 100 * WORLD_SCALE, y: 350 * WORLD_SCALE },
|
||||||
startAngle: 0,
|
startAngle: 0,
|
||||||
terrain: [{ points: buildPoints() }],
|
terrain: [{ points: buildPoints() }],
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export default {
|
||||||
name: "Mega Drop",
|
name: "Mega Drop",
|
||||||
description: "",
|
description: "",
|
||||||
kidsAboard: 5,
|
kidsAboard: 5,
|
||||||
|
timeLimit: 76,
|
||||||
startPosition: { x: 200, y: 700 },
|
startPosition: { x: 200, y: 700 },
|
||||||
startAngle: 0,
|
startAngle: 0,
|
||||||
terrain: [
|
terrain: [
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { createButton } from '../util/ui.js';
|
||||||
const REASON_TEXT = {
|
const REASON_TEXT = {
|
||||||
'all-kids-lost': 'Every kid got thrown off the bus!',
|
'all-kids-lost': 'Every kid got thrown off the bus!',
|
||||||
'bus-fell': 'The bus fell into the gap!',
|
'bus-fell': 'The bus fell into the gap!',
|
||||||
|
'time-up': 'Ran out of time before reaching the finish line!',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class LevelFailedScene extends Phaser.Scene {
|
export default class LevelFailedScene extends Phaser.Scene {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ export default class PlayScene extends Phaser.Scene {
|
||||||
this.levelId = data.levelId;
|
this.levelId = data.levelId;
|
||||||
this.level = getLevelById(this.levelId);
|
this.level = getLevelById(this.levelId);
|
||||||
this._levelEnded = false;
|
this._levelEnded = false;
|
||||||
|
this._timerStarted = false;
|
||||||
|
this._timeRemaining = this.level.timeLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
@ -62,11 +64,14 @@ export default class PlayScene extends Phaser.Scene {
|
||||||
if (this._levelEnded) return;
|
if (this._levelEnded) return;
|
||||||
|
|
||||||
const intent = this.inputController.getIntent();
|
const intent = this.inputController.getIntent();
|
||||||
|
if (!this._timerStarted && intent.throttle > 0) this._timerStarted = true;
|
||||||
|
|
||||||
this.bus.applyIntent(intent);
|
this.bus.applyIntent(intent);
|
||||||
this.engineSound.update();
|
this.engineSound.update();
|
||||||
this.finishLine.update(time);
|
this.finishLine.update(time);
|
||||||
this.cameraRig.update();
|
this.cameraRig.update();
|
||||||
this._updateParallax();
|
this._updateParallax();
|
||||||
|
this._updateTimer(delta);
|
||||||
this._checkBusFell();
|
this._checkBusFell();
|
||||||
this._checkAllKidsLost();
|
this._checkAllKidsLost();
|
||||||
|
|
||||||
|
|
@ -168,6 +173,28 @@ export default class PlayScene extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Counts down only once the player has actually hit the gas (throttle >
|
||||||
|
// 0) for the first time - sitting idle at the start line costs nothing,
|
||||||
|
// matching "the clock is your speedrun, not your reaction time" framing.
|
||||||
|
_updateTimer(delta) {
|
||||||
|
if (!this._timerStarted || this._timeRemaining <= 0) return;
|
||||||
|
|
||||||
|
this._timeRemaining = Math.max(0, this._timeRemaining - delta / 1000);
|
||||||
|
this.timerText.setText(this._formatTime(this._timeRemaining));
|
||||||
|
this.timerText.setColor(this._timeRemaining <= 10 ? '#c0392b' : '#1a1f29');
|
||||||
|
|
||||||
|
if (this._timeRemaining <= 0) {
|
||||||
|
this.events.emit('level-failed', { reason: 'time-up' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_formatTime(seconds) {
|
||||||
|
const whole = Math.max(0, Math.ceil(seconds));
|
||||||
|
const m = Math.floor(whole / 60);
|
||||||
|
const s = whole % 60;
|
||||||
|
return `${m}:${String(s).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
_buildGoal() {
|
_buildGoal() {
|
||||||
const goal = this.level.goal;
|
const goal = this.level.goal;
|
||||||
this.goalBody = this.matter.add.rectangle(goal.x, goal.y, goal.width, goal.height, {
|
this.goalBody = this.matter.add.rectangle(goal.x, goal.y, goal.width, goal.height, {
|
||||||
|
|
@ -196,6 +223,14 @@ export default class PlayScene extends Phaser.Scene {
|
||||||
backgroundColor: '#ffffffaa',
|
backgroundColor: '#ffffffaa',
|
||||||
}).setScrollFactor(0).setDepth(10);
|
}).setScrollFactor(0).setDepth(10);
|
||||||
|
|
||||||
|
this.timerText = this.add.text(GAME_WIDTH - startX, y, this._formatTime(this._timeRemaining), {
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: `${32 * WORLD_SCALE}px`,
|
||||||
|
fontStyle: 'bold',
|
||||||
|
color: '#1a1f29',
|
||||||
|
backgroundColor: '#ffffffaa',
|
||||||
|
}).setOrigin(1, 0).setScrollFactor(0).setDepth(10);
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
this.debugText = this.add.text(startX, y + 52 * WORLD_SCALE, 'g-force: 0.00', {
|
this.debugText = this.add.text(startX, y + 52 * WORLD_SCALE, 'g-force: 0.00', {
|
||||||
fontFamily: 'monospace',
|
fontFamily: 'monospace',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue