Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
153 commits
Select commit Hold shift + click to select a range
f15c16d
docs(game): design record, plan, handoff and session hooks for Fjord …
datagutt Sep 22, 2026
7869662
docs(game): link milestone issues and point CLAUDE.md at the game docs
datagutt Sep 22, 2026
3075475
docs(game): M0.5 done, private assets repo created
datagutt Sep 22, 2026
eeacd64
chore(game): M0 tooling: asset fetch, deps, lint boundary, tests
datagutt Sep 22, 2026
7846327
feat(game): title screen and Phaser boot (M1.1, M1.2); upgrade to Nex…
datagutt Sep 22, 2026
ae820ae
Relicense under GPL-3.0-or-later
datagutt Sep 22, 2026
b8f5611
feat(game): asset pipeline and greybox maps (M1.3, M1.12)
datagutt Sep 22, 2026
bd556d5
feat(game): engine core (M1.4 to M1.11)
datagutt Sep 22, 2026
a8718f5
test(game): e2e walkthrough and CI (M1.13)
datagutt Sep 22, 2026
e6d1732
docs(game): M1 complete; handoff points at M2.1
datagutt Sep 22, 2026
3125fd3
feat(content): single content module for game and site (M2.1)
datagutt Sep 22, 2026
c9288ef
fix(config): remove experimental.testProxy
datagutt Sep 22, 2026
03e218d
feat(game): live world state payload (M2.2)
datagutt Sep 22, 2026
5cde67c
feat(game): Ink dialogue pipeline with content validation (M2.3)
datagutt Sep 22, 2026
80c41c1
fix(dialogue): unasked questions stay available (topics pattern)
datagutt Sep 22, 2026
4c8d5f0
feat(game): wood-framed dialogue box with paced typewriter (M2.4)
datagutt Sep 22, 2026
b100598
feat(game): talking portraits in dialogue (M2.5)
datagutt Sep 22, 2026
9fb5752
feat(game): per-character dialogue blips (M2.6)
datagutt Sep 22, 2026
586c4b1
feat(game): open links from dialogue (M2.7)
datagutt Sep 23, 2026
32c313e
feat(game): draft NPC roster for review (M2.8)
datagutt Sep 23, 2026
c2dc98c
feat(dialogue): scripts for every NPC, all content playable (M2.9)
datagutt Sep 23, 2026
424ee97
feat(game): Fjord Passport with stamps (M2.10)
datagutt Sep 23, 2026
502c12f
feat(game): START menu with passport, settings and credits (M2.11)
datagutt Sep 23, 2026
555c51b
feat(world): autotile sets, map canvas and PNG render for the generat…
datagutt Sep 23, 2026
60df8c5
feat(world): generator core with tile registry, Tiled output, manual …
datagutt Sep 23, 2026
d010387
feat(world): overworld draft with buildings, piers, radio hill and fo…
datagutt Sep 23, 2026
7b8cde3
feat(world): overworld scatter, fences, crops, boats and props; docs …
datagutt Sep 23, 2026
281a80d
fix(world): one-storey kiosk, whole ferry boat, nobody stands on the …
datagutt Sep 23, 2026
e723504
feat(world): name signs on every building and a map validator (M3.6 s…
datagutt Sep 23, 2026
9e904f6
feat(world): the generated overworld is the live town (M3.7)
datagutt Sep 23, 2026
d43d630
feat(world): datagutt's house interior from LimeZu Room Builder; grey…
datagutt Sep 23, 2026
17d71b7
feat(world): datagutt's house gets an upstairs (M3.8)
datagutt Sep 23, 2026
af97939
feat(world): rethought house floors, prefab mirroring and rotation
datagutt Sep 23, 2026
1bd9481
feat(world): boathouse studio interior; Sunniva moves in (M3.8)
datagutt Sep 23, 2026
784dfad
feat(world): dock trim around the boathouse slip
datagutt Sep 23, 2026
fa6ab86
feat(engine): light objects and a multiplied shade layer; fix floor a…
datagutt Sep 23, 2026
b171700
feat(world): library interior and talking across counters (M3.8)
datagutt Sep 23, 2026
5fb2e17
feat(world): kiosk interior; counter talk reaches across two tiles (M…
datagutt Sep 23, 2026
64490b7
feat(world): post office interior; Liv moves in (M3.8)
datagutt Sep 23, 2026
014bf85
feat(world): smithy interior; Tor moves in (M3.8)
datagutt Sep 23, 2026
653d1f2
feat(world): farmhouse interior (M3.8)
datagutt Sep 23, 2026
28b56a0
feat(world): the smithy becomes a gym; Tor is a gym bro (M3.8)
datagutt Sep 23, 2026
b9962f9
feat(world): radio hut interior; Kjell moves in (M3.8)
datagutt Sep 23, 2026
8731de0
feat(world): town hall interior; Bjørn moves in (M3.8)
datagutt Sep 23, 2026
98fdea2
docs(game): tidy the handoff's next step
datagutt Sep 23, 2026
7b744a0
feat(world): sprite catalogue from LimeZu singles; prefabs can no lon…
datagutt Sep 23, 2026
57b97ee
fix(world): room to move; collision follows the art; everything reach…
datagutt Sep 23, 2026
2aeba9c
feat(world): closed buildings say so (M3.12); validator complete (M3.6)
datagutt Sep 23, 2026
da9a96c
feat(world): live farm field and library shelf (M3.11); stacked tiles…
datagutt Sep 23, 2026
c732a0d
feat(dialogue): ask anything again once every question has been asked
datagutt Sep 23, 2026
719eaf2
test(e2e): say goodbye once every question has been asked
datagutt Sep 23, 2026
68ced7f
docs(game): handoff after M3.11, ask-again dialogue and art fixes
datagutt Sep 23, 2026
7278c71
fix(world): ground furniture shadows under their base instead of a ro…
datagutt Sep 23, 2026
074e854
feat(world): seasons with swap tables, snow caps and hand-drawn overr…
datagutt Sep 23, 2026
401d4dc
fix(world): snow the gym's lower roof with per-single snow areas
datagutt Sep 23, 2026
a581800
fix(world): snow on every roof in town with ordered roof and cut edits
datagutt Sep 23, 2026
34f8395
fix(game): wrap long dialogue choices instead of overflowing the box
datagutt Sep 23, 2026
2048d01
docs(game): handoff note on wrapped dialogue choices
datagutt Sep 23, 2026
c8a089f
feat(game): npc looks per role and a character review sheet (m3.10)
datagutt Sep 23, 2026
28fed87
feat(game): lanyard presence client fed into the game (m4.1)
datagutt Sep 23, 2026
cc3238b
feat(game): live datagutt npc driven by discord presence (m4.2)
datagutt Sep 23, 2026
f1b6c4a
feat(game): ghost visitors over a world socket with rooms per map (m4…
datagutt Sep 23, 2026
cb10339
feat(game): emote wheel and an other-visitors setting (m4.5)
datagutt Sep 23, 2026
46db4c1
fix(game): the model by the door is a house, and thomas stops pointin…
datagutt Sep 23, 2026
354ff26
chore(site): remove the reactions overlay and lanyard card (m4.6)
datagutt Sep 23, 2026
64c4fc8
feat(game): time of day with dusk and night tints and lamps after dar…
datagutt Sep 23, 2026
cc80c97
feat(game): interaction prompts that follow the input in use (m5.10)
datagutt Sep 23, 2026
9b6303d
feat(game): season particles, winter frost and light june nights (m5.4)
datagutt Sep 23, 2026
ebd752d
feat(game): footstep dust, bump, squash, stamp shake and an easing ca…
datagutt Sep 23, 2026
4d41047
feat(game): fjord ripples and glints in a water shader (m5.2)
datagutt Sep 23, 2026
d8fd2ec
feat(game): northern lights on dark nights, reflected in the fjord (m…
datagutt Sep 23, 2026
849cd45
feat(game): synthesised ambience that follows the player, on audio bu…
datagutt Sep 23, 2026
03e8f6c
feat(game): first-visit ferry intro with arne's welcome (m5.7)
datagutt Sep 23, 2026
fa6577c
feat(game): the finale, from a full passport to credits and contact (…
datagutt Sep 23, 2026
00582fc
perf(game): lossless png shrinking and an auto effects quality settin…
datagutt Sep 23, 2026
323545e
feat(world): nettbureau office interior, first pass, on the modern of…
datagutt Sep 23, 2026
dd868b4
feat(world): big open-plan nettbureau office with departments and six…
datagutt Sep 23, 2026
8601236
feat(world): town hall basement it department with bjørn, racks and p…
datagutt Sep 23, 2026
1df62af
feat(world): looser office with layered desks, lounge games, and a ba…
datagutt Sep 23, 2026
f8a553a
fix(world): office chairs at desks face the desk
datagutt Sep 23, 2026
06453f5
docs(game): note which office chair sprite faces the desk
datagutt Sep 23, 2026
2b75835
Update .gitignore
datagutt Sep 23, 2026
0e98ae7
fix(world): pull the office kitchen and farmhouse chairs up to their …
datagutt Sep 23, 2026
09da3c9
feat(game): title screen v2 with a tiled waterfront, press start and …
datagutt Sep 23, 2026
6ad7ecb
docs(game): tick m5.11, title screen approved without the avatar
datagutt Sep 23, 2026
f57c9af
docs(game): tick m3 to m5 items the user signed off
datagutt Sep 23, 2026
c7dc9e6
feat(journal): the whole site as a plain page with deep links into th…
datagutt Sep 23, 2026
bc018aa
feat(site): metadata, sitemap and a title-card link preview (m6.2)
datagutt Sep 23, 2026
34618fb
feat(game): keyboard and screen reader path to the journal, no key ca…
datagutt Sep 23, 2026
3480750
test(content): keep the limezu credit, tick m6.4
datagutt Sep 23, 2026
1863b18
chore: delete legacy code
datagutt Sep 23, 2026
3169e53
d
datagutt Sep 24, 2026
9c55174
fix(game): building footprints, signs readable across their object, w…
datagutt Sep 24, 2026
a003bd8
docs(site): describe the game architecture in claude.md, tick m6.5
datagutt Sep 24, 2026
9aa402b
feat(game): background music per area, day and night (b4)
datagutt Sep 24, 2026
9fe3a74
feat(game): arcade cabinet games from the old site's canvases (b1, pa…
datagutt Sep 24, 2026
6eadad6
feat(game): youth club with playable arcade cabinets (b1); fix thomas…
datagutt Sep 24, 2026
1f01cf1
feat(game): pc screensaver and stargazing binoculars on the radio hil…
datagutt Sep 24, 2026
3ae6861
feat(game): mountain trail behind a passport gate, and the mountain m…
datagutt Sep 24, 2026
678a8dc
feat(game): achievements, a hidden cat and the edge of the world (b3)
datagutt Sep 24, 2026
3fe1085
feat(game): weather from oslo's real forecast (c2)
datagutt Sep 24, 2026
371dc3e
fix(game): lockable warps and a walled trail gully so the mountain st…
datagutt Sep 24, 2026
578772d
feat(game): weather where the visitor is, oslo as fallback; fix the c…
datagutt Sep 24, 2026
7ee759c
feat(game): readable falling blocks colours, a proper game over, and …
datagutt Sep 24, 2026
e74e1a7
test(site): run the browser matrix on two workers; firefox passes (m6.6)
datagutt Sep 24, 2026
b467431
fix(game): credits draw at whole pixels and scroll at an even pace
datagutt Sep 24, 2026
23b3ce3
docs(game): close the plan except the mine and multi-instance ghosts
datagutt Sep 24, 2026
c09553e
docs(kai): record the engine split design and plan
datagutt Sep 24, 2026
78e783b
build(datagutt): move the site into apps/datagutt on bun and turborepo
datagutt Sep 24, 2026
dcc4282
test(datagutt): expect no link preview image from a placeholder build
datagutt Sep 24, 2026
3a63499
build(tooling): share tsconfig, eslint 9 and vitest presets
datagutt Sep 24, 2026
54a38b4
refactor(kai-net): move the ghost protocol, client and rooms into a p…
datagutt Sep 24, 2026
714cdc1
build(datagutt): build on vercel through turborepo from apps/datagutt
datagutt Sep 24, 2026
f187086
docs(kai): hand off with k1.6 waiting on the vercel approval
datagutt Sep 24, 2026
63e7cac
build(datagutt): install with bun 1.4 on vercel
datagutt Sep 24, 2026
1d8f8b4
refactor(kai-arcade): move the cabinet games into a package
datagutt Sep 24, 2026
8ff11f1
refactor(kai-live): move lanyard, weather and the world state into a …
datagutt Sep 24, 2026
eb8a484
feat(kai): configure a game with kai.json
datagutt Sep 24, 2026
d50816b
refactor(kai): move map objects, seasons and light shapes into the ru…
datagutt Sep 24, 2026
e444868
refactor(kai-worldgen): split world generation into worldgen and lime…
datagutt Sep 24, 2026
06e7acb
feat(kai-assets): compile content from json and markdown with kai con…
datagutt Sep 24, 2026
8b29254
feat(kai): read characters, npcs, music, achievements, unlocks and cr…
datagutt Sep 24, 2026
df1b303
docs(kai): hand off after k4.3
datagutt Sep 24, 2026
0d7f97e
feat(kai-assets): build assets, maps and dialogue with the kai cli on…
datagutt Sep 24, 2026
e6a4cfe
fix(kai-assets): commit the asset build modules the ignore rules hid
datagutt Sep 24, 2026
a892ca7
feat(datagutt): move the maps' words into content/mapText.json
datagutt Sep 24, 2026
8c002c8
test(datagutt): give the finale e2e a time budget instead of a press …
datagutt Sep 24, 2026
b67dba7
feat(kai): take ui copy from strings.json over engine defaults
datagutt Sep 24, 2026
cf88d58
refactor(datagutt): run fjord town's own behaviour as plugins of a ge…
datagutt Sep 24, 2026
1c538e3
refactor(kai): move the generic runtime into the kai package
datagutt Sep 24, 2026
94e0279
docs(kai): hand off after the runtime move
datagutt Sep 24, 2026
28e9eaa
feat(kai-live): run the live npc and the github objects as configurab…
datagutt Sep 24, 2026
ede1373
test(kai): cover game data, the triggers and per-world plugin state
datagutt Sep 24, 2026
fb3c812
feat(kai-next): host a kai game in next with hooks, live data and the…
datagutt Sep 24, 2026
f533e0d
fix(kai-assets): count visits to every ink knot
datagutt Sep 24, 2026
3c644c6
fix(kai-assets): find the repository root by its workspaces, not turb…
datagutt Sep 24, 2026
44c9bad
feat(sandbox): add the smallest kai game as a second app
datagutt Sep 24, 2026
b30aa2d
build(eslint-config): lint the import boundaries between packages
datagutt Sep 24, 2026
74e4c8b
docs(datagutt): move the fjord town docs into the app
datagutt Sep 24, 2026
493fb57
chore(sandbox): stop tracking the generated content bundle
datagutt Sep 24, 2026
dde7b9b
docs(kai): write the architecture, plugin, content and new game guides
datagutt Sep 24, 2026
2533711
docs(kai): add package readmes and split claude.md into monorepo and …
datagutt Sep 24, 2026
6ec9991
docs(kai): hand off at the end of the engine split
datagutt Sep 24, 2026
2a78651
feat(kai-assets): read a game's season overrides from games/<id>/seasons
datagutt Sep 24, 2026
366930e
docs(kai): note k3.5 in the handoff
datagutt Sep 24, 2026
ebdd98e
fix(datagutt): bring the finale back until it is seen to its end
datagutt Sep 24, 2026
93cbb83
docs(kai): record the finale root cause
datagutt Sep 24, 2026
ab82bde
refactor(kai): define map object types as descriptors owned by their …
datagutt Sep 24, 2026
6788445
docs(kai): hand off after merging into game
datagutt Sep 24, 2026
da7d669
feat(kai): animated sprite map objects
datagutt Sep 24, 2026
115e53b
feat(datagutt): animate the town
datagutt Sep 24, 2026
1e3c759
feat(kai): critters that get away from the player
datagutt Sep 24, 2026
3ed501e
feat(datagutt): crows that fly off, gulls that waddle away
datagutt Sep 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
49 changes: 49 additions & 0 deletions .claude/hooks/game-session-start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# SessionStart hook: puts the Fjord Town handoff, the open tasks of the current
# milestone and recent git state into the session context. Plain stdout from a
# SessionStart hook is added to Claude's context.
set -u

root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
branch="$(git -C "$root" rev-parse --abbrev-ref HEAD 2>/dev/null || echo unknown)"
# The kai branches work on the engine split, which keeps its own plan and handoff.
case "$branch" in
kai | kai/*) rel="docs/kai"; title="kai engine split" ;;
*) rel="apps/datagutt/docs"; title="Fjord Town game project" ;;
esac
docs="$root/$rel"
[ -f "$docs/HANDOFF.md" ] || exit 0

echo "## $title: session context"
echo
echo "Branch: $branch. Decisions are in $rel/DESIGN.md and must not be re-litigated."
echo "Update $rel/HANDOFF.md and tick $rel/PLAN.md boxes in the same commit as the work."
echo
cat "$docs/HANDOFF.md"
echo

if [ -f "$docs/PLAN.md" ]; then
# Print the first milestone section that still has unticked tasks, open tasks only.
awk '
/^## [MK][0-9]+:/ { if (found) exit; heading = $0; open = 0; next }
/^## / { if (found) exit; heading = ""; next }
heading != "" && /^- \[ \] \*\*/ {
if (!found) { print "## Open tasks in current milestone"; print heading; found = 1 }
print; inTask = 1; next
}
/^- \[/ { inTask = 0; next }
inTask && /^ / { print; next }
{ inTask = 0 }
' "$docs/PLAN.md"
echo
fi

echo "## Git state"
git -C "$root" log --oneline -5 2>/dev/null
status="$(git -C "$root" status --short 2>/dev/null | head -20)"
if [ -n "$status" ]; then
echo
echo "Uncommitted changes (possibly unfinished work from the last session):"
echo "$status"
fi
exit 0
32 changes: 32 additions & 0 deletions .claude/hooks/game-stop-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
# Stop hook: on the game and kai branches, blocks the session from ending once when
# there are code commits newer than the last commit that touched that branch's HANDOFF.md.
set -u

input="$(cat)"
# Already continuing because of this hook: let the session stop, never loop.
if printf '%s' "$input" | grep -Eq '"stop_hook_active"[[:space:]]*:[[:space:]]*true'; then
exit 0
fi

root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"

branch="$(git -C "$root" rev-parse --abbrev-ref HEAD 2>/dev/null || echo)"
case "$branch" in
game | game/*) rel="apps/datagutt/docs" ;;
kai | kai/*) rel="docs/kai" ;;
*) exit 0 ;;
esac
[ -f "$root/$rel/HANDOFF.md" ] || exit 0

last_code="$(git -C "$root" log -1 --format=%ct -- . ":(exclude)$rel" 2>/dev/null)"
last_handoff="$(git -C "$root" log -1 --format=%ct -- "$rel/HANDOFF.md" 2>/dev/null)"
[ -n "$last_code" ] || exit 0
[ -n "$last_handoff" ] || last_handoff=0

if [ "$last_code" -gt "$last_handoff" ]; then
cat <<JSON
{"decision":"block","reason":"There are commits on $branch newer than the last $rel/HANDOFF.md update. Before stopping: rewrite $rel/HANDOFF.md (current state, next step, blockers, gotchas), make sure finished tasks are ticked in $rel/PLAN.md, and commit both. If this turn only answered a question and the handoff is already accurate, say so and stop."}
JSON
fi
exit 0
26 changes: 26 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/game-session-start.sh\"",
"timeout": 10
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/game-stop-check.sh\"",
"timeout": 10
}
]
}
]
}
}
3 changes: 0 additions & 3 deletions .eslintrc.json

This file was deleted.

33 changes: 33 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: CI

on:
push:
branches: [master, game, kai]
pull_request:

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
# Next, Playwright and the asset scripts run on Node.
- uses: actions/setup-node@v4
with:
node-version: 24
- run: bun install --frozen-lockfile
- run: bun run turbo run lint typecheck test world:check
- run: bunx playwright install --with-deps chromium
working-directory: apps/datagutt
# No access to the private art repo here: this builds and plays the game with
# placeholder art, which is exactly what a fresh clone gets.
- run: bun run turbo run test:e2e
- if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: apps/*/test-results/
retention-days: 7
39 changes: 18 additions & 21 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,36 +1,33 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
node_modules/
.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build
# build and tool output
.next/
.turbo/
coverage/
*.tsbuildinfo
next-env.d.ts
# kai content output (`kai content`), written into each app
.kai/

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
.claude/
.claude/*
!.claude/settings.json
!.claude/hooks/

# test output
test-results/
playwright-report/
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
apps/datagutt/world/maps/
apps/datagutt/world/tile-ids.json
apps/datagutt/world/tile-colors.json
119 changes: 38 additions & 81 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,97 +2,54 @@

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview
## What this is

Personal portfolio site for datagutt, built with **Next.js 16** (canary), **React 19**, and **TypeScript**. Uses **pnpm** as package manager.
A Bun workspace driven by Turborepo. It holds **kai**, an engine for top-down pixel-art walking games (Phaser 4, Ink dialogue, generated maps, LimeZu art), and the games built on it:

## Commands

```bash
pnpm dev # Start dev server
pnpm build # Production build
pnpm start # Start production server
pnpm lint # ESLint (Next.js config)
pnpm format # Format with Prettier (includes Tailwind class sorting)
pnpm format:check # Check formatting
```

## Architecture

### Page Structure

Single-page portfolio (`app/page.tsx`) — a server component that fetches GitHub data, then renders sections with `Suspense` boundaries. Below-fold components using GSAP/ScrollTrigger are dynamically imported to reduce initial bundle.

### Canvas System

Five interactive canvas backgrounds rendered in the hero section via `components/canvas/CanvasSwitcher.tsx`:

- **PixelCanvas** — Conway's Game of Life with data pulses, mouse-seeded life, avatar hover burst effect
- **TerrainCanvas** — Simplex noise terrain with mouse-driven elevation, continuous drift via GSAP
- **FallingBlocksCanvas** — Tetris-style falling blocks
- **DungeonCanvas** — Procedural dungeon generation
- **StarfieldCanvas** — Parallax starfield

All canvases use `app/utils/canvas.ts` for DPI-aware setup and `hooks/useResizeKey.ts` for responsive resizing.

### Data Fetching (lib/github.ts)

Three server-side functions, all cached for 1 hour via `unstable_cache` + React `cache()`:

- `getPinnedRepos()` — Scrapes GitHub profile HTML for pinned repos
- `getGitHubStats()` — GitHub REST API for user stats + total stars
- `getContributions()` — External API for contribution calendar data
- `apps/datagutt`: datagutt.no, the portfolio site as a game, "Fjord Town", with a plain-text twin, the Journal. Next.js 16.3. See `apps/datagutt/CLAUDE.md`.
- `apps/sandbox`: the smallest kai game. It proves the engine needs nothing from Fjord Town and is the template for new games.
- `packages/kai*`: the engine, as TypeScript source with no build step.
- `tooling/`: shared TypeScript, ESLint and Vitest presets.

GitHub username is hardcoded as `datagutt`.
## Docs and session workflow

### Animation
- Engine: `docs/kai/`. Start with `ARCHITECTURE.md`; then `PLUGINS.md`, `CONTENT.md` and `NEW-GAME.md` as needed. Decisions in `docs/kai/DESIGN.md` are settled.
- Fjord Town: `apps/datagutt/docs/` (`DESIGN.md`, `PLAN.md`, `ART.md`, `README.md` for the workflow).
- Each package has a README saying what it is and what it must not import.
- The SessionStart hook prints the handoff for the branch: `docs/kai/` on `kai` branches, `apps/datagutt/docs/` otherwise. The Stop hook asks for a HANDOFF.md update when there are newer code commits.

GSAP with ScrollTrigger for scroll-based section entrances. All animation code respects `prefers-reduced-motion`. Canvas animations run at 60fps via requestAnimationFrame.

### Styling

Tailwind CSS with dark mode (class strategy). Custom green color palette (`primary-50` through `primary-950`). Custom pixel font families defined in `tailwind.config.ts`. Global styles in `app/globals.css` include glitch effects, pixel dividers, and custom scrollbar.

### Data Files

Static data lives in `data/` — `projects.ts`, `experience.ts`, `skills.ts`. Each exports typed arrays used by their respective components.

### Lanyard live status (components/LanyardCard.tsx)

Live Discord/Spotify presence card in the hero, right side. Uses `react-use-lanyard` (client WebSocket to `wss://api.lanyard.rest/socket`).

- Discord ID resolves from `NEXT_PUBLIC_DISCORD_ID` (override) or the hardcoded `DEFAULT_DISCORD_ID` constant in `lib/lanyard.ts`. The card works out of the box, no env var required.
- Requires the Discord user to be a member of the Lanyard guild (`discord.gg/lanyard`). If not joined, the card silently renders nothing.
- Card is absolutely positioned to avoid layout shift on data arrival, fades in with GSAP. Hidden on mobile (`<md`) since the absolute slot would clip the content column.

### Reactions overlay (components/reactions/, lib/reactions/, app/api/reactions/)

Canvas-agnostic ephemeral multiplayer reaction layer. Visitors drop a chunky pixel ripple on click; click-and-hold (220ms) opens a radial palette of 8-bit pixel sprites (heart, star, fire, skull, sparkle).
## Commands

- Transport: a single bidirectional WebSocket at `/api/reactions/ws` (Node runtime, Fluid Compute, `maxDuration: 300`, 30s server ping). The client sends its reaction over the same socket it receives others on. Built on `experimental_upgradeWebSocket` from `@vercel/functions` (needs the `ws` package). Because `cacheComponents` is enabled, the route handler calls `connection()` before upgrading so it opts out of static prerendering. Local dev needs `vc dev` (Vercel CLI), since `next dev` does not run the WebSocket upgrade.
- Coordinates normalize to the nearest top-level `<section id>` (hero/portfolio/about/techstack/experience/opensource/stats/contact). Receivers re-project per RAF against their own section rect, so scroll/resize tracks naturally.
- The server skips echoing an event back to the connection that sent it (the sender already renders its own drop optimistically), so there is no client side sid filtering. Clients reconnect with exponential backoff on close, and buffer a small outbox of unsent reactions while the socket is down.
- Single-instance fan-out only. A WebSocket connection is pinned to one Fluid instance, and one instance can host many connections, but two clients pinned to different instances under load will not see each other. Acceptable for ephemeral vibes; upgrade path is Upstash Redis pub/sub if needed.
- Rate limit: in-process token bucket per WebSocket connection (5 tokens, 1/sec refill). The bucket lives and dies with the connection, so no shared map or GC is needed.
- Click capture is window-level (`pointerdown` capture phase). Canvas mousemove and other handlers are not affected. Interactive targets and any element with `[data-no-reactions]` are skipped.
```bash
# From the repository root (Turborepo runs the task in every workspace)
bun install # Install dependencies (Bun 1.4; the lockfile is version 2)
bun run dev # Content and assets, then every app's dev server
bun run build # Content and assets, then the production builds
bun run lint # ESLint, including the import boundaries
bun run typecheck # tsc --noEmit
bun run test # Vitest unit tests
bun run world:check # Fail if generated maps are stale or invalid
bun run test:e2e # Build, then every app's Playwright tests
bun run format # Prettier
bunx turbo run <task> --filter=<app or package> # One workspace only
```

#### Kill switch
The `kai` command (from `@datagutt/kai-assets`) runs inside an app folder: `kai content`, `kai assets`, `kai world gen|check|render`, `kai characters`, `kai art <tool>`, `kai dev`. Each app wraps them as `bun run` scripts. See `docs/kai/ARCHITECTURE.md`.

Visitors can disable the overlay locally:
## Rules the code relies on

```js
localStorage.setItem("rx_off", "1"); // disables sending and receiving
localStorage.removeItem("rx_off"); // re-enable
```
- **Import boundaries** (`tooling/eslint-config/boundaries.js`): packages never import an app; runtime packages (`kai`, `kai-net`, `kai-live`, `kai-arcade`, `kai-next`) never import build time packages (`kai-worldgen`, `kai-limezu`, `kai-assets`) and import schemas only with `import type`; the runtime never imports Next or React; `kai-worldgen` and `kai-assets` never import the art adapter `kai-limezu`.
- **No game content in `packages/`.** Content is JSON and Markdown in `apps/<app>/content/`, checked by Zod schemas; behaviour one game needs is a plugin in that app.
- **Licensed pixels never enter this repository**, including packed atlases and renders. The art is in the private `datagutt/datagutt-assets`, used from a sibling checkout (`../datagutt-assets`) or cloned with a token. Without it builds use placeholder art.
- **Imports inside packages carry their `.ts` extension**, and JSON imports that Node reaches need `with { type: "json" }`: Node and Bun load the source directly.
- Code that the build loads (dialogue hosts, map builders) must not import `@datagutt/kai`'s index, which pulls in Phaser. Import by path, such as `@datagutt/kai/data`.
- Dependencies change through `bun add`/`bun remove` in the workspace, never by editing `package.json`.

#### Environment
## Turborepo

- `NEXT_PUBLIC_DISCORD_ID` (optional). Discord snowflake for Lanyard. Defaults to the constant in `lib/lanyard.ts`.
`content`, `assets`, `build`, `dev`, `game:dev` and `test:e2e` are never cached: the asset build reads art from outside the repository, and the content bundle depends on schemas in other packages. `typecheck`, `test` and `world:check` run `content` first.

## Key Configuration
## CI and hosting

- **React Compiler** enabled (`babel-plugin-react-compiler`)
- **View Transitions** enabled experimentally
- **Inline CSS** enabled via Next.js experimental config
- **Path alias**: `@/*` maps to project root
- **TypeScript strict mode** enabled
- `.github/workflows/ci.yml`: `bun install --frozen-lockfile`, then lint, typecheck, unit tests and `world:check`, then every app's e2e tests.
- Vercel builds `apps/datagutt` (the project's Root Directory). `apps/datagutt/vercel.ts` installs with `npx [email protected]`, because the build image's Bun 1.3 cannot read the lockfile.
Loading
Loading