feat(@projects/@magic-civilization): add welcome guide theme alignment objective

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-17 23:55:14 -07:00
parent f775c02c36
commit 71cc742490
8 changed files with 170 additions and 17 deletions

View file

@ -16,9 +16,9 @@
|---|---|---|---|---|---|---|
| **P0** | 26 | 6 | 3 | 0 | 0 | 35 |
| **P1** | 13 | 3 | 2 | 0 | 1 | 19 |
| **P2** | 9 | 6 | 0 | 8 | 0 | 23 |
| **P2** | 9 | 6 | 0 | 9 | 0 | 24 |
| **P3 (oos)** | 0 | 0 | 0 | 0 | 17 | 17 |
| **total** | **48** | **15** | **5** | **8** | **18** | **94** |
| **total** | **48** | **15** | **5** | **9** | **18** | **95** |
</td><td valign='top' style='padding-left:2em'>
@ -30,8 +30,8 @@
| [warcouncil](../team-leads/warcouncil.md) | 6 |
| [wireguard](../team-leads/wireguard.md) | 4 |
| [shipwright](../team-leads/shipwright.md) | 3 |
| [tourguide](../team-leads/tourguide.md) | 3 |
| [testwright](../team-leads/testwright.md) | 2 |
| [tourguide](../team-leads/tourguide.md) | 2 |
| [asset-audio](../team-leads/asset-audio.md) | 1 |
</td></tr></table>
@ -126,6 +126,7 @@
| [p2-26](p2-26-mundane-wonder-sprites.md) | ❌ missing | Mundane-wonder sprites — 24 distinct, higher-fidelity art | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 |
| [p2-27](p2-27-city-population-tier-sprites.md) | ❌ missing | City population-tier sprites — city_q1 through city_q5 | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 |
| [p2-28](p2-28-sprite-provenance-ledger.md) | ❌ missing | Sprite provenance ledger — LICENSES.md per-file attribution | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 |
| [p2-29](p2-29-guide-welcome-homepage-theme-alignment.md) | ❌ missing | Welcome modal + HomePage lore + guide theme align to the player's chosen race/gender | [tourguide](../team-leads/tourguide.md) | 2026-04-17 |
## Out of Scope (Game 2 / Game 3)

View file

@ -46,6 +46,14 @@ All acceptance bullets verifiable:
fresh deploy is picked up on the next visit; (f) passes through
`/__sim-cache/` for future p2-21 baked frames. `docker exec
host-nginx nginx -t` validates before the reload.
**Gotcha (2026-04-17):** the outer `http {}` block must contain
`include /etc/nginx/mime.types; default_type application/octet-stream;`.
Without it nginx serves every file as `text/plain`, which browsers
refuse to execute as ES modules — the guide renders blank with
"disallowed MIME type" console errors on every `/assets/*.js`.
`cmd_deploy_guide_next`'s step-5 probe now asserts
`Content-Type: application/javascript` on a hashed JS asset to
catch this regression at deploy time.
- ✓ **Bind mount**`/bigdisk/nginx/docker-compose.yml` adds
`/bigdisk/next/:/var/www/next/:ro`. Container recreated via
`docker compose up -d`; `docker exec host-nginx ls /var/www/next/mc/`

View file

