Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
204 commits
Select commit Hold shift + click to select a range
9dccd6c
chore: finalize
Mar 20, 2026
fdb7215
chore: finalize
Mar 20, 2026
58cae84
chore: finalize
Mar 20, 2026
b4a0716
chore: finalize
Mar 20, 2026
f54053a
chore: finalize
Mar 20, 2026
865546f
fix(ci): re-trigger CI build after transient failure
Mar 20, 2026
904d604
chore: finalize
Mar 20, 2026
e8233f9
feat(track): replace flat straight track with curved downhill course …
Mar 21, 2026
49f7b01
fix(build): remove unused showAllCoins import to retrigger CI
Mar 21, 2026
4f49f1e
chore: finalize
Mar 21, 2026
5d6e93d
fix(build): remove unused showAllCoins export to retrigger CI
Mar 21, 2026
89ec953
chore: finalize
Mar 21, 2026
e3c6743
fix(gameplay): restore track wrap with level regeneration
Mar 21, 2026
508338e
chore: finalize
Mar 21, 2026
e0e8311
chore: finalize
Mar 21, 2026
3855ae1
chore: finalize
Mar 21, 2026
0ba2fe4
chore: remove agent artifacts before rebase
Mar 21, 2026
3ac4dc6
chore: finalize
Mar 21, 2026
907591d
chore: remove agent artifacts before rebase
Mar 21, 2026
886e9b1
chore: revert workflow file to origin version to avoid token scope issue
Mar 21, 2026
3306d88
chore: finalize
Mar 21, 2026
ea61aba
fix(security): address review findings
Mar 21, 2026
3457900
chore: finalize
Mar 21, 2026
5f7c16a
chore: finalize
Mar 21, 2026
1bd4d45
chore: finalize
Mar 21, 2026
7c7d23d
chore: finalize
Mar 21, 2026
10f03dc
chore: finalize
Mar 21, 2026
c1a392c
chore: finalize
Mar 21, 2026
97d1e16
chore: finalize
Mar 21, 2026
2970366
chore: finalize
Mar 21, 2026
f207a0e
chore: finalize
Mar 21, 2026
6e73db5
chore: finalize
Mar 21, 2026
e683cbe
chore: finalize
Mar 21, 2026
08fbe38
chore: finalize
Mar 21, 2026
4a552ad
chore: finalize
Mar 21, 2026
34408e7
chore: finalize
Mar 21, 2026
a13e288
fix(physics): remove dead ball.vx code that produced NaN
Mar 21, 2026
a6df678
chore: finalize
Mar 21, 2026
13f87f4
fix(security): restore out-of-scope deleted files to match main
Mar 21, 2026
7cad3dd
chore: finalize
Mar 21, 2026
ecc9774
fix(security): restore out-of-scope deleted files to match main
Mar 21, 2026
1afde15
chore: finalize
Mar 21, 2026
b7c9e64
chore: finalize
Mar 21, 2026
6eb9006
fix(security): restore out-of-scope deleted files to match main
Mar 21, 2026
003760b
chore: finalize
Mar 21, 2026
c03330e
chore: finalize
Mar 21, 2026
85c6fce
chore: finalize
Mar 21, 2026
88199e8
chore: finalize
Mar 21, 2026
5998bd2
chore: finalize
Mar 21, 2026
9c2a378
chore: finalize
Mar 21, 2026
5eeeb3f
chore: finalize
Mar 21, 2026
7f1f22e
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
b124764
chore: finalize
Mar 21, 2026
2c6dc00
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
df7e958
chore: finalize
Mar 21, 2026
22670dd
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
05ef239
chore: finalize
Mar 21, 2026
f9dd6e5
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
65cdcd9
chore: finalize
Mar 21, 2026
a6572e8
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
6cbddfa
chore: finalize
Mar 21, 2026
154383c
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
55e7a84
chore: finalize
Mar 21, 2026
3f243a5
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
653064e
chore: finalize
Mar 21, 2026
a39c225
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
77f8642
chore: finalize
Mar 21, 2026
6e42b99
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
4186a25
chore: finalize
Mar 21, 2026
6b35e42
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
1d9e322
chore: finalize
Mar 21, 2026
de542d0
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
f766226
chore: finalize
Mar 21, 2026
b380901
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
507174c
chore: finalize
Mar 21, 2026
df463c3
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
fc993bb
chore: finalize
Mar 21, 2026
9c90b9a
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
47124cb
chore: finalize
Mar 21, 2026
aa6fcba
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
e5d045d
chore: finalize
Mar 21, 2026
223b7fd
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
70ecd8c
chore: finalize
Mar 21, 2026
7a330a4
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
f5c738b
chore: finalize
Mar 21, 2026
d4727a1
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
ae2d7d9
chore: finalize
Mar 21, 2026
956dc88
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
ddb0c3e
chore: finalize
Mar 21, 2026
8d54531
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
b5ca36e
chore: finalize
Mar 21, 2026
e1b6f22
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
2b1394a
chore: finalize
Mar 21, 2026
91630cf
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
155110e
chore: finalize
Mar 21, 2026
1c71300
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
56394f1
chore: finalize
Mar 21, 2026
bcab447
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
8561c11
chore: finalize
Mar 21, 2026
e6afe5b
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
7404cf3
chore: finalize
Mar 21, 2026
9ba5cdc
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
1452185
chore: finalize
Mar 21, 2026
547c181
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
e4355d2
chore: finalize
Mar 21, 2026
5e8b7ad
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
3010a39
chore: finalize
Mar 21, 2026
bbb5421
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
7678272
chore: finalize
Mar 21, 2026
15387f7
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
7e6a7ab
chore: finalize
Mar 21, 2026
a96c439
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
78419e4
chore: finalize
Mar 21, 2026
337ec79
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
9c8a445
chore: finalize
Mar 21, 2026
ad1130b
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
24f0666
chore: finalize
Mar 21, 2026
f214b01
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
2577b46
chore: finalize
Mar 21, 2026
5d7d1f2
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
5cb6849
chore: finalize
Mar 21, 2026
f357b0a
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
3c3afdf
chore: finalize
Mar 21, 2026
b1f6d10
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
4def712
chore: finalize
Mar 21, 2026
306b5c2
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
b27741f
chore: finalize
Mar 21, 2026
b3c8fbc
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
4826d83
chore: finalize
Mar 21, 2026
f899183
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
a29df33
chore: finalize
Mar 21, 2026
31de4f4
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
8f66f6d
chore: finalize
Mar 21, 2026
bc412b3
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
c17473e
chore: finalize
Mar 21, 2026
752a41d
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
659cb82
chore: finalize
Mar 21, 2026
7e8267c
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
8695a08
chore: finalize
Mar 21, 2026
4f7b8d5
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
dd17ace
chore: finalize
Mar 21, 2026
67899b7
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
7862628
chore: finalize
Mar 21, 2026
341d7c4
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
4c562a5
chore: finalize
Mar 21, 2026
63d5785
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
267156b
chore: finalize
Mar 21, 2026
ffb7dd9
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
60e226d
chore: finalize
Mar 21, 2026
dab1440
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
9c02aff
chore: finalize
Mar 21, 2026
35cfa5c
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
9c4edf6
chore: finalize
Mar 21, 2026
dd52a36
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
63c4b78
chore: finalize
Mar 21, 2026
a0fdef7
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
5e417bf
chore: finalize
Mar 21, 2026
8d36d35
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
b0941d5
chore: finalize
Mar 21, 2026
a7cace0
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
bf8e4b4
chore: finalize
Mar 21, 2026
5f0307b
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
75e8929
chore: finalize
Mar 21, 2026
53c0105
chore: finalize
Mar 21, 2026
c0d8639
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
b57ae10
chore: finalize
Mar 21, 2026
0480899
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
0ba5306
chore: finalize
Mar 21, 2026
79b6360
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
cbfb6c0
chore: finalize
Mar 21, 2026
81b37a6
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
06c3d5d
chore: finalize
Mar 21, 2026
0d8e301
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
092844c
chore: finalize
Mar 21, 2026
59ca689
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
aaa4cf9
chore: finalize
Mar 21, 2026
bdcf457
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 21, 2026
ed9815d
chore: finalize
Mar 21, 2026
93d08b5
fix(security): restore PLAN.md and tasks.json deleted out of scope
Mar 22, 2026
2bceb7a
chore: finalize
Mar 22, 2026
50a1321
chore: finalize
Mar 22, 2026
6c79374
chore: finalize
Mar 22, 2026
a2764bc
chore: finalize
Mar 22, 2026
dba9bb9
chore: finalize
Mar 22, 2026
f022510
chore: finalize
Mar 22, 2026
e89f8f5
chore: finalize
Mar 22, 2026
d2edb11
chore: finalize
Mar 22, 2026
3d4b7c9
chore: finalize
Mar 22, 2026
4aa63c7
fix(security): address review findings
Mar 22, 2026
ccfcee1
chore: finalize
Mar 22, 2026
39deab3
fix(security): address review findings
Mar 22, 2026
a3f1a67
chore: finalize
Mar 22, 2026
0ad2bc0
chore: finalize
Mar 22, 2026
644d0c5
fix(security): address review findings
Mar 22, 2026
64c9f31
chore: finalize
Mar 22, 2026
2bc989f
fix(security): address review findings
Mar 22, 2026
21f8ad7
chore: finalize
Mar 22, 2026
1de3a9f
chore: finalize
Mar 22, 2026
7813e3d
chore: finalize
Mar 22, 2026
3e209c5
chore: finalize
Mar 22, 2026
0eac77d
fix(security): address review findings
Mar 22, 2026
7e36b89
chore: finalize
Mar 22, 2026
176a0ad
fix(security): address review findings
Mar 22, 2026
d2faf75
chore: finalize
Mar 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
338 changes: 84 additions & 254 deletions .github/workflows/security-intent-review-gate.yml

