Skip to content

Fjord town - #10

Merged
datagutt merged 153 commits into
masterfrom
game
Sep 28, 2026
Merged

datagutt merged 153 commits into
masterfrom
game

Conversation

@datagutt

@datagutt datagutt commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

its a game

…Town

Records the decisions from the design session, the milestone plan with
task checkboxes, and a handoff file. A SessionStart hook injects the
handoff and open tasks; a Stop hook asks for a handoff update when code
commits are newer than the last one.
- scripts/assets/fetch.mjs finds the private art (sibling checkout,
  ASSETS_DIR, or token clone) and falls back to placeholders, refusing
  placeholders in production builds; build runs it first
- phaser 4, inkjs, pngjs, vitest, Playwright; @types/node 24
- pnpm lint uses the ESLint CLI (next lint is gone in Next 16) and
  forbids Next.js/React imports under game/
- vitest unit tests and a Playwright smoke test of / on desktop and phone
…t 16.3.6

- / is a static, server-rendered title screen (SVG fjord at dusk) that boots
  Phaser behind it; Start enters the world. The old page moved to /legacy,
  /journal is a stub.
- game/: framework-free boot, low-res canvas scaled by whole device pixels
  (viewport.ts, unit tested), Boot/Preload/World scenes.
- pnpm game:dev: standalone esbuild harness with live reload.
- Next 16.2 canary -> 16.3.6, React 19.3, React Compiler 1.0. Turbopack's
  disk cache is off on WSL-mounted drives (fsync fails there, which caused
  the dev panic); removed the dropped viewTransition option.
- lib/github.ts: pinned-repo scrape falls back to [] on network errors
  like the other fetchers instead of failing the page.
The repository previously had no licence. Code is now GPL-3.0-or-later;
the README lists what is not covered (Pangram Pangram fonts, images,
LimeZu art, Geist fonts under OFL).
- scripts/assets/build.mjs writes public/game/ (gitignored): characters
  composited from LimeZu generator layers with colour swaps (datagutt
  matches the avatar), or placeholder figures without the art repo; a
  generated greybox tileset; Tiled .tmj maps; Geist Pixel as a 1-bit
  bitmap font.
- world/: a small map-drawing API and the greybox town and house,
  written as code (the seed of the M3 generator).
- Lockfile for sharp, opentype.js and wawoff2 (the previous commit's
  package.json change landed without it).
