Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 24 additions & 1 deletion projects/sunwake-a6f3/JOURNAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ Palette: ink plum #342c40, chalk #faf7f2, sand #f3ba7b, apricot #ef986f, rose #b
- [x] Browser playtest on desktop and mobile; exact repository CI gate.
- [x] Original static 16:10 catalog thumbnail.
- [x] Prepare and verify a single-project release for the automatic PR publishing workflow.
- [ ] Future: optional shareable ghost replays and more skiff silhouettes.
- [x] Shareable courses and local best-run ghost replays.
- [ ] Future: more skiff silhouettes.

## Session log

Expand All @@ -42,3 +43,25 @@ Palette: ink plum #342c40, chalk #faf7f2, sand #f3ba7b, apricot #ef986f, rose #b
- Visually reviewed desktop/mobile welcome, flight, results, and region palettes; loaded the built game at a nested static-server subpath without failed asset requests. Browser screenshots stay ignored in `qa/artifacts/`.

- 2026-09-24 (codex / gpt-6): Built and balanced the complete Sunwake game, refined mobile lookahead and dark-region contrast, verified persistence and reward isolation, and passed the exact publication gate. Release is ready for the automatic pull-request workflow.

## Second journey — improvement plan

- [x] Varied continuous terrain profiles, thermals, shields, magnets, ring chains, and perfect landings.
- [x] Best-run ghosts, same-course retries, and shareable versioned course links.
- [x] Flight log, richer debriefs, expanded challenges, and old-save migration.
- [x] Contextual flight coaching, readable power-up timers, larger touch controls, and input fixes.
- [x] Region-specific sky scenery and performance improvements.
- [x] Verify determinism, balance, persistence, browser behavior, and the exact gate; prepare the update for automatic PR publishing.

## Version 0.2 decisions

- Four dune profiles now vary the rhythm of each region. Quintic blending preserves terrain height and slope through every transition. Coordinate-based placement keeps shared courses independent of player input.
- Shields absorb one obstacle hit. Magnets gather sparks for ten seconds and pull fast enough to catch a bursting skiff. Rising thermals reward an open sail. Three consecutive rings within nine seconds of one another grant a sky-chain bonus and eight seconds of magnet; missing a ring breaks the chain. Gentle downhill landings earn perfect-landing rewards.
- Active bursts cannot be renewed, and recharge runs at 30% while bursting. In 16-seed simulations capped at 180 seconds, idle/held play averaged about 3.2/3.8 km, rhythmic bursts 9.9 km, and slope-timed bursts 12.0 km. Skilled averages are capped observations, not expected maximum distances. The new pickups extend well-flown journeys without making ordinary holds competitive.
- Course URLs use `#/course/2/<seed>`; unsupported versions are rejected. Links share a landscape, not another person's replay. Ghosts come from this device's best competitive flight on the same course, with at most four stored courses. Recordings start at five samples per second and progressively compact to at most 960 points while preserving both endpoints, including a finish exactly at the compaction boundary.
- Flight history retains twelve journeys, with old daily routes replayable as Voyage. The debrief shows a height trace, perfect landings, sky chains, longest glide, top speed, and a situational tip. Four new challenges extend the journal to sixteen. Existing records, skiffs, light, and completed challenges migrate without resetting progress.
- Coaching and ghost visibility are saved preferences. Input sources release independently. Portrait event messages occupy the coaching slot below the course, keeping approaching obstacles visible; touch targets and debrief labels are larger. Reduced motion freezes ambient flourishes and removes rapid invincibility blinking.
- Violet Reach gains suspended islands and Blue Hour gains aurora ribbons. Terrain phase caching and fewer contour samples reduce render work; ready and paused screens no longer schedule repeated React HUD snapshots.
- Verification: 22 progress/replay/input checks and expanded engine checks pass, including deterministic frame rates, four-minute free flight, shields, chains, thermals, magnet pursuit at burst speed, and smooth terrain. Twelve production browser scenarios pass with clean consoles, covering old flows plus shared-route ghosts, log replay, independent inputs, preferences, blocked clipboard/storage, and landscape fullscreen. The exact publication gate passes. Final visual review covered desktop, 320/390 px portrait, 844 px landscape, violet islands, and blue aurora. Landscape touch controls retain their styling, and the results fit without scrolling.

- 2026-09-24 (codex / gpt-6): Thoroughly expanded Sunwake's flight mechanics, replay loop, world variation, coaching, and saved history; balanced the new rewards, preserved existing progress, and passed the exact gate plus engine/unit/browser coverage. Version 0.2 is ready for the automatic PR publishing workflow.
20 changes: 15 additions & 5 deletions projects/sunwake-a6f3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

**Ride the last light.** An original, momentum-driven distance game about a solar skiff surfing an endless sea of dunes.

Hold to dive into a downhill slope. Release to launch. Thread sun rings, collect light, and turn clean landings into a solar burst. Four illustrated regions cycle across an endless, seeded landscape.
Hold to dive into a downhill slope. Release to launch. Thread sun rings, collect light, and turn clean landings into a solar burst. Four illustrated regions bring different dune rhythms across an endless, seeded landscape.

Catch rising thermals, chain three rings for a light magnet, carry a sun shield through danger, and meet downhill slopes gently for perfect landings. Timing matters: skillful dives and well-placed bursts carry you much farther than simply holding a button.

## Play

Expand All @@ -18,8 +20,13 @@ Hold to dive into a downhill slope. Release to launch. Thread sun rings, collect
- **Daily flight:** the same course for the same UTC date, with a personal daily record. No online leaderboard or account.
- **Free flight:** unlimited sunlight for practice. Counts toward lifetime flight count and distance, but does not award light, challenges, or competitive records.
- **The hangar:** spend earned light on Manta and Comet. All three skiffs share the same physics.
- **Race this route:** retry the same seed against a translucent replay of your best flight. Ghosts are saved for the four most recent improved courses; Free flight does not create competitive ghosts.
- **Course links:** share the exact landscape with a versioned URL. Recipients race their own local ghosts; the link shares the course, not your saved replay.
- **Flight log:** revisit the last twelve journeys, replay any course, and compare distance, duration, score, and light collected. Historical daily courses replay as Voyage.

The flight guide explains each pickup and lets you switch coaching or ghost visibility off. Your debrief includes a height trace, perfect landings, sky chains, longest glide, top speed, and a tip for the next journey. Sixteen challenges reward progress through the game.

Progress is stored on this device with `localStorage`. If browser storage is blocked, the game still works for the current session. Audio starts only after a gesture and can be muted. The game pauses on focus loss and follows the system’s reduced-motion preference.
Progress is stored on this device with `localStorage`. Existing Sunwake records, unlocks, and earned light survive the update. If browser storage is blocked, the game and ghost replay still work for the current session. Audio starts only after a gesture and can be muted. The game pauses on focus loss and follows the system’s reduced-motion preference.

## Develop

Expand All @@ -38,18 +45,18 @@ From this folder:
pnpm lint
pnpm build
node qa/engine-check.mjs
node --test tests/progress.test.mjs
node --test tests/*.test.mjs
```

Engine checks require Node 24+ native TypeScript support. They cover stable results at 30/60/144 Hz, seeded courses, input transitions, collision grace, charge use, attainable airtime, and bounded effects during long runs. Progress checks cover malformed saves, unavailable storage, reward eligibility, daily records, and practice isolation.
Engine checks require Node 24+ native TypeScript support. They cover stable results at 30/60/144 Hz, seeded courses, input transitions, collision grace, charge use, attainable airtime, continuous terrain transitions, new pickups, and bounded effects during long runs. Progress and replay checks cover old-save migration, malformed saves, unavailable storage, reward eligibility, daily records, practice isolation, shared seeds, interpolation, multi-source input, and bounded hour-long ghost recordings.

For browser checks, install a Playwright Chromium browser (`pnpm exec playwright install chromium`) and serve the built app with `pnpm preview --host 127.0.0.1 --port 4179`. Then:

```sh
SUNWAKE_URL=http://127.0.0.1:4179 node qa/browser-check.mjs
```

Set `CHROMIUM_PATH` if using an existing browser executable. The suite exercises the real canvas loop and React UI, including desktop and touch controls, pause/resume, once-per-run rewards, saved purchases, repeatable daily flights, dialogs, and narrow-screen layouts. Local screenshots are ignored under `qa/artifacts/`.
Set `CHROMIUM_PATH` if using an existing browser executable. The suite exercises the real canvas loop and React UI, including desktop and touch controls, pause/resume, once-per-run rewards, saved purchases, repeatable daily flights, shared courses, ghost accuracy, history replay, preferences, blocked clipboard/storage, dialogs, and narrow-screen layouts. Local screenshots are ignored under `qa/artifacts/`.

The exact repository publishing gate, from the repository root:

Expand All @@ -62,6 +69,9 @@ node scripts/verify-project.mjs sunwake-a6f3
- `src/game/engine.ts`: fixed-timestep physics, terrain, seeded pickups and hazards, collision handling, and scoring.
- `src/game/renderer.ts`: layered landscapes, skiffs, effects, region transitions, and the personal-best flag.
- `src/game/progress.ts`: validated saves, challenges, daily seeds, and cosmetic unlocks.
- `src/game/replay.ts`: versioned course links, bounded recordings, saved ghosts, and interpolation.
- `src/game/input.ts`: independent keyboard/pointer holds and short burst pulses.
- `src/game/audio.ts`: gesture-unlocked wind, ambient tones, and event sounds.
- `src/App.tsx`: menus, input, HUD, results, accessibility, and persistence integration.
- `src/FlightExtras.tsx`: flight coaching, route progress, pickup status, and post-flight analysis.
- `JOURNAL.md`: design decisions, shipped work, and future ideas.
2 changes: 1 addition & 1 deletion projects/sunwake-a6f3/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "sunwake",
"private": true,
"version": "0.0.0",
"version": "0.2.0",
"type": "module",
"packageManager": "pnpm@11.5.2",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion projects/sunwake-a6f3/project.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"title": "Sunwake",
"description": "Ride the last light in a momentum-driven dune-surfing odyssey. Dive, soar, thread sun rings, and chase the horizon through four endless worlds.",
"description": "Surf four endless worlds, ride thermals, chain sun rings, and race your own ghost. Share a course, unlock skiffs, and chase your next horizon.",
"agent": "codex",
"model": "gpt-6",
"tags": ["game", "arcade", "adventure"],
Expand Down
105 changes: 104 additions & 1 deletion projects/sunwake-a6f3/qa/browser-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,109 @@ try {
await expect(page.getByRole('button', { name: 'Enable sound', exact: true })).toHaveAttribute('aria-pressed', 'false')
})

await scenario('shared-course-ghost-log-and-reload', { width: 1280, height: 950 }, async page => {
await page.goto(`${baseURL}/#/course/2/3j`, { waitUntil: 'networkidle' })
await expect(page.locator('.mode-description')).toContainText('Shared course')
await fly(page)
await page.clock.runFor(6000)
await finish(page)
const saved = await progress(page)
assert.equal(saved.history.length, 1)
assert.equal(saved.history[0].seed, 127)
const ghost = await page.evaluate(() => JSON.parse(localStorage.getItem('sunwake.ghosts.v2'))[0])
assert.equal(ghost.seed, 127)
assert.ok(ghost.points.length >= 20)
await expect(page.locator('.flight-debrief')).toBeVisible()
await page.getByRole('button', { name: 'Race this route', exact: true }).click()
await page.clock.runFor(2500)
await expect(page.locator('.ghost-gap')).toBeVisible()
const gap = await page.locator('.ghost-gap b').innerText()
assert.ok(integer(gap) <= 3, `Matching inputs should closely match the ghost: ${gap}`)
await finish(page)
await page.reload({ waitUntil: 'networkidle' })
await page.getByRole('button', { name: 'Open flight log', exact: true }).click()
await expect(page.locator('.flight-log-row')).toHaveCount(2)
await page.locator('.flight-log-row').last().getByRole('button').click()
await page.clock.runFor(800)
await expect(page.locator('.flight-stage')).toHaveClass(/phase-running/)
await expect(page.locator('.ghost-gap')).toBeVisible()
await page.screenshot({ path: `${artifactDirectory}/ghost-replay-verified.png` })
})

await scenario('coaching-preferences-and-independent-inputs', { width: 1280, height: 950 }, async page => {
await fly(page)
const box = await page.locator('canvas').boundingBox()
await page.keyboard.down('Space')
await page.mouse.move(box.x + box.width * .6, box.y + box.height * .4)
await page.mouse.down()
await page.clock.runFor(500)
await page.keyboard.up('Space')
await page.clock.runFor(150)
await expect(page.locator('.flight-coach i')).toHaveText('DIVING')
await page.mouse.up()
await page.clock.runFor(150)
await expect(page.locator('.flight-coach i')).toHaveCount(0)
await page.getByRole('button', { name: 'How to fly' }).click()
await page.getByRole('switch', { name: 'Flight coaching' }).click()
await page.getByRole('switch', { name: 'Race your ghost' }).click()
await page.keyboard.press('Escape')
await page.getByRole('button', { name: 'Keep flying', exact: true }).click()
await page.clock.runFor(150)
await expect(page.locator('.flight-coach')).toHaveCount(0)
await finish(page)
assert.equal((await progress(page)).coach, false)
await page.reload({ waitUntil: 'networkidle' })
await fly(page)
await expect(page.locator('.flight-coach')).toHaveCount(0)
assert.equal((await progress(page)).ghost, false)
})

await scenario('course-share-clipboard-fallback', { width: 390, height: 844 }, async page => {
await page.goto(`${baseURL}/#/course/2/3j`, { waitUntil: 'networkidle' })
await fly(page)
await page.clock.runFor(1200)
await finish(page)
await page.evaluate(() => Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: async () => { throw new Error('Clipboard denied') } } }))
await page.getByRole('button', { name: 'Copy course link', exact: true }).click()
await expect(page.getByRole('dialog', { name: 'Share this course' })).toBeVisible()
await expect(page.locator('.share-field input')).toHaveValue(/#\/course\/2\/3j$/)
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth))
await page.keyboard.press('Escape')
await expect(page.locator('.results-card')).toBeVisible()
}, true)

await scenario('blocked-storage-still-replays', { width: 1280, height: 950 }, async page => {
await page.evaluate(() => { Storage.prototype.setItem = () => { throw new Error('Storage unavailable') } })
await fly(page)
await page.clock.runFor(1800)
await finish(page)
await expect(page.locator('.saved-note')).toContainText('this session')
await page.getByRole('button', { name: 'Race this route', exact: true }).click()
await page.clock.runFor(800)
await expect(page.locator('.ghost-gap')).toBeVisible()
})

await scenario('landscape-fullscreen-touch-and-results', { width: 844, height: 390 }, async page => {
await page.goto(`${baseURL}/#/course/2/3j`, { waitUntil: 'networkidle' })
await page.getByRole('button', { name: 'Toggle fullscreen', exact: true }).click()
assert.ok(await page.evaluate(() => Boolean(document.fullscreenElement)))
await fly(page)
const dive = page.getByRole('button', { name: 'Hold to dive, release to soar', exact: true })
await expect(dive).toBeVisible()
const style = await dive.evaluate(el => ({ height: el.clientHeight, radius: getComputedStyle(el).borderRadius }))
assert.ok(style.height >= 44 && style.radius !== '0px', 'Landscape touch controls need their full styling and tap area')
await page.clock.runFor(8800)
await page.screenshot({ path: `${artifactDirectory}/landscape-flight-verified.png` })
await finish(page)
const dimensions = await page.locator('.results-card').evaluate(el => ({ height: el.clientHeight, scroll: el.scrollHeight }))
assert.ok(dimensions.scroll <= dimensions.height + 2, `Fullscreen results must fit: ${JSON.stringify(dimensions)}`)
await page.screenshot({ path: `${artifactDirectory}/landscape-results-verified.png` })
await page.getByRole('button', { name: 'Race this route', exact: true }).click()
await page.clock.runFor(800)
await expect(page.locator('.ghost-gap')).toBeVisible()
await page.evaluate(() => document.exitFullscreen())
}, true)

for (const width of [320, 390]) {
await scenario(`mobile-${width}`, { width, height: 844 }, async page => {
const noOverflow = async label => {
Expand Down Expand Up @@ -272,5 +375,5 @@ if (failures.length) {
} else {
console.log(process.env.SUNWAKE_CHECK
? `Selected browser checks passed: ${process.env.SUNWAKE_CHECK}; clean browser console.`
: 'Browser checks passed: desktop and touch controls, pause/resume, one-time settlement, persistence, seeded daily flights, practice isolation, hangar, dialogs, audio controls, responsive widths, and clean browser console.')
: `All ${scenariosRun} browser checks passed: controls, saved progress, daily courses, practice, purchases, audio, shared-route ghosts, history, preferences, clipboard/storage fallbacks, mobile layouts, and clean consoles.`)
}
Loading
Loading