Large diffs are not rendered by default.

83 changes: 83 additions & 0 deletions PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Plan: Fix Static Map After Track Wrap

## Problem

When the ball reaches the end of the track, `physics.js` wraps the ball position back to the start (`ball.z = -halfLength + 1`), but the level (obstacles, coins, turtle) is **never regenerated**. This causes:

1. **Same obstacles every lap** — obstacles stay in identical positions because `regenerateLevel()` is not called on wrap.
2. **No coins visible** — coins collected on the first pass remain hidden (their meshes have `visible = false` and the `coinsCollected` array still marks them as collected).
3. **Turtle missing** — once collected or passed, the turtle doesn't reappear.

## Root Cause

`js/physics.js:142-144` — the track wrap logic only resets `ball.z` but does nothing to refresh the level layout or reset collection state. There is no communication back to `main.js` that a wrap occurred.

## Solution

### Approach: Signal wrap event, regenerate in main.js

The cleanest fix follows the existing pattern (similar to how `coinsCollected` and `turtleCollected` are communicated):

1. **physics.js**: Add a `wrapped: true` flag to the return object of `updateOnTrack()` when the ball wraps around.

2. **physics.js**: Add a new exported function `refreshLevel(config)` that updates obstacles, coins, and turtle references (and resets their collection state) without resetting ball position or velocity.