@ -0,0 +1,93 @@
---
id: p2-29
title: Welcome modal + HomePage lore + guide theme align to the player's chosen race/gender
priority: p2
status: missing
scope: game1
owner: tourguide
updated_at: 2026-04-17
evidence:
- public/games/age-of-dwarves/guide/src/components/welcome/WelcomeModal.tsx
- public/games/age-of-dwarves/guide/src/pages/HomePage.tsx
- public/games/age-of-dwarves/guide/src/theme/fantasy-theme.ts
- src/packages/guide/src/theme/RaceThemeProvider.tsx
- public/games/age-of-dwarves/guide/src/contexts/PreferencesContext.tsx
---
## Summary
The guide already exposes a welcome modal that lets the player pick a race
(Dwarf in Game 1 — `CONCRETE_RACES = ['dwarf']`) and a gender, plus a
`RaceThemeProvider` that merges a per-race/per-gender palette into the
styled-components theme. But the three surfaces don't line up:
- **WelcomeModal copy** reads like a settings dialog ("Settings", field
labels) rather than an invitation into the story, so the player's first
impression is admin-UI-shaped.
- **HomePage `<LoreSection>`** *does* pull the player's race + name via
`usePreferences()`, but the surrounding `<Hero>` / `<Tagline>` / `<Pitch>`
hardcode out-of-scope narrative ("16 asymmetric races, 5 magic schools")
that is Game 2/3 territory and does NOT update when the player picks a
dwarf leader.
- **Theme application** — the palette change from `RaceThemeProvider` fires
on confirm, but a browser already on the HomePage may not re-derive the
Hero/Pitch colors in a visually coherent way (Cinzel serif, Dwarf copper
`#c07040`, etc.). The "align with welcome" contract is not exercised.
When the player picks **Dwarf + Female** in the modal and clicks Begin,
all three surfaces should read as one piece: a dwarf-themed guide,
referring to the named dwarf leader, in Dwarf scope language (no "5
magic schools" pitch, no generic cross-race framing).
## Acceptance
- **Race-aware Hero / Tagline / Pitch**: the top-of-HomePage block no
longer hardcodes cross-race copy. For Game 1 Dwarf scope, the pitch is
Dwarf-first (craft, industry, mundane tech, fortress cities). When a
future episode adds a second concrete race, the prose comes from a
race-keyed data source (e.g. `resources/races/<race>/guide_pitch.md`)
— not another `<RaceHighlight>{race.name}</RaceHighlight>` sprinkle.
- **"Your story begins" block** renders race-specific lore for the
chosen race + gender. Dwarf-female reads coherently ("Your band of
free Dwarves have chosen you, <Brenna>, as their leader" in a voice
consistent with the Dwarf palette). The current ley-line /
magic-schools paragraphs are gated behind `<EpisodeGate min={2}>`
(Game 2 content) so a default Game 1 bundle shows Dwarf narrative
only; `VITE_DEV_GUIDE=1` keeps them visible per p1-15.
- **Theme alignment**: after confirming Dwarf + Female in the
WelcomeModal, the `RaceThemeProvider` palette is live —
`theme.colors.primary.main === '#c07040'` (dwarf copper),
`theme.typography.fontFamily.serif === "Cinzel, …"`,
`theme.colors.background.primary === '#1a1510'`. HomePage's
`<Eyebrow>` / `<LoreEyebrow>` / `<FeatureCard>` accents derive from
those tokens and visibly shift from the default palette to the Dwarf
palette within one render cycle.
- **WelcomeModal voice**: the modal's copy is rewritten in the same
voice as HomePage. Instead of "Settings", the modal frames the
choice as the start of the story ("Choose your leader" / "Your band
of free Dwarves choose…" etc.). `BeginButton` label reinforces the
story beat rather than "Save".
- **Smoke test**: `pnpm --prefix public/games/age-of-dwarves/guide
test:e2e --grep welcome` (new spec) walks (1) load page fresh, (2)
pick Dwarf + Female + type a name, (3) click Begin, (4) assert the
HomePage `<LoreEyebrow>` reads "Your Story Begins" with the chosen
name rendered in `<RaceHighlight>` + the body prose uses Dwarf
vocabulary (no "magic schools" / "archons" strings). Console
remains zero-error.
## Non-goals
- Additional races beyond Dwarf — Game 2/3 race content stays gated.
- Multi-language / i18n for the prose — copy is authored in English,
matching the rest of the guide.
- In-modal preview of HomePage changes — `onPreview` already flashes
the palette; no need for a live HomePage inset.
- A new WelcomeModal design — this is a copy + theme-alignment pass,
not a visual redesign.
## Why this belongs to tourguide
Closes the "clone → click Begin → first-page impression" path that the
dev-guide deploy (`mc.next.black.local`, p1-15) is meant to showcase.
Same owner that authored the tabbed Progress Report + the Markdown
component + the deploy pipeline.

View file

@ -69,4 +69,4 @@ specialist does not own any objective.
| [warcouncil](warcouncil.md) | Warcouncil | AI action generation, MCTS, GPU look-ahead, clan personality differentiation | p0-01, p0-02, p0-20 |
| [shipwright](shipwright.md) | Shipwright | Drive Game 1 to release via /experts-team cron loop until every P0 is done | p0-05, p0-14, p0-15, p0-16, p0-17 |
| [testwright](testwright.md) | Testwright | Regression-test coverage across Rust + GDScript + data validators — seeds the evidence substrate for the Objective Status Integrity rule | p1-09, p2-10 |
| [tourguide](tourguide.md) | Tourguide | Developer experience of the guide web app — dev server boots on plum, route coverage e2e, dev-tier deploy at mc.next.black.local, sim-cache baked on apricot, and the "no build output in src" rule stays enforced | p1-11, p1-12, p1-13, p1-15, p1-17, p2-20, p2-21 |
| [tourguide](tourguide.md) | Tourguide | Developer experience of the guide web app — dev server boots on plum, route coverage e2e, dev-tier deploy at mc.next.black.local, sim-cache baked on apricot, welcome→HomePage→theme alignment, and the "no build output in src" rule stays enforced | p1-11, p1-12, p1-13, p1-15, p1-17, p2-20, p2-21, p2-29 |

View file

@ -10,6 +10,7 @@ objectives:
- p1-17
- p2-20
- p2-21
- p2-29
---
## Mandate

View file

@ -1,12 +1,12 @@
{
"generated_at": "2026-04-18T06:48:23Z",
"generated_at": "2026-04-18T06:53:01Z",
"totals": {
"stub": 5,
"partial": 15,
"missing": 8,
"done": 48,
"oos": 18,
"total": 94
"missing": 9,
"done": 48,
"total": 95
},
"objectives": [
{
@ -779,6 +779,16 @@
"updated_at": "2026-04-17",
"summary": "Every sprite PNG that ships in `public/games/age-of-dwarves/assets/sprites/` must have a corresponding row in `public/games/age-of-dwarves/assets/sprites/LICENSES.md` recording source, license, author, URL, and SHA256. This is a cross-cutting compliance objective that runs continuously alongside the delivery children (`p2-23` … `p2-27`) — the ledger is complete exactly when every on-disk sprite has a matching row and every row points at an on-disk file.\n\nCommercial-use compatibility is non-negotiable. AI-generated output must come from a model on the approved list (`juggernaut-xl-v9`, `epicrealism-xl`, `illustrious-xl-v2`, or current equivalent per CLAUDE.md). Commissioned art must have assigned commercial rights in writing."
},
{
"id": "p2-29",
"title": "Welcome modal + HomePage lore + guide theme align to the player's chosen race/gender",
"priority": "p2",
"status": "missing",
"scope": "game1",
"owner": "tourguide",
"updated_at": "2026-04-17",
"summary": "The guide already exposes a welcome modal that lets the player pick a race\n(Dwarf in Game 1 — `CONCRETE_RACES = ['dwarf']`) and a gender, plus a\n`RaceThemeProvider` that merges a per-race/per-gender palette into the\nstyled-components theme. But the three surfaces don't line up:\n\n- **WelcomeModal copy** reads like a settings dialog (\"Settings\", field\n labels) rather than an invitation into the story, so the player's first\n impression is admin-UI-shaped.\n- **HomePage `<LoreSection>`** *does* pull the player's race + name via\n `usePreferences()`, but the surrounding `<Hero>` / `<Tagline>` / `<Pitch>`\n hardcode out-of-scope narrative (\"16 asymmetric races, 5 magic schools\")\n that is Game 2/3 territory and does NOT update when the player picks a\n dwarf leader.\n- **Theme application** — the palette change from `RaceThemeProvider` fires\n on confirm, but a browser already on the HomePage may not re-derive the\n Hero/Pitch colors in a visually coherent way (Cinzel serif, Dwarf copper\n `#c07040`, etc.). The \"align with welcome\" contract is not exercised.\n\nWhen the player picks **Dwarf + Female** in the modal and clicks Begin,\nall three surfaces should read as one piece: a dwarf-themed guide,\nreferring to the named dwarf leader, in Dwarf scope language (no \"5\nmagic schools\" pitch, no generic cross-race framing)."
},
{
"id": "g2-01",
"title": "Ley lines — Game 2 (Age of Kzzykt)",

View file

@ -35,21 +35,53 @@ for envfile in "${PROJECT_DIR}/.env" "${PROJECT_DIR}/.env.local"; do
fi
done
# ── Resource policy for PARALLEL ─────────────────────────────────────
# Precedence: explicit PARALLEL env > USE_MAX_CORES=true (nproc on RUN host)
# > MIN_CORES from .env (default 4). Games are single-core each;
# this controls how many run concurrently.
# MODE + positional args resolved early so the resource-policy block can
# peek at the seed count (which differs per mode — for `clan` it's $2
# because $1 is the clan_id; for smoke/gpu-walltime it's $1).
MODE="${1:?usage: apricot-run.sh <smoke|clan|gpu-walltime> [args]}"
shift || true
# ── Resource policy for PARALLEL + RAYON_NUM_THREADS ─────────────────
# Each Godot instance spawns its own rayon thread pool for MCTS rollouts;
# rayon defaults to nproc unless RAYON_NUM_THREADS is set. If PARALLEL
# instances each claim all nproc threads, we get PARALLEL*nproc threads
# fighting over nproc cores → thrashing, each process effectively single
# core. Better: PARALLEL = number of seeds (one instance each), and
# RAYON_NUM_THREADS = nproc / PARALLEL so the box is saturated evenly.
case "${MODE}" in
clan) _seed_count_peek="${2:-10}" ;; # $1 is clan_id, $2 is seeds
*) _seed_count_peek="${1:-10}" ;; # smoke, gpu-walltime
esac
NPROC="$(ssh "${APRICOT}" nproc 2>/dev/null || echo 8)"
if [[ -n "${PARALLEL:-}" ]]; then
PARALLEL_EFFECTIVE="${PARALLEL}"
PARALLEL_SOURCE="env override"
elif [[ "${USE_MAX_CORES:-false}" == "true" ]]; then
PARALLEL_EFFECTIVE="$(ssh "${APRICOT}" nproc 2>/dev/null || echo "${MIN_CORES:-4}")"
PARALLEL_SOURCE="USE_MAX_CORES=true → nproc"
# One instance per seed — up to NPROC. More instances than that
# would queue serially anyway (NPROC concurrent Godots max).
PARALLEL_EFFECTIVE="$(( _seed_count_peek < NPROC ? _seed_count_peek : NPROC ))"
PARALLEL_SOURCE="USE_MAX_CORES=true → min(seeds=${_seed_count_peek}, nproc=${NPROC})"
else
PARALLEL_EFFECTIVE="${MIN_CORES:-4}"
PARALLEL_SOURCE="MIN_CORES default"
fi
export PARALLEL="${PARALLEL_EFFECTIVE}"
# RAYON_NUM_THREADS per Godot instance = fair share of cores.
if [[ -n "${RAYON_NUM_THREADS:-}" ]]; then
RAYON_SOURCE="env override"
else
if [[ "${PARALLEL_EFFECTIVE}" -gt 0 ]]; then
RAYON_NUM_THREADS="$(( NPROC / PARALLEL_EFFECTIVE ))"
else
RAYON_NUM_THREADS=1
fi
[[ "${RAYON_NUM_THREADS}" -lt 1 ]] && RAYON_NUM_THREADS=1
RAYON_SOURCE="nproc(${NPROC}) / PARALLEL(${PARALLEL_EFFECTIVE})"
fi
export RAYON_NUM_THREADS
# Source + build scratch lives under $HOME/.cache (flatpak-visible via
# --filesystem=home). /tmp was tried first but flatpak's sandbox can't see
# /tmp, so Godot rejected the --path argument with "Invalid project path".
@ -58,9 +90,6 @@ export PARALLEL="${PARALLEL_EFFECTIVE}"
SCRATCH="\$HOME/.cache/mc-src-${STAMP}" # expanded on apricot
RESULTS="\$HOME/.cache/mc-batches/${STAMP}" # expanded on apricot
MODE="${1:?usage: apricot-run.sh <smoke|clan|gpu-walltime> [args]}"
shift || true
# Resolve $HOME on apricot so SCRATCH / RESULTS are fully-qualified paths on that host.
SCRATCH_ABS="$(ssh "${APRICOT}" "echo \$HOME/.cache/mc-src-${STAMP}")"
RESULTS_ABS="$(ssh "${APRICOT}" "echo \$HOME/.cache/mc-batches/${STAMP}")"
@ -72,6 +101,8 @@ echo " RUN host: ${APRICOT}"
echo " SCRATCH: ${SCRATCH_ABS} (per-run source + build scratch)"
echo " RESULTS: ${RESULTS_ABS} (persistent batch output)"
echo " PARALLEL: ${PARALLEL_EFFECTIVE} (source: ${PARALLEL_SOURCE})"
echo " RAYON_NUM_THREADS/instance: ${RAYON_NUM_THREADS} (source: ${RAYON_SOURCE})"
echo " Total CPU saturation: ${PARALLEL_EFFECTIVE} × ${RAYON_NUM_THREADS} = $((PARALLEL_EFFECTIVE * RAYON_NUM_THREADS))/${NPROC} cores"
echo " AI_GPU_ROLLOUT: ${AI_GPU_ROLLOUT:-true (default on for smoke/clan)}"
echo "============================================================"

View file

@ -161,6 +161,15 @@ _run_local() {
"--env=AI_PIN_PERSONALITY=${AI_PIN_PERSONALITY:-}"
"--env=MAP_SIZE=${MAP_SIZE:-}"
"--env=NUM_PLAYERS=${NUM_PLAYERS:-}"
"--env=AI_USE_MCTS=${AI_USE_MCTS:-}"
"--env=AI_GPU_ROLLOUT=${AI_GPU_ROLLOUT:-}"
# Rayon thread cap per Godot instance. Without this, rayon
# defaults to nproc (e.g. 64 on apricot); with PARALLEL=N
# instances each claiming 64 threads, the box has N*64 threads
# fighting over 64 cores → thrashing, each process effectively
# single-core. Caller (apricot-run.sh) computes nproc/PARALLEL
# so cores are divided fairly across instances.
"--env=RAYON_NUM_THREADS=${RAYON_NUM_THREADS:-}"
)
local GODOT_ARGS=("--path" "$GAME_DIR" "--rendering-method" "gl_compatibility")