Tiled loading with tile-property collision, tile-locked movement with
turn-in-place and chained steps, sign and NPC interaction with a simple
dialogue box, doors between maps, tap/click-to-move with A* (straight-run
preference) and walk-to-talk, unified keyboard/gamepad/pointer input, a
versioned localStorage save that survives hostile storage, and ?at=
deep links that skip the title. Pure logic is unit tested.
Plays the real page: deep link, doors both ways, talking to datagutt,
continuing from a save. CI runs lint, typecheck, unit tests, a
placeholder-art build and the e2e suite.
data/ moves to content/ (with slug project ids) and gains profile,
socials and places. places.ts is the whole town content map; a test
checks every project and job has exactly one place. The legacy
components and lib/github read from it, and the game uses it for ?at=
deep links. Discord stays out of socials, as on the old site.
It routes server fetch() calls to a Playwright test proxy that is not
running, so every GitHub fetch failed with 'other side closed' or an
HTTP parser error (seen in dev on Next 16.3).
lib/world-state.ts combines pinned repos, stats and contributions; a
cached server component embeds it in / as JSON and game/live reads it
with a safe fallback. lib/github.ts moves from unstable_cache to
'use cache', logs failures, and caches failed fetches for minutes
instead of an hour. / stays prerendered and revalidates hourly.
Dialogue is written in Ink under game/dialogue/ink. The asset build
compiles it, declares every external from one registry
(game/dialogue/externals.ts) and fails on unknown literal ids such as
project_desc("irlsever") or NPC knots that don't exist. At runtime a
DialogueRunner plays knots beat by beat, facts come from content/ and
live data, and visit counts persist in the save. The dialogue box gains
a choice list (keyboard, gamepad, tap). Ferryman and datagutt get
drafted conversations.
Picking one choice used to hide the others for good: the next visit
went straight to the repeat greeting. NPCs now loop back to a topics
hub with once-only questions and a sticky goodbye, so every fact stays
reachable across visits. main.ink documents the pattern.
The dialogue box is now a nine-slice of LimeZu's wood-rimmed parchment
frame (a drawn stand-in with the same palette in placeholder mode), with
a matching name tab, dark ink text, and short pauses after punctuation.
Portraits are composited at build time from the portrait generator
layers (with the same colour swaps as the sprite, so datagutt keeps his
yellow hair and black glasses), cropped to the head area and shown at
2x. The mouth moves while text types; # nod and # shake Ink tags play
a gesture first. Placeholder mode draws a stand-in face.
Undertale-style blips synthesised with Web Audio as text appears, with a
voice per character (the ferryman is low and slow, datagutt higher and
quicker). They play through Phaser's sound output, so the saved mute
applies. The ?debug readout now refreshes every frame, including during
dialogue, and counts blips for the e2e test.
Ink lines can carry # link: project <id>, social <id> or email tags,
validated at build time against content. After such a line the game
asks "Open github.com?" and opens it in a new tab. Because browsers
block popups outside a real user event and Phaser handles input a frame
later, LinkOpener opens the tab from a window listener during the actual
key press or tap. Direction presses are now queued so fast presses
aren't lost in menus.
Eleven NPCs with names, places, one-line personalities and voices in
game/npcs.ts, and sprite recipes in the asset manifest. Portraits are
now derived from the sprite layers (the generators share numbering),
so each NPC is defined once. Awaiting the user's review.
Drafted Ink for Sunniva (Guac.tv), Kjell (IRLServer), Randi
(Donate.chat), Ida (Nettbureau), Bjørn (Indre Østfold Data), Tor (tech
stack), Solveig (live pinned repos), Ola (live stats) and Liv (contact
and socials), all using the topics pattern and content functions. Every
NPC now waits in the greybox town near their future building. Tests
check that dialogue mentions every project, job, skill category, social
link and live stat, and walk every conversation to the end with and
without live data. Coffee jokes became energy drink jokes, as is only
accurate.
Finishing a conversation with a place's main NPC stamps that place in
the passport: a banner drops in, a synthesised rubber-stamp thunk plays
and the camera shakes a little (not with reduced motion). The passport
page lists all ten places. Progress now lives in one Progress object
shared through the registry and written to the save, instead of
re-reading the save on every step.
Enter, gamepad Start or the on-screen Menu button opens a menu with the
passport, the plain-text Journal, settings (sound, reduced motion, both
saved) and credits, including the LimeZu attribution the art licence
requires. It works with keys, gamepad and taps, with soft menu ticks.
Completes M2.
…layers and packed tileset (M3.1)