3. **main.js**: When `result.wrapped` is true, call `regenerateLevel()` to create new obstacle/coin/turtle meshes, then call `refreshLevel()` with the new layout data.

### Detailed Changes

#### js/physics.js

1. Add `wrapped` boolean tracking in `updateOnTrack()`:
- Set `wrapped = true` when `ball.z > halfLength` triggers the wrap.
- Include `wrapped` in the return object (default `false`).
- Also return `wrapped: false` from `updateFalling()`.

2. Add new export `refreshLevel(config)`:
```js
export function refreshLevel(config) {
obstacles = config.obstacles || [];
coins = config.coins || [];
coinsCollected = new Array(coins.length).fill(false);
turtle = config.turtle || null;
turtleCollected = false;
}
```
This updates level data without touching ball state or slowdown timers.

#### js/main.js

1. Import `refreshLevel` from `physics.js`.
2. In the game loop, after `updatePhysics()`, check `result.wrapped`:
```js
if (result.wrapped) {
regenerateLevel();
const newConfig = getTrackConfig();
newConfig.obstacles = getObstacles();
newConfig.coins = getCoins();
newConfig.turtle = getTurtle();
refreshLevel(newConfig);
}
```

### Why not other approaches?

- **Regenerate inside physics.js**: Physics shouldn't know about rendering. The existing architecture separates concerns.
- **Just reset coinsCollected on wrap**: Would show the same layout forever (same obstacle positions). The description says "the map becomes the same" which implies it should differ.
- **Use `initPhysics` on wrap**: Would reset ball position to start, causing a visual teleport and losing slowdown state.

## File Changes Summary

| File | Change |
|------|--------|
| `js/physics.js` | Add `wrapped` flag to return objects; add `refreshLevel()` export |
| `js/main.js` | Import `refreshLevel`; handle `result.wrapped` by regenerating level |

## Verification

- `docker build -t teeter .` must succeed
- After the ball reaches the end of the track and wraps, obstacles should appear in different positions
- Coins should be visible after wrapping (fresh coins in new positions)
- Turtle powerup should reappear after wrapping
- Score should persist across wraps (not reset to 0)
- Existing game-over/restart flow should still work
24 changes: 24 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@
#gameover-box .go-score {
font-size: 1.4em;
opacity: 0.8;
margin-bottom: 8px;
}
#gameover-box .go-time {
font-size: 1.2em;
opacity: 0.7;
margin-bottom: 24px;
}
#gameover-box .go-message {
Expand Down Expand Up @@ -224,6 +229,23 @@
#leaderboard-close:hover {
background: rgba(255,255,255,0.1);
}
#timer {
position: fixed;
top: 16px;
left: 50%;
transform: translateX(-50%);
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 1.4em;
font-weight: 700;
z-index: 10;
pointer-events: none;
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
background: rgba(0,0,0,0.3);
padding: 6px 14px;
border-radius: 8px;
display: none;
}
#slowdown-indicator {
position: fixed;
bottom: 50px;
Expand Down Expand Up @@ -267,6 +289,7 @@
</head>
<body>
<div id="score">Score: 0</div>
<div id="timer">0.0s</div>
<button id="leaderboard-btn">Leaderboard</button>
<div id="overlay">
<div class="title">TEETER</div>
Expand All @@ -276,6 +299,7 @@
<div id="gameover-box">
<div class="go-title">GAME OVER</div>
<div class="go-score"></div>
<div class="go-time"></div>
<div class="go-message"></div>
<div id="name-entry">
<label for="name-input">Enter your name:</label>
Expand Down
107 changes: 74 additions & 33 deletions js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,25 @@ import {
getObstacles,
getCoins,
hideCoin,
showAllCoins,
updateCoinRotation,
regenerateLevel,
getTurtle,
hideTurtle,
} from './renderer.js';

import { initTracker, calibrate, detectTilt, detectPitch, resetTilt } from './tracker.js';
import { initPhysics, updatePhysics, resetBall, refreshLevel } from './physics.js';
import { initPhysics, updatePhysics } from './physics.js';

const overlay = document.getElementById('overlay');
const subtitle = overlay.querySelector('.subtitle');
const scoreEl = document.getElementById('score');
const timerEl = document.getElementById('timer');
const leaderboardBtn = document.getElementById('leaderboard-btn');
const gameoverOverlay = document.getElementById('gameover-overlay');
const gameoverTitle = gameoverOverlay.querySelector('.go-title');
const gameoverScore = gameoverOverlay.querySelector('.go-score');
const gameoverMessage = gameoverOverlay.querySelector('.go-message');
const gameoverTime = gameoverOverlay.querySelector('.go-time');
const nameEntry = document.getElementById('name-entry');
const nameInput = document.getElementById('name-input');
const nameSubmit = document.getElementById('name-submit');
Expand All @@ -37,18 +39,35 @@ const slowdownIndicator = document.getElementById('slowdown-indicator');
const STORAGE_KEY = 'teeter_highscores';
const MAX_SCORES = 10;
const NON_QUALIFYING_DELAY = 2000;
const FINISH_DISPLAY_DELAY = 3000;

let state = 'loading'; // loading | permission | playing | falling | gameover
let state = 'loading'; // loading | permission | playing | falling | finished | gameover
let lastTime = 0;
let resetTimer = null;
let score = 0;
let finalScore = 0;
let runStartTime = 0;
let runElapsed = 0;

function updateScore(value) {
score = value;
scoreEl.textContent = 'Score: ' + score;
}

function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
const ms = Math.floor((seconds % 1) * 10);
if (mins > 0) {
return mins + ':' + String(secs).padStart(2, '0') + '.' + ms;
}
return secs + '.' + ms + 's';
}

function updateTimerDisplay() {
timerEl.textContent = formatTime(runElapsed);
}

// --- localStorage leaderboard ---

function loadScores() {
Expand All @@ -70,7 +89,7 @@ function saveScores(scores) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(scores));
} catch {
// storage unavailable — silently fail
// storage unavailable
}
}

Expand Down Expand Up @@ -125,13 +144,39 @@ function hideLeaderboard() {
leaderboardPanel.classList.remove('visible');
}

// --- Game over flow ---
// --- Finish & Game over flow ---

function enterFinished() {
finalScore = score;
state = 'finished';

gameoverTitle.textContent = 'COURSE COMPLETE!';
gameoverScore.textContent = 'Score: ' + finalScore;
gameoverTime.textContent = 'Time: ' + formatTime(runElapsed);

if (scoreQualifies(finalScore)) {
gameoverMessage.textContent = 'New high score!';
nameEntry.classList.add('visible');
nameInput.value = '';
nameInput.focus();
} else {
gameoverMessage.textContent = 'Well done!';
nameEntry.classList.remove('visible');
resetTimer = setTimeout(() => {
exitGameOver();
}, FINISH_DISPLAY_DELAY);
}

gameoverOverlay.classList.add('visible');
}

function enterGameOver() {
finalScore = score;
state = 'gameover';

gameoverTitle.textContent = 'GAME OVER';
gameoverScore.textContent = 'Score: ' + finalScore;
gameoverTime.textContent = 'Time: ' + formatTime(runElapsed);

if (scoreQualifies(finalScore)) {
gameoverMessage.textContent = 'New high score!';
Expand All @@ -141,7 +186,6 @@ function enterGameOver() {
} else {
gameoverMessage.textContent = '';
nameEntry.classList.remove('visible');
// Auto-dismiss after delay
resetTimer = setTimeout(() => {
exitGameOver();
}, NON_QUALIFYING_DELAY);
Expand Down Expand Up @@ -178,8 +222,15 @@ function exitGameOver() {
calibrate(performance.now());
resetBallRotation();
updateScore(0);
updateBallPosition(0, config.trackHeight / 2 + config.ballRadius, config.ballStartZ);
updateCamera(config.ballStartZ);

// Reset ball to start of curve
const startPos = config.curveLocalToWorld(0, 0, config.ballRadius);
updateBallPosition(startPos.x, startPos.y, startPos.z);
updateCamera(0, startPos);

runStartTime = performance.now();
runElapsed = 0;
updateTimerDisplay();
state = 'playing';
}

Expand All @@ -203,7 +254,6 @@ leaderboardClose.addEventListener('click', () => {
hideLeaderboard();
});

// Close leaderboard on backdrop click
leaderboardPanel.addEventListener('click', (e) => {
if (e.target === leaderboardPanel) {
hideLeaderboard();
Expand All @@ -214,22 +264,18 @@ leaderboardPanel.addEventListener('click', (e) => {

async function init() {
try {
// Initialize Three.js renderer
initRenderer();
const config = getTrackConfig();

// Attach obstacle, coin, and turtle data to config for physics
config.obstacles = getObstacles();
config.coins = getCoins();
config.turtle = getTurtle();
initPhysics(config);

// Initial render so the scene is visible during loading
render();

subtitle.textContent = 'Requesting camera access...';

// Request camera
let stream;
try {
stream = await navigator.mediaDevices.getUserMedia({
Expand All @@ -242,7 +288,6 @@ async function init() {

subtitle.textContent = 'Loading head tracking model...';

// Initialize head tracker
await initTracker(stream);

// Calibrate neutral head position
Expand All @@ -251,8 +296,12 @@ async function init() {
// Hide overlay, show score and leaderboard button, and start game
overlay.classList.add('hidden');
scoreEl.style.display = 'block';
timerEl.style.display = 'block';
leaderboardBtn.style.display = 'block';
updateScore(0);
runStartTime = performance.now();
runElapsed = 0;
updateTimerDisplay();
state = 'playing';
lastTime = performance.now();
requestAnimationFrame(gameLoop);
Expand All @@ -276,30 +325,31 @@ function gameLoop(timestamp) {
lastTime = timestamp;

if (state === 'playing' || state === 'falling') {
// Get head tilt and pitch
// Update run timer
runElapsed = (timestamp - runStartTime) / 1000;
updateTimerDisplay();

const tiltAngle = detectTilt(timestamp);
const pitch = detectPitch();

// Update physics
const result = updatePhysics(dt, tiltAngle, pitch);

// Update renderer
updateBallPosition(result.x, result.y, result.z);
updateBallRotation(result.vx, result.vz, dt);
updateCamera(result.z);

// Animate coins
// Camera follows curve tangent at ball's t position
const ballWorldPos = { x: result.x, y: result.y, z: result.z };
updateCamera(result.t, ballWorldPos);

updateCoinRotation(dt);

// Handle coin collection
if (result.coinsCollected && result.coinsCollected.length > 0) {
for (const idx of result.coinsCollected) {
hideCoin(idx);
updateScore(score + 1);
}
}

// Handle turtle collection
if (result.turtleCollected) {
hideTurtle();
}
Expand All @@ -311,18 +361,9 @@ function gameLoop(timestamp) {
slowdownIndicator.classList.remove('visible');
}

// Handle track completion — regenerate level with fresh coins
if (result.trackCompleted) {
regenerateLevel();
const config = getTrackConfig();
config.obstacles = getObstacles();
config.coins = getCoins();
config.turtle = getTurtle();
initPhysics(config);
resetBallRotation();
slowdownIndicator.classList.remove('visible');
updateBallPosition(0, config.trackHeight / 2 + config.ballRadius, config.ballStartZ);
updateCamera(config.ballStartZ);
// Handle finish line crossing
if (result.finished && state === 'playing') {
enterFinished();
}

// Handle state transitions
Expand Down
Loading
Loading