|
| 1 | +/* |
| 2 | +Toolbox Aid |
| 3 | +David Quesenberry |
| 4 | +04/16/2026 |
| 5 | +FinalReferenceGameScene.js |
| 6 | +*/ |
| 7 | +import { drawPanel } from '/src/engine/debug/index.js'; |
| 8 | +import GameplayMetricsTelemetryScene from '/samples/phase-17/1712/GameplayMetricsTelemetryScene.js'; |
| 9 | + |
| 10 | +function clamp(value, min, max) { |
| 11 | + return Math.max(min, Math.min(max, value)); |
| 12 | +} |
| 13 | + |
| 14 | +function computeReferenceRank(scene) { |
| 15 | + if (scene.gameState === 'lost') { |
| 16 | + return 'D'; |
| 17 | + } |
| 18 | + if (scene.gameState !== 'won') { |
| 19 | + return 'N/A'; |
| 20 | + } |
| 21 | + |
| 22 | + const objectiveRatio = clamp(scene.score / Math.max(1, scene.targetScore), 0, 1); |
| 23 | + const healthRatio = clamp(scene.health / Math.max(1, scene.maxHealth), 0, 1); |
| 24 | + const timeRatio = clamp(scene.timeLeft / Math.max(1, scene.roundSeconds), 0, 1); |
| 25 | + const collisionPenalty = Math.min(0.35, scene.telemetry.collisionsTotal * 0.01); |
| 26 | + const weightedScore = objectiveRatio * 0.6 + healthRatio * 0.25 + timeRatio * 0.15 - collisionPenalty; |
| 27 | + |
| 28 | + if (weightedScore >= 0.9) return 'S'; |
| 29 | + if (weightedScore >= 0.78) return 'A'; |
| 30 | + if (weightedScore >= 0.63) return 'B'; |
| 31 | + return 'C'; |
| 32 | +} |
| 33 | + |
| 34 | +function isBetterRank(candidate, baseline) { |
| 35 | + const rankOrder = ['S', 'A', 'B', 'C', 'D', 'N/A']; |
| 36 | + return rankOrder.indexOf(candidate) < rankOrder.indexOf(baseline); |
| 37 | +} |
| 38 | + |
| 39 | +export default class FinalReferenceGameScene extends GameplayMetricsTelemetryScene { |
| 40 | + constructor() { |
| 41 | + super(); |
| 42 | + |
| 43 | + this.maxHealth = 4; |
| 44 | + this.targetScore = 8; |
| 45 | + this.roundSeconds = 70; |
| 46 | + this.playerSpeed = 8.8; |
| 47 | + this.hitCooldownSeconds = 0.95; |
| 48 | + this.resetMatch({ toReady: true }); |
| 49 | + |
| 50 | + this.referenceRuntime = { |
| 51 | + runCount: 0, |
| 52 | + activeSeconds: 0, |
| 53 | + missionRank: 'N/A', |
| 54 | + bestRank: 'N/A', |
| 55 | + bestScore: 0, |
| 56 | + bestTimeRemaining: 0, |
| 57 | + completionBonus: 0, |
| 58 | + phase: 'briefing', |
| 59 | + }; |
| 60 | + } |
| 61 | + |
| 62 | + step3DPhysics(dtSeconds, engine) { |
| 63 | + const dt = Math.max(0, Math.min(0.05, Number(dtSeconds) || 0)); |
| 64 | + const previousState = this.gameState; |
| 65 | + const input = engine?.input; |
| 66 | + |
| 67 | + if (previousState === 'ready') { |
| 68 | + const wantsMovement = |
| 69 | + input?.isDown('KeyW') === true || |
| 70 | + input?.isDown('KeyA') === true || |
| 71 | + input?.isDown('KeyS') === true || |
| 72 | + input?.isDown('KeyD') === true; |
| 73 | + if (wantsMovement) { |
| 74 | + this.startMatch(); |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + super.step3DPhysics(dt, engine); |
| 79 | + |
| 80 | + if (this.gameState === 'running') { |
| 81 | + if (previousState !== 'running') { |
| 82 | + this.referenceRuntime.runCount += 1; |
| 83 | + this.referenceRuntime.activeSeconds = 0; |
| 84 | + this.referenceRuntime.phase = 'active'; |
| 85 | + } |
| 86 | + this.referenceRuntime.activeSeconds += dt; |
| 87 | + this.referenceRuntime.missionRank = 'N/A'; |
| 88 | + this.referenceRuntime.completionBonus = 0; |
| 89 | + return; |
| 90 | + } |
| 91 | + |
| 92 | + if (this.gameState === 'ready') { |
| 93 | + this.referenceRuntime.phase = 'briefing'; |
| 94 | + this.referenceRuntime.activeSeconds = 0; |
| 95 | + this.referenceRuntime.missionRank = 'N/A'; |
| 96 | + this.referenceRuntime.completionBonus = 0; |
| 97 | + return; |
| 98 | + } |
| 99 | + |
| 100 | + if (previousState !== this.gameState) { |
| 101 | + this.referenceRuntime.phase = this.gameState === 'won' ? 'complete' : 'failed'; |
| 102 | + this.referenceRuntime.missionRank = computeReferenceRank(this); |
| 103 | + this.referenceRuntime.completionBonus = this.gameState === 'won' |
| 104 | + ? Math.max(0, Math.floor(this.timeLeft * 4 + this.health * 25)) |
| 105 | + : 0; |
| 106 | + |
| 107 | + if (this.gameState === 'won') { |
| 108 | + if (this.score > this.referenceRuntime.bestScore) { |
| 109 | + this.referenceRuntime.bestScore = this.score; |
| 110 | + this.referenceRuntime.bestTimeRemaining = this.timeLeft; |
| 111 | + } else if (this.score === this.referenceRuntime.bestScore) { |
| 112 | + this.referenceRuntime.bestTimeRemaining = Math.max(this.referenceRuntime.bestTimeRemaining, this.timeLeft); |
| 113 | + } |
| 114 | + |
| 115 | + if ( |
| 116 | + this.referenceRuntime.bestRank === 'N/A' || |
| 117 | + isBetterRank(this.referenceRuntime.missionRank, this.referenceRuntime.bestRank) |
| 118 | + ) { |
| 119 | + this.referenceRuntime.bestRank = this.referenceRuntime.missionRank; |
| 120 | + } |
| 121 | + } |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + render(renderer) { |
| 126 | + super.render(renderer); |
| 127 | + |
| 128 | + drawPanel(renderer, 384, 286, 228, 248, 'Final Reference Runtime', [ |
| 129 | + `profile=final-reference`, |
| 130 | + `phase=${this.referenceRuntime.phase}`, |
| 131 | + `runs=${this.referenceRuntime.runCount}`, |
| 132 | + `activeSeconds=${this.referenceRuntime.activeSeconds.toFixed(1)}`, |
| 133 | + `missionRank=${this.referenceRuntime.missionRank}`, |
| 134 | + `bestRank=${this.referenceRuntime.bestRank}`, |
| 135 | + `bestScore=${this.referenceRuntime.bestScore}/${this.targetScore}`, |
| 136 | + `bestTimeLeft=${this.referenceRuntime.bestTimeRemaining.toFixed(1)}s`, |
| 137 | + `completionBonus=${this.referenceRuntime.completionBonus}`, |
| 138 | + 'restart=R after mission outcome', |
| 139 | + ]); |
| 140 | + } |
| 141 | +} |
0 commit comments