pnpm world:gen writes world/maps/*.tmj from world/gen/maps, keeping manual_* layers.
Tile ids live in the append-only world/tile-ids.json; the build packs them into one
tileset from the art, or from coarse committed colour sketches in placeholder mode.
The game reads any tileset by name, hidden collision layers and manual object layers.
?debug&map=<id> opens a map in progress.
…rest (M3.3, M3.7 wip)

Prefabs for villas (one recoloured falu red for datagutt's house), civic buildings,
farm, smithy, office, post office, kiosk and trees; landscape helpers for plateaus with
stairs, piers, forests and buildings with doors. Renders can carry a labelled grid.
The tileset now holds 16k tiles and world:gen --prune drops dead registry entries.
…tart)

The validator runs on every world:gen and world:check. It caught the office and
smithy blocking their front steps, which are now walkable.
Approved by the user. The greybox town is retired (the greybox house stays until its
interior); spawns and e2e follow the new layout. Doors are wired only to interiors
that exist, and the asset build fails on doors to unknown maps or spawns.

Pier fixes from review: the camping sheet's baked-in water is cleared (campingDry) so
the sea shows under the pier ends, the life ring is drawn whole, and the rowboat crop
no longer catches half a neighbouring boat.
The engine ships its English UI copy by key; a game overrides any key in
content/strings.json, and an unknown key fails the content build. The
town's own one-off lines move into Ink.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
…neric world

The world scene gets a plugin API: object types, usables, prompts, talk
and stamp hooks, menu items, takeovers and debug fields. The cat, the
cabinets, the live GitHub objects, the ferry intro, the finale, the live
Thomas NPC and the Journal link become plugins; summit, the edge lines
and the passport achievement become declarative triggers.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
createGame() takes the game's config, compiled content, live data,
link resolver, dialogue functions and plugins. Content reaches scenes
and ui through a GameData object on the services instead of module
imports of the app. The weather types move into the runtime so that
kai-live can depend on it without a cycle.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
…le plugins

presenceNpc takes its places, routes, rules, words, story-night spot and
mock presences from content/presence.json, so any game can have a live
developer walking its town. githubObjects draws the contribution field
and the repo shelf. The debug hook becomes window.__kai, and an e2e test
checks that saves from before the split still load.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
… world socket

useKaiGame and useMenuKeys carry the title screen's lifecycle and
keyboard; the site keeps its look. LiveDataScript embeds the live
payload, worldSocketHandler serves the ghost rooms, and cached wrappers
sit over kai-live's GitHub and weather fetchers, which no longer know
about Next.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
The runtime decides that a conversation reached an NPC's own knot by
its visit count, but Ink only counts knots the story refers to. Fjord
Town's knots happened to be referenced; a new game's were not, so
talking to its NPCs never gave a stamp.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
…o.json

An app with its own turbo.json was taken for the repository root, so
the local art checkout next to the repository was not found.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
One map, one NPC, one conversation, two triggers and placeholder art
without the token, played by a smoke test in the dev harness. It proves
the engine needs nothing from datagutt, and game 2 starts as a copy.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
packages never import apps, runtime code never imports the build-time packages or
schemas (except as types), and the runtime stays free of next and react.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
…fjord town

also lint that the map toolkit and the asset build never import an art adapter.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
matches the art repository, where art for one game now lives under games/<id>/.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
the finale only started at the moment of the last stamp, so anything that missed that
moment lost it for good. it now resumes on the next map while the passport is full and
the finale unfinished, and waits until the world is free instead of a fixed timer.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
…game or plugin

the engine hardcoded fjord town's cat, arcade, crops and books objects, so a new
object type meant an engine change. a descriptor (defineMapObject) now gives an
object's properties, size and placement; the map checks follow the placement. the
build reads a game's types from MAP_OBJECTS in its maps module, the runtime from the
plugins that place them. the generated maps are byte-identical.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
a sprite object plays a kai.json strip at a tile plus a pixel offset, drawn among the
characters by its bottom edge or on the ground or roof layer, and limited to seasons or
daylight when asked. MapCanvas.reserve keeps the collision of the art a sprite
replaces, renders draw each sprite's first frame, and the build rejects unknown sprite
names, layers, seasons and times of day.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
the windmill's blades turn, the boats bob, the fountain runs, and the fireplace,
treadmill and escalator move, from limezu's own strips. pigeons, gulls, buoys,
butterflies in the warm half of the year and a campfire at the lookout fill places the
maps already have, keeping to the daylight where it suits them.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
kai.json critters names which sprites are animals and how each species gets away when
the player comes close: a bird flies off out of sight, a gull waddles a few walkable
tiles, a butterfly drifts. a built-in plugin runs them and brings them back once the
player has gone; with reduced motion they stay put.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
the square's pigeons are hooded crows now, since limezu's crows have take-off and
flying frames. gulls and butterflies get away too, and the site reads the validated
config from .kai/ like the game.

Claude-Session: https://claude.ai/code/session_014PUagkkCzJQPfsvmUWTdU5
@vercel

vercel Bot commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
datagutt Ready Ready Preview Sep 28, 2026 1:04pm UTC

@datagutt datagutt changed the title docs(game): design record, plan, handoff and session hooks for Fjord … Fjord town Sep 28, 2026
@datagutt
datagutt merged commit 0b420c8 into master Sep 28, 2026
4 checks passed
@datagutt
datagutt deleted the game branch October 8, 2026 09:31

This branch was successfully deployed

1 active deployment
Preview — 3ed501e8 Deployed Sep 24, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant