feat: publish complete preproduction prototype

This commit is contained in:
2026-08-10 21:00:58 +08:00
parent 28e857a4c0
commit f8c665055f
38 changed files with 7049 additions and 12 deletions
+1
View File
@@ -3,4 +3,5 @@ tmp/
build/
dist/
node_modules/
test-results/
*.log
+33 -3
View File
@@ -23,7 +23,37 @@ The game may learn structural lessons from authorized private reference
research, but its setting, characters, enemies, names, art, audio, UI,
progression, formulas, and expression must be original.
## Design Package
## Review The Design
The design package will be developed under `docs/` and published as a reviewable
HTML gallery before any formal implementation starts.
The single owner-facing review artifact is the interactive HTML prototype in
[`prototype/site/`](prototype/site/). It shows one subject at a time and links
the complete design package behind these review paths:
1. **Core flow:** walk through the 17-step title-to-restoration path.
2. **Eight chapters:** inspect route, battle, and restored-world direction for
every chapter.
3. **All screens:** inspect all 82 player-facing pages and their key states.
4. **Content:** search all 30 characters, 36 professions, 100 enemies, 320
items, 48 achievements, and 20 collections.
5. **Production and readiness:** inspect the art/audio scope and the gates that
remain open before runtime development.
Run it locally from the repository root:
```bash
python3 -m http.server 8766 --directory prototype/site
```
Then open `http://127.0.0.1:8766`. The published Artifact Gallery link is a
disposable presentation copy; this repository remains the design authority.
## Evidence Boundary
The HTML is an interactive **design prototype**, not a playable build. Its four
AI-generated boards are concept direction only. The UI chrome and readable text
are authored in HTML; no concept board is a final background, sprite, item icon,
animation, integrated asset, runtime screenshot, or human-approved result.
See [`docs/README.md`](docs/README.md) for the complete authority map and
[`docs/reviews/PRE_DEVELOPMENT_READINESS_AUDIT.md`](docs/reviews/PRE_DEVELOPMENT_READINESS_AUDIT.md)
for the exact completed and open gates.
+50 -9
View File
@@ -1,13 +1,54 @@
# Design Authority Map
Design documents are split by ownership so parallel agents do not edit the same
files.
Use the interactive site in [`../prototype/site/`](../prototype/site/) as the
owner-facing entry point. The Markdown files below are the exact implementation
authority after the direction is approved.
- `reference/`: evidence-labeled reference census and transferable lessons
- `product/`: player contract, systems, economy, balance, save/failure
- `content/`: characters, professions, enemies, equipment, chapters, collection
- `presentation/`: screen map, UI, art, animation, assets, audio
- `prototype/`: HTML gallery and interactive design-prototype specifications
- `reviews/`: independent consistency and readiness reviews
## Product And Rules
Status and task ownership live in `.agent-taskgraph/`.
- [`product/GAME_PRODUCT_CONTRACT.md`](product/GAME_PRODUCT_CONTRACT.md):
audience, fantasy, play loop, duration, modes, onboarding, completion
- [`product/SYSTEMS_AND_BATTLE.md`](product/SYSTEMS_AND_BATTLE.md): deterministic
battle, six-person formation, Directives, damage, professions and failure
- [`product/ECONOMY_AND_BALANCE.md`](product/ECONOMY_AND_BALANCE.md): progression,
prices, pacing profiles, difficulty and solvency
- [`product/SAVE_AND_FAILURE_CONTRACT.md`](product/SAVE_AND_FAILURE_CONTRACT.md):
atomic save, migration, corruption, rollback, offline and retry behavior
## Complete Content
- [`content/CHARACTER_BIBLE.md`](content/CHARACTER_BIBLE.md): 30 named characters
- [`content/PROFESSION_AND_SKILL_MATRIX.md`](content/PROFESSION_AND_SKILL_MATRIX.md):
12 base and 24 advanced professions
- [`content/ENEMY_BESTIARY.md`](content/ENEMY_BESTIARY.md): 100 enemy identities
- [`content/EQUIPMENT_CATALOG.md`](content/EQUIPMENT_CATALOG.md): 320 items
- [`content/CAMPAIGN_AND_CHAPTERS.md`](content/CAMPAIGN_AND_CHAPTERS.md): eight
chapters, ending and postgame
- [`content/ACHIEVEMENTS_AND_COLLECTIONS.md`](content/ACHIEVEMENTS_AND_COLLECTIONS.md):
48 achievements and 20 collections
## Presentation And Production
- [`presentation/SCREEN_AND_STATE_MAP.md`](presentation/SCREEN_AND_STATE_MAP.md):
82 player-facing pages and required states
- [`presentation/UI_AND_VISUAL_SYSTEM.md`](presentation/UI_AND_VISUAL_SYSTEM.md):
landscape layout, interaction, accessibility and localization
- [`presentation/ART_ASSET_AND_ANIMATION_CATALOG.md`](presentation/ART_ASSET_AND_ANIMATION_CATALOG.md):
asset identities, rigs, animation and VFX production
- [`presentation/AUDIO_MUSIC_AND_VOICE_CATALOG.md`](presentation/AUDIO_MUSIC_AND_VOICE_CATALOG.md):
music, ambience, dialogue and SFX identities
## Prototype, Research And Review
- [`prototype/INTERACTIVE_PROTOTYPE_SPEC.md`](prototype/INTERACTIVE_PROTOTYPE_SPEC.md):
HTML behavior, responsive and evidence contract
- [`prototype/generation/README.md`](prototype/generation/README.md): four concept
image calls and their provenance
- [`reference/REFERENCE_RUNTIME_CENSUS.md`](reference/REFERENCE_RUNTIME_CENSUS.md)
and [`reference/TRANSFERABLE_STRUCTURE_LESSONS.md`](reference/TRANSFERABLE_STRUCTURE_LESSONS.md):
evidence-labeled reference research and originality boundary
- [`reviews/PRE_DEVELOPMENT_READINESS_AUDIT.md`](reviews/PRE_DEVELOPMENT_READINESS_AUDIT.md):
verified completion and open human/runtime gates
Historical Agent TaskGraph records remain under `.agent-taskgraph/`; they are
not the current product or prototype authority.
@@ -0,0 +1,133 @@
# Aetherbound Guild: Interactive Design Prototype
> Scope: reviewable pre-production HTML. It visualizes the approved product,
> content, screen, art, and audio specifications without implementing game
> simulation, save behavior, packaging, or final runtime assets.
## 1. Review Decisions
The prototype must let the owner decide whether:
1. the landscape 2D hand-drawn direction fits the intended game;
2. the player can recognize Guild, company, route, battle, diagnosis, reward,
restoration, records, and settings without a card-wall interface;
3. the 82-screen inventory and eight chapters appear complete;
4. formation, telegraph, Directive, Cache, Anchor, Strain, profession, and item
concepts are sufficiently visible to proceed to a dynamic vertical slice;
5. any page family or chapter direction should be redesigned before runtime
work begins.
Owner approval of the HTML is design approval only. It does not approve engine
implementation, paid bulk art/audio, packaging, publishing, or release.
## 2. Information Architecture
The gallery uses a persistent left navigation on desktop and a compact drawer
on narrow screens. The right side presents one review subject at a time with
ample margin and no tiled wall of tiny screenshots.
```text
Overview
|- Product thesis and complete loop
|- Quantitative scope
`- Production/evidence boundary
Game Screens
|- System / Guild / Workshop / Records
|- World / Expedition / Battle / Results / Settings
`- 82 individually selectable screen specifications
Chapters
|- CH-01..CH-08
`- route, battlefield, and restoration states
Content
|- 30 Characters
|- 36 Professions
|- 100 Enemies
|- 320 Items
`- 48 Achievements / 20 Collections
Production
|- Art and animation
|- Music, ambience, voice and SFX
|- CH-01 dynamic slice
`- Development-readiness gates
```
## 3. Game Screen Presentation
Every screen entry shows:
- one landscape `16:9` game-stage mockup at a stable aspect ratio;
- page ID/name and the three-second focal order;
- the main action, required variants, and safe exit;
- status chips declaring `design spec`, `concept art`, and open evidence gates;
- previous/next page actions plus direct selection from the left list;
- a full-size viewing mode without cropping the game canvas.
The mockup renderer uses the canonical shell for the selected prefix and adds
page-specific state. Core flow pages receive more detailed compositions. The
prototype must not imply that all 82 pages have final bespoke raster art.
## 4. Compressed Click Path
The clickable experience uses these states:
```text
Title SYS-004
-> Guild GLD-001
-> Formation GLD-005
-> World WLD-001
-> Route EXP-002
-> Encounter Preview EXP-004
-> Battle BAT-002
-> Directive BAT-005 / target BAT-006 / review BAT-007
-> Victory RES-001 / Diagnosis RES-002
-> Reward EXP-008 / Cache EXP-009
-> Anchor EXP-010
-> Restoration RES-005
-> Guild transformed GLD-001
```
This proves navigation and information hierarchy only. It does not simulate
combat formulas, persistence, pacing, audio, touch feel, or fun.
## 5. Visual Assets In This Prototype
Four paid image calls are the fixed task ceiling and have been used:
- CH-01 battle concept;
- CH-01..04 environment grid;
- CH-05..08 environment grid;
- character/profession/enemy/item element grid.
All outputs are `concept` evidence. The gallery may crop/slice them for review,
but cannot call them final backgrounds, sprites, icons, integrated assets, or
runtime screenshots. UI text and controls are authored in HTML so they remain
legible and do not depend on generated text.
## 6. Responsive Contract
- Desktop: 280 px navigation, centered content, game canvas max 1440 px.
- Tablet: 230 px navigation, canvas remains fully visible.
- Phone portrait: navigation becomes a drawer; the canvas fits width and opens
in a full-screen viewer for close inspection.
- Phone landscape: content uses the full safe width and hides nonessential
gallery chrome while the viewer is active.
- No design image uses `cover` where cropping would hide the intended screen.
- Long specifications wrap below the canvas rather than overlap it.
## 7. Prototype Acceptance
- [x] All 82 screen IDs appear exactly once in the screen selector.
- [x] All eight chapters and 24 chapter screen families are represented.
- [x] Content indexes show 30/36/100/320/48/20 exact counts.
- [x] Core click path reaches restoration without a dead control.
- [x] Desktop, tablet, phone portrait, and phone landscape do not overlap or
crop functional content.
- [x] Generated concepts remain labeled honestly and carry task provenance.
- [x] Local links, image paths, JSON, browser verification, Gallery publication,
and Gallery health/share checks pass.
- [x] Owner visual, comprehension, fun, audio, device, and release gates remain
explicitly open.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

@@ -0,0 +1,13 @@
{
"mode": "execute",
"model": "gpt-image-2",
"size": "16:9",
"resolution": "2k",
"output": "docs/prototype/generation/01_ch01_battle.png",
"local_references": [],
"remote_reference_count": 0,
"prompt_chars": 2900,
"task_id": "task_01KZNSKPCKHJQ8032RP05JM13Z",
"status": "completed",
"bytes": 4164158
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

@@ -0,0 +1,13 @@
{
"mode": "execute",
"model": "gpt-image-2",
"size": "16:9",
"resolution": "2k",
"output": "docs/prototype/generation/02_chapters_01_04.png",
"local_references": [],
"remote_reference_count": 0,
"prompt_chars": 2287,
"task_id": "task_01KZNSKPCQTT9BXB4W6X2962X8",
"status": "completed",
"bytes": 4962407
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

@@ -0,0 +1,13 @@
{
"mode": "execute",
"model": "gpt-image-2",
"size": "16:9",
"resolution": "2k",
"output": "docs/prototype/generation/03_chapters_05_08.png",
"local_references": [],
"remote_reference_count": 0,
"prompt_chars": 2557,
"task_id": "task_01KZNSKPDC0GZ2VFSZ9D0JW5BH",
"status": "completed",
"bytes": 4552176
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 MiB

@@ -0,0 +1,13 @@
{
"mode": "execute",
"model": "gpt-image-2",
"size": "1:1",
"resolution": "2k",
"output": "docs/prototype/generation/04_elements_board.png",
"local_references": [],
"remote_reference_count": 0,
"prompt_chars": 2706,
"task_id": "task_01KZNSKPCD3NWJND0H69GQGXQS",
"status": "completed",
"bytes": 6489015
}
+23
View File
@@ -0,0 +1,23 @@
# Concept Image Provenance
These four calls are the complete paid-generation set for the current design
prototype. The owner standing authorization allowed up to four calls and USD 5
equivalent for this task. Exactly four calls were made and no retry occurred.
| Purpose | Provider task ID | Prompt | Source output |
|---|---|---|---|
| CH-01 battle direction | `task_01KZNSKPCKHJQ8032RP05JM13Z` | [`prompts/01_ch01_battle.txt`](prompts/01_ch01_battle.txt) | [`01_ch01_battle.png`](01_ch01_battle.png) |
| CH-01 through CH-04 environments | `task_01KZNSKPCQTT9BXB4W6X2962X8` | [`prompts/02_chapters_01_04.txt`](prompts/02_chapters_01_04.txt) | [`02_chapters_01_04.png`](02_chapters_01_04.png) |
| CH-05 through CH-08 environments | `task_01KZNSKPDC0GZ2VFSZ9D0JW5BH` | [`prompts/03_chapters_05_08.txt`](prompts/03_chapters_05_08.txt) | [`03_chapters_05_08.png`](03_chapters_05_08.png) |
| Heroes, professions, enemies and items | `task_01KZNSKPCD3NWJND0H69GQGXQS` | [`prompts/04_elements_board.txt`](prompts/04_elements_board.txt) | [`04_elements_board.png`](04_elements_board.png) |
Each adjacent `*.report.json` stores the provider response facts without a
credential. The optimized slices in `prototype/site/assets/web/` are derived
presentation copies.
## Evidence Boundary
Every output is `concept` evidence only. It is not final art, a sprite sheet,
an animation, an integrated asset, a runtime screenshot, or a human-approved
result. Generated lettering is never UI authority; all readable prototype text
and controls are authored in HTML.
@@ -0,0 +1,9 @@
Create one original 16:9 landscape gameplay illustration for a premium 2D Japanese hand-drawn tactical auto-battle RPG named Aetherbound Guild. This is clean production concept art for an actual game playfield, not marketing key art.
Scene: CH-01 windmill relay battlefield in bright daylight. A working magical repair guild fights on three clearly separated shallow perspective wind lanes across grassy mill terraces. On the left/lower half, three named adult adventurers at readable full-body gameplay scale: Mara, a weathered square-faced woman in her thirties with dark curls shaved at one temple, repaired yellow field coat and compact enamel shield, bracing in the front lane; Oren, a silver-flecked bearded relay mechanic with copper goggles and a coil vest, deploying a portable brass Waylamp in the center lane; Ilyo, a compact brown-skinned clinician with mint scarf, slate earrings and a visible hand brace, protected in the rear lane. They are practical workers, not glamorous heroes, not chibi, not photorealistic.
On the right/upper half, an original Bell-Feeder boss: a large ceramic-and-brass windmill predator with hollow rib-shaped bells, visible clockwork feeding jaws, three stable logical target anchors, and a readable winding-up attack pose. Two small plated scavenger constructs support it. A dark silent wavefront cuts through the bright wind, while a clean cyan double-line aether route and one repair-gold retuning seam cross the center lane. Show an imminent telegraph through in-world coral forecast geometry projected from the boss onto the center lane, but NO UI panels and NO text.
Composition: fixed 16:9 game camera, shallow three-quarter side view, horizon around 42 percent. Leave clean negative space at the top 9 percent for a compact HUD, bottom 12 percent for two tactical controls, and far right 10 percent for a cache strap. The battlefield and characters still fill the frame; no giant empty region. First visual read is the boss telegraph, second Mara protecting Ilyo, third Oren's Waylamp about to retune the route. Distinct silhouettes, crisp readable edges, restrained cel shading over painterly environment, selective ink texture, coherent daylight, no fog hiding actors. Layer-friendly composition with clear background, midground, playfield, actor, prop, foreground separation.
Art direction: original clean 2D Japanese hand-drawn fantasy, mature proportions, premium mobile/PC strategy RPG, clear hard-edged shapes and limited purposeful effects. Palette uses grass green, sky cyan, windmill yellow, chalk white, charcoal rupture, cyan aether, repair gold, danger coral. No copied characters, no commercial game layout, no dwarves, no pixel art, no 3D render, no western mobile-game gloss, no gacha portrait cards, no dark dashboard, no card wall, no gradients/orbs/bokeh, no text, no logos, no watermark, no border, no split-screen, no collage.
@@ -0,0 +1,11 @@
Create ONE REGULAR 2x2 GRID with FOUR EQUAL 16:9-like landscape scene cells separated by clean thin neutral gutters. Each cell is an original premium 2D Japanese hand-drawn tactical RPG environment concept for Aetherbound Guild. Subjects MUST NOT cross cell boundaries. No UI, no labels, no text, no logos, no watermark. Consistent mature hand-drawn style and shallow three-quarter gameplay camera, but every region must be unmistakably different. Each cell must show a useful game environment with background, midground, three-band playfield or route surface, separate structural props, and one visible magical repair process. No actors required; small distant worker silhouettes are allowed only for scale.
TOP-LEFT, CH-01 First Light Broken Bell: bright windmill basin, green terraces, farm pumps, yellow warning tower, a silent charcoal wavefront cutting the air, cyan double-line route reaching a ceramic bell relay, repair-gold seam. Daylight, hopeful but pressured.
TOP-RIGHT, CH-02 Voices in Glass: flooded jade reed channels, red ferry platforms, translucent voiceglass reeds carrying white ribbon-like recorded voices with distinct patterned source marks, rising water bands, a civic recital mechanism in the distance. Avoid monochrome blue and avoid ghost horror.
BOTTOM-LEFT, CH-03 The Heat We Owe: iron-gray furnace district split between a cobalt worker heat line and silver mint conduits, vermilion pressure gates, vertical smelter lift, alternating amber and cyan pressure seams, readable safe work platforms. Avoid all-orange forge monotony.
BOTTOM-RIGHT, CH-04 Leaves That Disagree: living archive canopy with two visibly incompatible charter branches, paper-white bark, black ink annotations, green leaves, magenta marginal notes, deliberate redacted gaps and cyan restoration strokes, connected court platforms. Not a generic enchanted forest.
Overall art direction: clean original 2D Japanese hand-drawn fantasy, painterly environments with crisp game-readable silhouettes, selective ink texture, strong material identity, mature premium strategy RPG, no copied commercial imagery, no pixel art, no photorealism, no 3D render, no generic dark dashboard, no cards, no oversized characters, no gradients, no bokeh, no atmospheric fog that hides gameplay surfaces.
@@ -0,0 +1,11 @@
Create ONE REGULAR 2x2 GRID with FOUR EQUAL 16:9-like landscape scene cells separated by clean thin neutral gutters. Each cell is an original premium 2D Japanese hand-drawn tactical RPG environment concept for Aetherbound Guild. Subjects MUST NOT cross cell boundaries. No UI, no labels, no text, no logos, no watermark. Consistent mature hand-drawn style and shallow three-quarter gameplay camera, but every region must be unmistakably different. Each cell must show a useful game environment with background, midground, three-band playfield or route surface, separate structural props, and one visible magical repair process. No actors required; small distant worker silhouettes are allowed only for scale.
TOP-LEFT, CH-05 Two Tides One Name: moon-tide harbor with teal water and silver tide light, rescue-orange beacon pier and indigo customs pier, real and mirrored manifests traveling as offset physical glass strips, reef lanes, asymmetrical target markers, true-source rim patterns. Avoid generic blue seascape.
TOP-RIGHT, CH-06 A Crown Made of Weather: four distinct floating civic wards orbit a clear calm eye, saffron lift machinery, jade bridges, storm-gray clouds, readable coral and cyan directional gust arrows, selective anchor mechanisms, layered parallax rotation without disorienting tilt. Avoid dark thunder wallpaper.
BOTTOM-LEFT, CH-07 The Market Beneath Names: descending circular market beneath the city, wet green stone, warm coral lanterns, ivory registry tables with intentionally blank fields placed beside living self-identified workers, memory-map cords, intimate human scale, a comforting false-safe white shrine in the distance. Never render erased people as featureless silhouettes; avoid purple horror cliche.
BOTTOM-RIGHT, CH-08 A Concord of Many: seven material corridors from previous regions converge on the First Junction, an open civic repair mechanism woven from grass, reed glass, iron, archive leaf, harbor rope, storm vane and registry cord, eight distinct color/material cadences held by dark ink scaffolding, a working federation not a throne room. Structured plurality, not rainbow glow.
Overall art direction: clean original 2D Japanese hand-drawn fantasy, painterly environments with crisp game-readable silhouettes, selective ink texture, strong material identity, mature premium strategy RPG, no copied commercial imagery, no pixel art, no photorealism, no 3D render, no generic dark dashboard, no cards, no oversized characters, no gradients, no bokeh, no atmospheric fog that hides gameplay surfaces.
@@ -0,0 +1,11 @@
Create ONE REGULAR 2x2 GRID with FOUR EQUAL CELLS and clear neutral gutters. This is an original visual-development element board for Aetherbound Guild, a premium 2D Japanese hand-drawn tactical guild RPG. Keep every subject centered with generous padding. Subjects MUST NOT cross cell boundaries. Use a consistent clean hand-drawn style, mature proportions, crisp readable silhouettes, selective ink texture, neutral light paper-gray background suitable for later reference. No labels, no text, no logos, no watermark, no UI.
TOP-LEFT, three canonical named adventurer full-body identity designs: Mara, weathered square-faced adult woman with dark curls shaved at left temple, repaired yellow field coat, compact enamel shield and bell-key badge; Oren, middle-aged long-faced relay mechanic with silver-flecked beard braid, copper goggles above eyes, patched coil vest and portable brass Waylamp; Ilyo, compact adult clinician with freckled brown skin, mint scarf, slate earrings, visible hand brace and diagnostic slate. Practical working poses and distinct body shapes, no glamour pose, not chibi.
TOP-RIGHT, six modular profession visual kits shown as equipment/pose silhouettes rather than new characters: Bellguard brace shield with ceramic bell; Wayblade paired short blades and split coat tail; Mender diagnostic slate and mint remedy device; Pathfinder sighting rod and folded map; Relay Smith folding bridge antenna and magnetic tool frame; Concord Mediator open circular staff with unmatched colored segments. Clear sockets and resource devices.
BOTTOM-LEFT, six original CH-01 enemy and mechanic elements: Bell-Feeder ceramic-and-brass windmill predator boss with hollow rib bells; plated scavenger; gust bailiff with stamp-like vane; relay mite; dark silent wavefront hazard; coral lane telegraph geometry and breakable stability contour. Monsters are constructed from regional civic machinery, distinctive and readable, not cute mascots.
BOTTOM-RIGHT, twelve independent equipment and expedition objects arranged in a clean 4x3 mini-grid inside the cell: repaired yellow guild coat, compact enamel shield, portable Waylamp, ceramic rib bell relic, red-white signal shutter, brass ridge scope, mint remedy tin, two-metal cross-payment token, folded memory map with cord, open copper Cache clasp, closed green Anchor clasp, multicolor reconciliation lamp. Each object complete, no crop, normalized perceived size, generous space.
No copied commercial designs, no dwarves, no fantasy tavern tropes, no pixel art, no photorealism, no 3D render, no gacha character cards, no background scenery, no gradients/orbs/bokeh, no text, no annotations, no cell-border ornaments beyond simple gutters.
@@ -0,0 +1,73 @@
# Pre-Development Readiness Audit
Date: 2026-08-10
## Decision
The complete static design package and owner-facing HTML prototype are ready
for owner review. Formal game implementation remains frozen. This result does
not claim that the game is playable, understandable, fun, visually accepted,
heard, device-verified, packaged, or release-ready.
## Verified Design Authority
| Domain | Verified scope | Current result |
|---|---|---|
| Product | original fantasy, audience, 25-35 hour campaign, full completion and postgame | specified |
| Battle and systems | six-person formation, telegraphs, Directives, automation, failure and recovery | specified and cross-checked |
| Economy and save | 1,700 authored minutes, five pacing traces, prices, caps, atomic save and corruption handling | static validator pass |
| Content | 30 characters, 36 professions, 100 enemies, 320 items, 8 chapters, 48 achievements, 20 collections | complete indexed rows |
| Pages and states | 82 pages, 24 chapter visual families, onboarding, accessibility, localization, errors and exits | complete indexed rows |
| Art production | character, profession, enemy, item, environment, UI, animation and VFX identities | production catalog complete; final assets open |
| Audio production | 30 music suites, 94 ambience configurations, at least 2,602 bilingual voice assets, 710 SFX identities | production catalog complete; generation/listening open |
| HTML prototype | 11 views, 82-screen selector, 17-step flow, 8 chapters and searchable content catalogs | browser verified |
## Reproducible Local Checks
Run from the repository root while the site is served on port `8766`:
```bash
python3 tools/validate_design_contracts.py
python3 tools/build_prototype_data.py
node --check prototype/site/app.js
node --check tools/verify_prototype.js
NODE_PATH=/Users/wusumac/Documents/Projects/hapi/node_modules node tools/verify_prototype.js
git diff --check
```
Expected design marker:
```text
ABG_DESIGN_CONTRACTS_OK chapter_minutes=1700
gaps={'normal':150,'active':110,'efficient':160,'adversarial':130,'returning':140}
recruit_cap=250 counter_cap=180 cache_vectors=5 damage_floor=6
```
Expected prototype markers:
```text
ABG_PROTOTYPE_DATA_OK screens=82 characters=30 professions=36 enemies=100 items=320 chapters=8 achievements=48 collections=20
ABG_PROTOTYPE_BROWSER_OK views=11 screens=82 flow_steps=17 chapters=8 responsive=desktop,tablet,phone_portrait,phone_landscape
```
## Owner Review Order
1. Complete the 17-step core flow and name any step whose purpose or result is
unclear.
2. Inspect all eight chapter directions and judge whether each promises a
strategically and visually different problem.
3. Inspect the 82-screen map for missing pages, crowded pages, unclear primary
actions, and unsafe exits.
4. Search the complete content catalogs and challenge roles, counters,
progression relationships, and production feasibility.
5. Accept, revise, or reject the visual direction and product scope before a
CH-01 runtime vertical-slice Goal is opened.
## Open Gates
- Owner visual acceptance and page-by-page comprehension.
- Human judgment of choice quality, fun, pacing and willingness to continue.
- Final art selection, production, integration, animation and runtime review.
- Music, ambience, voice and SFX production plus human listening.
- Engine implementation, simulation, runtime, physical device and persistence.
- Packaging, signing, store records, submission, publishing and release.
+582
View File
@@ -0,0 +1,582 @@
const state = {
data: null,
view: "overview",
screenId: "SYS-004",
screenCategory: "ALL",
chapter: 0,
flowIndex: 0,
queries: {},
};
const viewNames = {
overview: "总览",
screens: "全部页面",
flow: "核心流程",
chapters: "八章画面",
characters: "角色图鉴",
professions: "职业系统",
enemies: "敌人图鉴",
items: "装备与物品",
records: "成就与收藏",
production: "美术与音频生产",
readiness: "开发准备度",
};
const screenCategoryNames = {
ALL: "全部",
SYS: "系统",
GLD: "Guild",
WRK: "工坊",
REC: "档案",
WLD: "世界",
EXP: "远征",
BAT: "战斗",
RES: "结果",
SET: "设置",
};
const chapterImages = Array.from({ length: 8 }, (_, index) =>
`assets/web/ch_ch${String(index + 1).padStart(2, "0")}.webp`,
);
const flowIds = [
"SYS-004",
"GLD-001",
"GLD-005",
"WLD-001",
"EXP-002",
"EXP-004",
"BAT-002",
"BAT-005",
"BAT-006",
"BAT-007",
"RES-001",
"RES-002",
"EXP-008",
"EXP-009",
"EXP-010",
"RES-005",
"GLD-001",
];
const $ = (selector, root = document) => root.querySelector(selector);
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
function escapeHtml(value = "") {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
function splitIdentity(identity) {
const match = identity.match(/^([^ ]+)\s+(.+)$/);
return match ? { id: match[1], name: match[2] } : { id: identity, name: identity };
}
function getScreen(id) {
return state.data.screens.find((screen) => screen.id === id);
}
function chapterNumberFromIdentity(identity) {
const match = identity.match(/CH-(\d{2})/);
return match ? Number(match[1]) : 1;
}
function setView(view, options = {}) {
state.view = view;
if (options.screenId) state.screenId = options.screenId;
if (Number.isInteger(options.chapter)) state.chapter = options.chapter;
history.replaceState(null, "", `#${view}`);
render();
closeMobileNav();
$("#content")?.focus({ preventScroll: true });
}
function viewHeading(eyebrow, title, description, aside = "") {
return `
<header class="view-heading">
<div>
<span class="eyebrow">${escapeHtml(eyebrow)}</span>
<h1>${escapeHtml(title)}</h1>
<p>${escapeHtml(description)}</p>
</div>
${aside}
</header>`;
}
function metric(value, label) {
return `<div class="metric"><strong>${escapeHtml(value)}</strong><span>${escapeHtml(label)}</span></div>`;
}
function chip(label) {
return `<span class="chip">${escapeHtml(label)}</span>`;
}
function renderOverview() {
const counts = state.data.counts;
return `
<section class="view">
${viewHeading(
"Complete pre-production",
"先把整款游戏设计清楚,再进入开发",
"Aetherbound Guild 是一款横屏 2D 日系手绘的六人编队策略 RPG。玩家不是连续点技能,而是读懂威胁、调整编队与规则、让方案自动执行,并在关键时刻用两次 Directive 改变战局。",
`${chip("25-35 小时主线")} ${chip("一次性买断")} ${chip("iOS / Android / PC / Console")}`,
)}
<div class="overview-hero">
<img src="assets/web/ch01_battle.webp" alt="第一章风车中继战斗概念图:三名冒险者面对机械钟兽" />
<div class="overview-hero-copy">
<span class="eyebrow">CH-01 DYNAMIC SLICE</span>
<h2>看预告、改规则、验证判断</h2>
<p>第一视觉焦点是敌方的中心线预告,第二是 Mara 保护后排,第三是可用的 Retune Directive。概念图只确定方向,HUD 和所有文字由真实界面实现。</p>
</div>
</div>
<div class="metric-row">
${metric(counts.screens, "完整页面与状态定义")}
${metric("8 × 3", "章节路线 / 战场 / 修复画面族")}
${metric(counts.characters, "有姓名、有成长弧的角色")}
${metric(counts.professions, "12 基础 + 24 进阶职业")}
${metric(counts.enemies, "具有独立机制的敌人")}
${metric(counts.items, "逐件定义效果与视觉的装备")}
${metric("30", "自适应音乐套件")}
${metric("710", "需要被验证的 SFX 事件身份")}
</div>
<section class="section-band">
<span class="eyebrow">PLAYER LOOP</span>
<h2>玩家真正反复做什么</h2>
<div class="thesis-grid">
<article class="info-block"><h3>读懂问题</h3><p>先看敌人的目标、倒计时、路线压力与奖励风险。所有必要信息在承诺前可见,随机只提出问题,不直接判玩家输。</p></article>
<article class="info-block"><h3>改变方案</h3><p>调整六人位置、职业、技能、装备、目标策略和 Standing Orders。每个选择都必须改变首个失败点或后续机会。</p></article>
<article class="info-block"><h3>形成世界后果</h3><p>战斗执行准备,奖励进入 Unsecured Cache;到 Anchor 决定安全与风险。每章修复一种公共服务,并改变下一轮路线。</p></article>
</div>
</section>
<section class="section-band">
<span class="eyebrow">REVIEW PATH</span>
<h2>建议你怎么检查</h2>
<div class="production-grid">
<article class="info-block"><h3>1. 先看核心流程</h3><p>从 Title 点到 Guild、编队、地图、路线、战斗、诊断、奖励、Anchor 与修复,判断每一步是否知道为什么要点。</p></article>
<article class="info-block"><h3>2. 再看八章差异</h3><p>检查每章是否具有不同的环境、学习目标、敌人问题、Boss 测试和永久世界变化,而不是换背景和数值。</p></article>
<article class="info-block"><h3>3. 最后看完整内容</h3><p>搜索 30 角色、36 职业、100 敌人、320 装备,确认它们都有玩法用途、反制关系和生产需求。</p></article>
</div>
<div class="flow-controls">
<button class="flow-action" type="button" data-open-view="flow">开始体验核心流程 →</button>
</div>
</section>
</section>`;
}
function artForScreen(screen) {
const prefix = screen.id.slice(0, 3);
if (prefix === "BAT" || ["EXP-004", "RES-001", "RES-002", "RES-003", "RES-004"].includes(screen.id)) {
return "assets/web/ch01_battle.webp";
}
if (prefix === "REC") {
if (screen.id === "REC-002") return "assets/web/el_enemies.webp";
if (screen.id === "REC-003") return "assets/web/el_items.webp";
return "assets/web/el_heroes.webp";
}
if (["GLD-003", "GLD-004", "GLD-007", "GLD-008", "GLD-009", "GLD-011", "GLD-012"].includes(screen.id)) {
return screen.id === "GLD-007" ? "assets/web/el_profession_kits.webp" : "assets/web/el_heroes.webp";
}
if (prefix === "WLD" || prefix === "EXP" || prefix === "WRK" || prefix === "GLD" || prefix === "SYS" || prefix === "RES") {
return "assets/web/ch_ch01.webp";
}
return "";
}
function stageTop(screen) {
return `
<div class="hud-top">
<div class="hud-group">
<span class="hud-chip"><i></i>${escapeHtml(screen.id)}</span>
<span class="hud-chip">目标 · 修复风车中继</span>
</div>
<div class="hud-group">
<span class="hud-chip safe"><i></i>Saved</span>
<span class="hud-chip risk"><i></i>Cache 180</span>
</div>
</div>`;
}
function renderRouteStage(screen) {
return `
${stageTop(screen)}
<div class="stage-title"><small>CH-01 · 风车盆地</small><strong>${escapeHtml(screen.name)}</strong><p>路线提前显示威胁、奖励、Cache 风险与 Anchor 距离。</p></div>
<div class="route-map">
<div class="route-line"></div>
<button class="route-node" type="button" title="营地">营</button>
<button class="route-node" type="button" title="救援目标">救</button>
<button class="route-node" type="button" title="精英威胁">危</button>
<button class="route-node" type="button" title="Anchor">锚</button>
</div>
<div class="stage-bottom"><button class="stage-secondary" type="button">查看比较</button><button class="stage-primary" type="button" data-flow-next>选择风车高地 →</button></div>`;
}
function renderFormationStage(screen) {
const slots = ["Mara · Front", "Oren · Mid", "Tamsin · Rear", "Suri · Front", "Ilyo · Rear", "空位"];
return `
${stageTop(screen)}
<div class="stage-title"><small>WEAVE GRID</small><strong>${escapeHtml(screen.name)}</strong><p>点角色再点目标位置;拖动只是可选快捷方式。</p></div>
<div class="formation-board">
${slots.map((slot, index) => `<button class="formation-slot ${index < 5 ? "is-filled" : ""} ${index === 2 ? "is-risk" : ""}" type="button">${escapeHtml(slot)}</button>`).join("")}
</div>
<div class="stage-generic-panel"><h3>首次受击预测</h3><div class="stage-list-row">Mara 拦截中心线</div><div class="stage-list-row">Tamsin 暴露于 Gust</div><div class="stage-list-row">角色标签 4/6 不重复</div></div>
<div class="stage-bottom"><button class="stage-secondary" type="button">预设</button><button class="stage-primary" type="button" data-flow-next>确认 Company →</button></div>`;
}
function renderBattleStage(screen) {
const directiveFlow = ["BAT-005", "BAT-006", "BAT-007"].includes(screen.id);
return `
${stageTop(screen)}
<div class="battle-telegraph"></div>
<div class="cache-strap"><b>UNSECURED CACHE</b><strong>180</strong></div>
<div class="directive-dock">
<button class="directive" type="button" data-flow-next>${directiveFlow ? "RETUNE · LEGAL" : "RETUNE · 1"}</button>
<button class="directive is-locked" type="button" title="需要另一个 Command">HOLD · 0</button>
</div>
<div class="stage-bottom"><button class="stage-secondary" type="button">事件记录</button><button class="stage-secondary" type="button">1× · 2×</button></div>`;
}
function renderSettingsStage(screen) {
return `
<div class="settings-stage">
<div class="settings-nav"><strong>SETTINGS</strong><span>显示与大字</span><span>动效与闪烁</span><span>音频与字幕</span><span>控制方式</span><span>存档与云端</span></div>
<div class="settings-content"><h3>${escapeHtml(screen.name)}</h3>
<div class="setting-row"><span>UI 与文字缩放 · 130%</span><div class="fake-slider"><i></i></div></div>
<div class="setting-row"><span>减少动态</span><i class="toggle"></i></div>
<div class="setting-row"><span>高对比目标轮廓</span><i class="toggle"></i></div>
<div class="setting-row"><span>字幕与事件标签</span><i class="toggle"></i></div>
<div class="setting-row"><span>低功耗模式</span><i class="toggle"></i></div>
</div>
</div>`;
}
function renderRecordsStage(screen, art) {
return `
${stageTop(screen)}
<div class="records-stage">
<div class="records-art"><img src="${art}" alt="概念元素图" /></div>
<div class="records-list"><strong>${escapeHtml(screen.name)}</strong><p>图鉴同时展示身份、机制、反制、获取与完成状态。</p><div class="record-line">已研究 · 机制完整</div><div class="record-line">可用反制 · 2</div><div class="record-line">章节关联 · CH-01</div><div class="record-line">生产状态 · Concept</div></div>
</div>`;
}
function renderGenericStage(screen) {
const prefix = screen.id.slice(0, 3);
const light = prefix === "WRK" || prefix === "GLD";
return `
${stageTop(screen)}
<div class="stage-title"><small>${escapeHtml(screen.id)} · DESIGN SPEC</small><strong>${escapeHtml(screen.name)}</strong><p>${escapeHtml(screen.focal)}</p></div>
<div class="stage-generic-panel"><h3>当前操作</h3><p>${escapeHtml(screen.action)}</p><div class="stage-list-row">状态与成本可见</div><div class="stage-list-row">取消不会消耗资源</div><div class="stage-list-row">所有拖动都有点选路径</div></div>
<div class="stage-bottom"><button class="stage-secondary" type="button">返回</button><button class="stage-primary" type="button" data-flow-next>${escapeHtml(screen.action)}</button></div>
${light ? "" : ""}`;
}
function renderGameStage(screen) {
const prefix = screen.id.slice(0, 3);
const art = artForScreen(screen);
const light = prefix === "SET" || prefix === "REC";
let content;
if (screen.id === "GLD-005") content = renderFormationStage(screen);
else if (["EXP-002", "EXP-003", "WLD-001", "WLD-002", "WLD-003"].includes(screen.id)) content = renderRouteStage(screen);
else if (prefix === "BAT" || ["EXP-004", "RES-001", "RES-002", "RES-003", "RES-004"].includes(screen.id)) content = renderBattleStage(screen);
else if (prefix === "SET") content = renderSettingsStage(screen);
else if (prefix === "REC") content = renderRecordsStage(screen, art);
else content = renderGenericStage(screen);
const decorativeArt = art && prefix !== "REC" ? `<img class="stage-art" src="${art}" alt="" />` : "";
return `<div class="game-stage ${light ? "stage-light" : ""}" data-stage-id="${escapeHtml(screen.id)}">${decorativeArt}${content}</div>`;
}
function screenMeta(screen, index, screens) {
const previous = screens[(index - 1 + screens.length) % screens.length];
const next = screens[(index + 1) % screens.length];
return `
<div class="screen-meta">
<div class="screen-meta-head">
<div><span class="screen-id">${escapeHtml(screen.id)}</span><h2>${escapeHtml(screen.name)}</h2>${chip("Design specification")} ${chip("Human review open")}</div>
<div class="pager"><button type="button" data-screen-id="${escapeHtml(previous.id)}">← 上一页</button><button type="button" data-screen-id="${escapeHtml(next.id)}">下一页 →</button></div>
</div>
<div class="spec-grid">
<div class="spec-cell"><span>三秒焦点顺序</span><p>${escapeHtml(screen.focal)}</p></div>
<div class="spec-cell"><span>唯一主操作</span><p>${escapeHtml(screen.action)}</p></div>
<div class="spec-cell"><span>状态、异常与退出</span><p>${escapeHtml(screen.variants)}</p></div>
</div>
</div>`;
}
function renderScreens() {
const category = state.screenCategory;
const filtered = state.data.screens.filter((screen) => category === "ALL" || screen.id.startsWith(`${category}-`));
if (!filtered.some((screen) => screen.id === state.screenId)) state.screenId = filtered[0].id;
const screen = getScreen(state.screenId);
const index = filtered.findIndex((entry) => entry.id === screen.id);
return `
<section class="view">
${viewHeading("82 SCREEN DEFINITIONS", "每一个页面和关键状态", "页面按系统、Guild、工坊、档案、世界、远征、战斗、结果与设置分类。每一项都锁定玩家先看什么、主操作是什么,以及错误和返回怎么处理。")}
<div class="toolbar"><div class="segmented">${Object.entries(screenCategoryNames).map(([key, label]) => `<button class="${key === category ? "is-active" : ""}" type="button" data-screen-category="${key}">${escapeHtml(label)}</button>`).join("")}</div></div>
<div class="screen-layout">
<aside class="screen-index" aria-label="页面列表">${filtered.map((entry) => `<button class="${entry.id === screen.id ? "is-active" : ""}" type="button" data-screen-id="${escapeHtml(entry.id)}"><span>${escapeHtml(entry.id)}</span><b>${escapeHtml(entry.name)}</b></button>`).join("")}</aside>
<div class="screen-detail"><div class="screen-stage-wrap">${renderGameStage(screen)}</div>${screenMeta(screen, index, filtered)}</div>
</div>
</section>`;
}
function renderFlow() {
const flowScreens = flowIds.map(getScreen);
const screen = flowScreens[state.flowIndex];
return `
<section class="view">
${viewHeading("CLICKABLE EXPERIENCE", "从首页到章节修复的完整点击链", "这是压缩的设计原型路径,用来检查玩家是否始终知道当前位置、压力、下一步和退出方式。它不模拟战斗公式或存档。", `${chip(`${state.flowIndex + 1} / ${flowScreens.length}`)} ${chip("无死按钮")}`)}
<div class="flow-shell">
<div><div class="screen-stage-wrap">${renderGameStage(screen)}</div>${screenMeta(screen, state.flowIndex, flowScreens)}</div>
<aside class="flow-steps" aria-label="核心流程步骤">${flowScreens.map((entry, index) => `<button class="${index === state.flowIndex ? "is-active" : ""}" type="button" data-flow-index="${index}"><span>${String(index + 1).padStart(2, "0")}</span><b>${escapeHtml(entry.id)}</b>${escapeHtml(entry.name)}</button>`).join("")}</aside>
</div>
<div class="flow-controls"><button class="text-button" type="button" data-flow-prev>← 上一步</button><button class="flow-action" type="button" data-flow-next>${state.flowIndex === flowScreens.length - 1 ? "回到流程开头" : "执行主操作,进入下一步 →"}</button></div>
</section>`;
}
function renderChapters() {
const chapter = state.data.chapters[state.chapter];
const { id, name } = splitIdentity(chapter.identity);
return `
<section class="view">
${viewHeading("8 CHAPTERS · 24 VISUAL FAMILIES", "每一章都提出新的策略问题", "每章都有独立的路线/聚落、代表战场和修复/抉择画面。概念图用于确认环境方向,完整细节以章节策划文档为准。")}
<div class="chapter-tabs">${state.data.chapters.map((entry, index) => `<button class="${index === state.chapter ? "is-active" : ""}" type="button" data-chapter="${index}">CH-${String(index + 1).padStart(2, "0")}</button>`).join("")}</div>
<div class="chapter-layout">
<div><div class="chapter-art"><img src="${chapterImages[state.chapter]}" alt="${escapeHtml(name)} 章节环境概念图" /></div><div class="chapter-states"><div class="chapter-state"><b>${id}A · 路线 / 聚落</b><span>${escapeHtml(chapter.region)} · ${escapeHtml(chapter.route)}</span></div><div class="chapter-state"><b>${id}B · 代表战场</b><span>${escapeHtml(chapter.learning)},Boss 通过真实机制检验这一章。</span></div><div class="chapter-state"><b>${id}C · 修复 / 抉择</b><span>${escapeHtml(chapter.change)}</span></div></div></div>
<article class="chapter-copy"><span class="eyebrow">${escapeHtml(id)}</span><h2>${escapeHtml(name)}</h2><h3>本章学习目标</h3><p>${escapeHtml(chapter.learning)}</p><h3>Boss 测试与奖励</h3><p>${escapeHtml(chapter.boss)}</p><h3>永久变化</h3><p>${escapeHtml(chapter.change)}</p>${chip("Concept art")} ${chip("Owner review open")}</article>
</div>
</section>`;
}
function searchableValues(entries, query) {
const normalized = query.trim().toLocaleLowerCase();
if (!normalized) return entries;
return entries.filter((entry) => Object.values(entry).join(" ").toLocaleLowerCase().includes(normalized));
}
function renderCatalog(view, entries, config) {
const query = state.queries[view] || "";
const filtered = searchableValues(entries, query);
const shown = filtered.slice(0, config.limit || 320);
return `
<section class="view">
${viewHeading(config.eyebrow, config.title, config.description, `${chip(`${filtered.length} / ${entries.length}`)} ${chip("完整权威索引")}`)}
<div class="toolbar"><input class="search-input" data-catalog-search="${view}" type="search" value="${escapeHtml(query)}" placeholder="搜索 ID、名称、技能、机制、章节…" aria-label="搜索${escapeHtml(config.title)}" /></div>
<div class="catalog-layout">
<aside class="catalog-visual"><img src="${config.image}" alt="${escapeHtml(config.alt)}" /><div class="catalog-visual-copy"><span class="eyebrow">CONCEPT ELEMENT BOARD</span><p>${escapeHtml(config.visualNote)}</p></div></aside>
<div class="catalog-list">${shown.length ? shown.map((entry) => config.row(entry)).join("") : `<div class="empty-state">没有匹配内容。请清除搜索条件。</div>`}</div>
</div>
</section>`;
}
function catalogEntry(identity, meta, body) {
const { id, name } = splitIdentity(identity);
return `<article class="catalog-entry"><div><small>${escapeHtml(id)}</small><strong>${escapeHtml(name)}</strong><span class="chip">${escapeHtml(meta)}</span></div><p>${escapeHtml(body)}</p></article>`;
}
function renderCharacters() {
return renderCatalog("characters", state.data.characters, {
eyebrow: "30 NAMED ADVENTURERS",
title: "每个角色都有玩法、动机与成长弧",
description: "角色不是随机兵种皮肤。每个人有稳定身份、职业亲和、招募路径、关系、个人弧线、视觉和双语声音需求。",
image: "assets/web/el_heroes.webp",
alt: "Mara、Oren 和 Ilyo 三名核心角色概念图",
visualNote: "概念板用于锁定成熟比例、职业仍保留人物身份、以及 Guild 修复者的工作质感。并非最终角色立绘或 sprite。",
row: (entry) => catalogEntry(entry.identity, "Character", `${entry.gameplay} ${entry.story}`),
});
}
function renderProfessions() {
return renderCatalog("professions", state.data.professions, {
eyebrow: "12 BASE · 24 ADVANCED",
title: "36 个职业都改变行动逻辑",
description: "每个职业拥有资源、两项主动技能、一项被动、AI 优先级、反制面、晋升证明,以及可生产的武器、动作、VFX 与声音身份。",
image: "assets/web/el_profession_kits.webp",
alt: "六套职业装备与资源装置概念图",
visualNote: "职业采用模块化装备和动作套件叠加在 30 个稳定人物身份上,不为每个角色重画一套通用职业人。",
row: (entry) => catalogEntry(entry.identity, entry.role.split(".")[0], `${entry.skills} ${entry.build}`),
});
}
function renderEnemies() {
return renderCatalog("enemies", state.data.enemies, {
eyebrow: "56 ORDINARY · 24 ELITE · 12 SUB-BOSS · 8 BOSS",
title: "100 个敌人不是换色数值包",
description: "每个敌人通过独立目标、时序、空间、资源或信息规则改变准备方案,并具有可读预告、至少两个回应和生产规格。",
image: "assets/web/el_enemies.webp",
alt: "第一章机械敌人、暗潮危险和预告几何概念图",
visualNote: "第一章敌人来自被破坏的公共机械,轮廓和动作直接表达机制。后续七章保留各自材料与社会功能。",
row: (entry) => catalogEntry(entry.identity, "Enemy mechanic", `${entry.mechanic} 反制读取:${entry.read}`),
});
}
function renderItems() {
return renderCatalog("items", state.data.items, {
eyebrow: "320 AUTHORED ITEMS",
title: "装备必须改变判断,而不是只抬战力",
description: "320 件物品逐件定义槽位、章节、效果、状态依赖、协同、反制、获取、升级与美术/声音。160 件属于行为变化物品。",
image: "assets/web/el_items.webp",
alt: "Guild 装备、工具、Cache 与 Anchor 元素概念图",
visualNote: "物品按真实物件展示,稀有度不会用巨大光效抢走机制信息。最终 320 件都需要独立透明物件图。",
row: (entry) => catalogEntry(entry.identity, `${entry.slot} · ${entry.tier} · ${entry.flag}`, `${entry.effect} ${entry.build}`),
});
}
function renderRecords() {
const achievements = state.data.achievements;
const collections = state.data.collections;
return `
<section class="view">
${viewHeading("48 ACHIEVEMENTS · 20 COLLECTIONS", "完成度记录玩家学会了什么", "成就验证真实机制、构筑和章节结果;收藏把区域证据变成 Guild 中可见的物理陈列。它们不靠重复刷次数延长游戏。")}
<div class="metric-row">${metric(48, "机制与叙事成就")}${metric(20, "区域收藏")}${metric("100 h", "全收集与高难完成目标")}${metric("0", "每日任务、通行证与抽卡")}</div>
<section class="section-band"><h2>成就</h2><div class="catalog-list">${achievements.map((entry) => catalogEntry(entry.identity, "Achievement", `${entry.requirement} 奖励:${entry.reward}`)).join("")}</div></section>
<section class="section-band"><h2>收藏</h2><div class="catalog-list">${collections.map((entry) => catalogEntry(entry.identity, "Collection", `${entry.entries} 陈列:${entry.display}`)).join("")}</div></section>
</section>`;
}
function renderProduction() {
return `
<section class="view">
${viewHeading("COMPLETE PRODUCTION INVENTORY", "美术、动画、音乐和声音怎么做", "完整数量已经拆成可生产的身份包、模块、动作、场景和事件。当前四张 AI 图都是概念方向,不是最终运行时资源。")}
<div class="metric-row">${metric("30", "角色身份 rig + 6 表情")}${metric("72", "主动技能动作 / VFX 合同")}${metric("752+", "非 Boss 敌人动作状态")}${metric("88", "章节环境主状态")}${metric("101", "模块化剧情构图")}${metric("30", "音乐套件")}${metric("94", "环境声配置")}${metric("2,602", "双语最低角色录音资产")}</div>
<div class="production-grid">
<article class="info-block"><h3>角色与职业</h3><p>30 套稳定角色身份 + 36 套职业装备、资源装置和动作模块。职业改变人物的战斗轮廓,但不把角色替换成无名 class sprite。</p></article>
<article class="info-block"><h3>敌人与装备</h3><p>100 个敌人各自拥有机制轮廓、动作与预告;320 件物品都有独立物件图,160 件行为物品还必须显示真实触发。</p></article>
<article class="info-block"><h3>环境与章节</h3><p>八章共 88 个环境状态和至少 200 个独立道具/结构族。背景、交互物、前景、危险与修复层分开,不能用一张平图冒充地图。</p></article>
<article class="info-block"><h3>音乐</h3><p>30 套自适应音乐包含 base、pulse、tension 和 resolution;八章保留不同材料与节拍,最终章组合而不抹平差异。</p></article>
<article class="info-block"><h3>语音</h3><p>角色圣经定义 773 条战斗/Hub 语义额度和 144 个角色场景。英文与简中最低 2,602 个录音资产,语义语音严格控制并发。</p></article>
<article class="info-block"><h3>SFX 与无障碍</h3><p>710 个事件身份必须与真实提交状态一致。静音、字幕、单声道、输出切换、低功耗和振动关闭都不能改变玩法信息。</p></article>
</div>
<section class="section-band"><h2>先做哪一块</h2><p>正式批量生产前只做 CH-01 风车中继动态切片:Mara / Oren / Ilyo、Bell-Feeder、两名支援敌人、三条战线、Retune Directive、诊断、Reward → Cache → Anchor,以及 Clinic / Warning Tower 两个永久修复结果。</p><button class="flow-action" type="button" data-open-view="flow">查看动态切片流程 →</button></section>
</section>`;
}
function renderReadiness() {
const rows = [
["产品与玩家体验", "完整", "Product contract, 25-35h campaign, onboarding and evidence gates", "设计一致性已校验;人类理解/乐趣开放"],
["战斗 / 职业 / 装备", "完整", "Deterministic combat, 12+24 professions, 320 items", "需通过可玩动态切片验证"],
["经济 / 难度 / 存档", "完整", "1700-min authored budget, five traces, atomic save/failure", "公式静态校验通过;运行时未实现"],
["世界与内容", "完整", "30 characters, 100 enemies, 8 chapters, ending/postgame", "数量与链接校验通过;无人工完整试玩"],
["页面与 UI", "完整规格", "82 screens, 24 chapter screen families, 14 layout families", "本 HTML 等 Owner 视觉/流程审核"],
["美术与动画", "完整清单", "Identity/modular production and CH-01 slice", "仅 4 张概念图;无 final/integrated/runtime"],
["音乐 / 环境 / 语音 / SFX", "完整清单", "30 music, 94 ambience, 2602 voice, 710 events", "未生成、未试听、未授权批量 SFX/music"],
["HTML 产品原型", "当前候选", "All indexes + compressed clickable core path", "需 Owner 逐页审阅与修改"],
["引擎开发", "冻结", "No Godot project or runtime code", "完整设计 Owner 批准后另开 Goal"],
["设备 / 打包 / 上架", "开放", "No build, signing, device or store action", "必须在运行时完成后分别授权"],
];
return `
<section class="view">
${viewHeading("PRE-DEVELOPMENT GATE", "现在还没有进入正式开发", "策划与生产规格已经大幅补齐,但 Owner 仍需要先审核页面、八章画面和核心流程。开发不能把‘文档完整’误认为‘游戏好玩’。")}
<table class="readiness-table"><thead><tr><th>领域</th><th>状态</th><th>已经具备</th><th>仍然开放</th></tr></thead><tbody>${rows.map(([area, status, ready, open]) => `<tr><td><strong>${escapeHtml(area)}</strong></td><td class="${status === "冻结" || status === "开放" ? "state-open" : "state-ready"}">${escapeHtml(status)}</td><td>${escapeHtml(ready)}</td><td>${escapeHtml(open)}</td></tr>`).join("")}</tbody></table>
<section class="section-band"><h2>你的最短下一步</h2><p>先走一遍“核心流程”,再看八章画面和全部页面。把不理解、不喜欢或认为过于复杂的页面 ID 直接记下来。确认这些设计方向后,才适合制作 CH-01 可玩动态切片。</p><button class="flow-action" type="button" data-open-view="flow">开始 Owner 审核 →</button></section>
</section>`;
}
function render() {
if (!state.data) return;
const renderers = {
overview: renderOverview,
screens: renderScreens,
flow: renderFlow,
chapters: renderChapters,
characters: renderCharacters,
professions: renderProfessions,
enemies: renderEnemies,
items: renderItems,
records: renderRecords,
production: renderProduction,
readiness: renderReadiness,
};
$("#content").innerHTML = renderers[state.view]();
$("#view-title").textContent = viewNames[state.view];
$$(".nav-item").forEach((button) => button.classList.toggle("is-active", button.dataset.view === state.view));
bindViewEvents();
}
function bindViewEvents() {
$$('[data-open-view]').forEach((button) => button.addEventListener("click", () => setView(button.dataset.openView)));
$$('[data-screen-category]').forEach((button) => button.addEventListener("click", () => {
state.screenCategory = button.dataset.screenCategory;
render();
}));
$$('[data-screen-id]').forEach((button) => button.addEventListener("click", () => {
state.screenId = button.dataset.screenId;
render();
}));
$$('[data-chapter]').forEach((button) => button.addEventListener("click", () => {
state.chapter = Number(button.dataset.chapter);
render();
}));
$$('[data-flow-index]').forEach((button) => button.addEventListener("click", () => {
state.flowIndex = Number(button.dataset.flowIndex);
render();
}));
$$('[data-flow-next]').forEach((button) => button.addEventListener("click", () => {
state.flowIndex = (state.flowIndex + 1) % flowIds.length;
if (state.view !== "flow") setView("flow"); else render();
}));
$$('[data-flow-prev]').forEach((button) => button.addEventListener("click", () => {
state.flowIndex = (state.flowIndex - 1 + flowIds.length) % flowIds.length;
render();
}));
$$('[data-catalog-search]').forEach((input) => input.addEventListener("input", () => {
const view = input.dataset.catalogSearch;
state.queries[view] = input.value;
render();
const replacement = $(`[data-catalog-search="${view}"]`);
replacement?.focus();
replacement?.setSelectionRange(replacement.value.length, replacement.value.length);
}));
}
function openMobileNav() {
$("#sidebar").classList.add("is-open");
$("#nav-scrim").hidden = false;
}
function closeMobileNav() {
$("#sidebar").classList.remove("is-open");
$("#nav-scrim").hidden = true;
}
function openCurrentStage() {
const stage = $(".game-stage");
if (!stage) return;
const dialog = $("#stage-dialog");
$("#dialog-stage").replaceChildren(stage.cloneNode(true));
dialog.showModal();
}
async function init() {
try {
const response = await fetch("data/catalog.json");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
state.data = await response.json();
const hashView = location.hash.replace("#", "");
if (viewNames[hashView]) state.view = hashView;
render();
} catch (error) {
$("#content").innerHTML = `<div class="empty-state"><h1>设计索引载入失败</h1><p>${escapeHtml(error.message)}</p><p>请通过本地 HTTP 服务或 Artifact Gallery 打开此页面。</p></div>`;
}
}
$("#primary-nav").addEventListener("click", (event) => {
const button = event.target.closest("[data-view]");
if (button) setView(button.dataset.view);
});
$("#open-nav").addEventListener("click", openMobileNav);
$("#close-nav").addEventListener("click", closeMobileNav);
$("#nav-scrim").addEventListener("click", closeMobileNav);
$("#fullscreen-current").addEventListener("click", openCurrentStage);
$("#close-dialog").addEventListener("click", () => $("#stage-dialog").close());
$("#stage-dialog").addEventListener("click", (event) => {
if (event.target === event.currentTarget) event.currentTarget.close();
});
window.addEventListener("keydown", (event) => {
if (event.key === "Escape") closeMobileNav();
});
init();
Binary file not shown.

After

Width:  |  Height:  |  Size: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

+64
View File
@@ -0,0 +1,64 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#171c22" />
<title>Aetherbound Guild 完整游戏设计</title>
<link rel="icon" type="image/webp" href="favicon.webp" />
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<a class="skip-link" href="#content">跳到内容</a>
<div class="app-shell">
<aside class="sidebar" id="sidebar" aria-label="设计导航">
<div class="brand-block">
<button class="icon-button mobile-only" id="close-nav" type="button" aria-label="关闭导航">×</button>
<span class="brand-mark" aria-hidden="true">AG</span>
<div>
<strong>Aetherbound Guild</strong>
<span>裂脉远征团 · 开发前设计</span>
</div>
</div>
<nav class="primary-nav" id="primary-nav">
<button class="nav-item is-active" data-view="overview" type="button"><span>01</span> 总览</button>
<button class="nav-item" data-view="screens" type="button"><span>02</span> 全部页面 <b>82</b></button>
<button class="nav-item" data-view="flow" type="button"><span>03</span> 核心流程</button>
<button class="nav-item" data-view="chapters" type="button"><span>04</span> 八章画面 <b>24</b></button>
<button class="nav-item" data-view="characters" type="button"><span>05</span> 角色 <b>30</b></button>
<button class="nav-item" data-view="professions" type="button"><span>06</span> 职业 <b>36</b></button>
<button class="nav-item" data-view="enemies" type="button"><span>07</span> 敌人 <b>100</b></button>
<button class="nav-item" data-view="items" type="button"><span>08</span> 装备 <b>320</b></button>
<button class="nav-item" data-view="records" type="button"><span>09</span> 成就与收藏</button>
<button class="nav-item" data-view="production" type="button"><span>10</span> 美术与音频</button>
<button class="nav-item" data-view="readiness" type="button"><span>11</span> 开发准备度</button>
</nav>
<div class="sidebar-footer">
<span class="status-dot"></span>
<div><strong>DESIGN SPEC</strong><span>运行时开发仍冻结</span></div>
</div>
</aside>
<div class="page-shell">
<header class="topbar">
<button class="icon-button mobile-only" id="open-nav" type="button" aria-label="打开导航">☰</button>
<div class="breadcrumbs"><span>完整策划</span><b>/</b><strong id="view-title">总览</strong></div>
<div class="top-actions">
<span class="evidence-badge">概念图 · 非运行时</span>
<button class="icon-button" id="fullscreen-current" type="button" title="全屏查看当前游戏画面" aria-label="全屏查看当前游戏画面">⛶</button>
</div>
</header>
<main id="content" tabindex="-1">
<div class="loading-state" id="loading">正在载入完整设计索引…</div>
</main>
</div>
</div>
<div class="nav-scrim" id="nav-scrim" hidden></div>
<dialog class="stage-dialog" id="stage-dialog">
<button class="dialog-close" id="close-dialog" type="button" aria-label="关闭全屏画面">×</button>
<div id="dialog-stage"></div>
</dialog>
<script src="app.js" defer></script>
</body>
</html>
+302
View File
@@ -0,0 +1,302 @@
:root {
color-scheme: light;
--ink-950: #171c22;
--ink-800: #2b333b;
--ink-650: #46515b;
--paper-050: #f7f3e8;
--paper-100: #eee9dc;
--paper-200: #ddd4c2;
--white: #fffef9;
--aether: #32b8c6;
--gold: #e5b445;
--jade: #3f9e78;
--coral: #d95c55;
--command: #477ec7;
--violet: #8c63a8;
--copper: #b86d3f;
--secured: #287a5a;
--line: rgba(23, 28, 34, 0.16);
--shadow: 0 16px 38px rgba(23, 28, 34, 0.14);
--sidebar: 278px;
--topbar: 64px;
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
* { box-sizing: border-box; }
html { min-width: 320px; background: #e8e4da; }
body { margin: 0; color: var(--ink-950); background: var(--paper-100); letter-spacing: 0; }
button, input, select { font: inherit; letter-spacing: 0; }
button { cursor: pointer; }
button:focus-visible, input:focus-visible, select:focus-visible { outline: 3px solid #fff; box-shadow: 0 0 0 5px var(--command); }
img { display: block; max-width: 100%; }
.skip-link { position: fixed; z-index: 100; left: 12px; top: -60px; padding: 10px 14px; color: #fff; background: var(--ink-950); }
.skip-link:focus { top: 12px; }
.app-shell { min-height: 100vh; display: grid; grid-template-columns: var(--sidebar) minmax(0, 1fr); }
.sidebar { position: sticky; top: 0; height: 100vh; overflow-y: auto; color: #f8f6ef; background: var(--ink-950); border-right: 1px solid rgba(255,255,255,.08); z-index: 40; }
.brand-block { min-height: 94px; display: flex; gap: 12px; align-items: center; padding: 18px; border-bottom: 1px solid rgba(255,255,255,.1); }
.brand-block > div { min-width: 0; }
.brand-block strong, .brand-block span { display: block; }
.brand-block strong { font-size: 15px; }
.brand-block span { margin-top: 3px; color: #bfc7cd; font-size: 12px; }
.brand-mark { flex: 0 0 42px; width: 42px; height: 42px; display: grid !important; place-items: center; color: var(--ink-950) !important; font-weight: 900; background: var(--gold); border: 2px solid #ffe295; border-radius: 4px; }
.primary-nav { padding: 14px 10px 90px; display: grid; gap: 4px; }
.nav-item { width: 100%; min-height: 48px; display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; align-items: center; gap: 8px; padding: 8px 10px; color: #d5dadd; text-align: left; border: 0; border-left: 3px solid transparent; border-radius: 4px; background: transparent; }
.nav-item > span { font-size: 11px; color: #87939c; }
.nav-item > b { min-width: 25px; padding: 2px 5px; text-align: center; font-size: 10px; color: var(--ink-950); background: #c5cdd2; border-radius: 8px; }
.nav-item:hover { background: rgba(255,255,255,.06); }
.nav-item.is-active { color: #fff; border-left-color: var(--gold); background: rgba(229,180,69,.12); }
.nav-item.is-active > span { color: var(--gold); }
.nav-item.is-active > b { background: var(--gold); }
.sidebar-footer { position: sticky; bottom: 0; display: flex; gap: 10px; align-items: center; padding: 14px 18px; background: #12171c; border-top: 1px solid rgba(255,255,255,.1); }
.sidebar-footer strong, .sidebar-footer span { display: block; }
.sidebar-footer strong { font-size: 11px; color: #fff; }
.sidebar-footer span { color: #9ca7af; font-size: 10px; }
.status-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--gold); box-shadow: 0 0 0 4px rgba(229,180,69,.15); }
.page-shell { min-width: 0; }
.topbar { position: sticky; top: 0; z-index: 30; min-height: var(--topbar); display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 10px clamp(16px, 3vw, 38px); background: rgba(247,243,232,.94); border-bottom: 1px solid var(--line); backdrop-filter: blur(12px); }
.breadcrumbs { min-width: 0; display: flex; gap: 9px; align-items: center; font-size: 13px; }
.breadcrumbs span, .breadcrumbs b { color: #75808a; font-weight: 500; }
.breadcrumbs strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.top-actions { display: flex; gap: 10px; align-items: center; }
.evidence-badge, .chip { display: inline-flex; min-height: 26px; align-items: center; padding: 4px 8px; font-size: 11px; font-weight: 700; color: var(--ink-800); background: var(--paper-200); border: 1px solid rgba(23,28,34,.12); border-radius: 4px; }
.icon-button { width: 42px; height: 42px; padding: 0; display: grid; place-items: center; font-size: 22px; line-height: 1; color: var(--ink-950); background: transparent; border: 1px solid var(--line); border-radius: 4px; }
.icon-button:hover { background: #fff; }
.mobile-only { display: none; }
main { min-height: calc(100vh - var(--topbar)); padding: clamp(22px, 3.5vw, 54px); }
.view { width: min(100%, 1480px); margin: 0 auto; }
.view-heading { display: flex; justify-content: space-between; gap: 24px; align-items: flex-end; margin-bottom: 28px; }
.view-heading > div { max-width: 860px; }
.eyebrow { display: block; margin-bottom: 8px; color: var(--copper); font-size: 11px; font-weight: 900; text-transform: uppercase; }
h1, h2, h3, p { margin-top: 0; }
h1 { margin-bottom: 10px; font-size: clamp(28px, 3.3vw, 46px); line-height: 1.08; }
h2 { margin-bottom: 10px; font-size: 24px; line-height: 1.2; }
h3 { margin-bottom: 8px; font-size: 17px; }
p { color: var(--ink-650); line-height: 1.7; }
.view-heading p { max-width: 820px; margin-bottom: 0; }
.metric-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1px; margin: 24px 0 36px; border: 1px solid var(--line); background: var(--line); }
.metric { min-height: 108px; padding: 18px; background: var(--white); }
.metric strong { display: block; margin-bottom: 7px; font-size: 30px; font-variant-numeric: tabular-nums; }
.metric span { color: var(--ink-650); font-size: 12px; }
.overview-hero { position: relative; min-height: 0; aspect-ratio: 16/9; overflow: hidden; background: var(--ink-950); border: 1px solid rgba(23,28,34,.32); box-shadow: var(--shadow); }
.overview-hero img { width: 100%; height: 100%; object-fit: contain; }
.overview-hero-copy { position: absolute; left: 3.5%; bottom: 5%; width: min(46%, 580px); padding: 20px 22px; color: #fff; background: rgba(23,28,34,.89); border-left: 5px solid var(--gold); }
.overview-hero-copy h2 { margin-bottom: 7px; font-size: clamp(20px, 2.3vw, 34px); }
.overview-hero-copy p { margin: 0; color: #dce4e8; line-height: 1.55; }
.section-band { margin-top: 42px; padding: 30px 0; border-top: 1px solid var(--line); }
.thesis-grid, .production-grid { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 18px; }
.info-block { min-height: 160px; padding: 20px; background: var(--white); border: 1px solid var(--line); border-radius: 6px; }
.info-block::before { content: ""; display: block; width: 42px; height: 4px; margin-bottom: 16px; background: var(--aether); }
.info-block:nth-child(2)::before { background: var(--gold); }
.info-block:nth-child(3)::before { background: var(--coral); }
.info-block p { margin-bottom: 0; font-size: 14px; }
.toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 22px; }
.search-input { min-width: min(100%, 320px); min-height: 44px; padding: 10px 12px; color: var(--ink-950); background: #fff; border: 1px solid var(--line); border-radius: 4px; }
.segmented { display: flex; flex-wrap: wrap; gap: 1px; border: 1px solid var(--line); background: var(--line); }
.segmented button { min-height: 42px; padding: 8px 12px; color: var(--ink-650); background: var(--white); border: 0; }
.segmented button.is-active { color: #fff; background: var(--ink-800); }
.screen-layout { display: grid; grid-template-columns: minmax(220px, 300px) minmax(0, 1fr); gap: clamp(18px, 2.5vw, 34px); align-items: start; }
.screen-index { position: sticky; top: calc(var(--topbar) + 20px); max-height: calc(100vh - var(--topbar) - 40px); overflow: auto; background: var(--white); border: 1px solid var(--line); }
.screen-index button { width: 100%; min-height: 52px; display: grid; grid-template-columns: 68px minmax(0,1fr); gap: 8px; align-items: center; padding: 9px 12px; text-align: left; color: var(--ink-650); background: transparent; border: 0; border-bottom: 1px solid var(--line); }
.screen-index button span { font-size: 11px; font-weight: 800; color: var(--copper); }
.screen-index button b { overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.screen-index button.is-active { color: var(--ink-950); background: #f1ead8; box-shadow: inset 4px 0 var(--gold); }
.screen-detail { min-width: 0; }
.screen-stage-wrap { padding: clamp(8px, 1.2vw, 16px); background: #d9d4c7; border: 1px solid var(--line); box-shadow: var(--shadow); }
.game-stage { position: relative; width: 100%; aspect-ratio: 16/9; overflow: hidden; color: #fff; background: #26313a; isolation: isolate; }
.game-stage > img.stage-art { position: absolute; inset: 0; z-index: -2; width: 100%; height: 100%; object-fit: cover; }
.game-stage::after { content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none; background: linear-gradient(180deg, rgba(10,15,18,.48) 0, transparent 22%, transparent 70%, rgba(10,15,18,.68) 100%); }
.game-stage.stage-light { color: var(--ink-950); background: #e6e0d2; }
.game-stage.stage-light::after { background: linear-gradient(180deg, rgba(247,243,232,.2), rgba(247,243,232,.02) 55%, rgba(247,243,232,.55)); }
.game-stage button { min-height: 38px; color: inherit; border: 1px solid rgba(255,255,255,.34); border-radius: 4px; background: rgba(23,28,34,.84); }
.hud-top { position: absolute; inset: 2.2% 2.4% auto; height: 8%; display: flex; justify-content: space-between; gap: 8px; align-items: center; }
.hud-group { display: flex; gap: 7px; align-items: center; }
.hud-chip { min-height: 32px; display: flex; align-items: center; gap: 6px; padding: 5px 9px; font-size: clamp(9px, .85vw, 13px); font-weight: 800; color: #fff; background: rgba(23,28,34,.84); border: 1px solid rgba(255,255,255,.24); border-radius: 4px; }
.hud-chip i { width: 9px; height: 9px; border-radius: 50%; background: var(--gold); }
.hud-chip.risk i { background: var(--copper); }
.hud-chip.safe i { background: var(--secured); }
.stage-title { position: absolute; top: 13%; left: 4%; max-width: 43%; text-shadow: 0 2px 12px #000; }
.stage-title small { display: block; margin-bottom: 6px; color: #ffe295; font-weight: 900; }
.stage-title strong { display: block; font-size: clamp(17px, 2.2vw, 34px); line-height: 1.15; }
.stage-title p { margin: 8px 0 0; color: #fff; font-size: clamp(9px, .9vw, 14px); line-height: 1.45; }
.stage-bottom { position: absolute; left: 3%; right: 3%; bottom: 3%; min-height: 11%; display: flex; justify-content: space-between; gap: 12px; align-items: end; }
.stage-primary { min-width: 23%; padding: 10px 16px; font-weight: 900; color: var(--ink-950) !important; background: var(--gold) !important; border-color: #ffe49d !important; }
.stage-secondary { padding: 8px 12px; }
.route-map { position: absolute; inset: 18% 6% 18%; }
.route-line { position: absolute; left: 8%; right: 8%; top: 50%; height: 4px; background: rgba(50,184,198,.86); box-shadow: 0 0 10px rgba(50,184,198,.8); transform: rotate(-5deg); }
.route-node { position: absolute; width: clamp(34px, 4.5vw, 66px); aspect-ratio: 1; display: grid; place-items: center; color: #fff; font-size: clamp(10px, 1vw, 15px); font-weight: 900; background: var(--ink-800); border: 3px solid var(--aether); border-radius: 50%; box-shadow: 0 4px 16px rgba(0,0,0,.35); }
.route-node:nth-child(2) { left: 8%; top: 47%; }
.route-node:nth-child(3) { left: 32%; top: 31%; border-color: var(--gold); }
.route-node:nth-child(4) { left: 57%; top: 47%; border-color: var(--coral); }
.route-node:nth-child(5) { right: 7%; top: 22%; border-color: var(--secured); }
.battle-telegraph { position: absolute; right: 23%; top: 37%; width: 25%; height: 20%; border: 3px solid var(--coral); clip-path: polygon(8% 0,100% 28%,90% 100%,0 78%); background: rgba(217,92,85,.16); }
.battle-telegraph::after { content: "3.2s · CENTER"; position: absolute; right: 8%; top: 12%; padding: 4px 7px; color: #fff; font-size: clamp(8px, .8vw, 12px); font-weight: 900; background: var(--coral); }
.directive-dock { position: absolute; left: 36%; right: 36%; bottom: 3%; height: 12%; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.directive { display: grid; place-items: center; font-size: clamp(9px, .85vw, 13px); font-weight: 900; background: rgba(34,67,114,.94) !important; border: 2px solid #71a9ef !important; }
.directive.is-locked { opacity: .55; background: rgba(43,51,59,.9) !important; border-color: #777 !important; }
.cache-strap { position: absolute; top: 15%; right: 1.5%; bottom: 18%; width: 7%; display: flex; flex-direction: column; justify-content: space-between; align-items: center; padding: 8px 4px; background: rgba(51,32,22,.83); border: 1px solid rgba(255,206,154,.42); }
.cache-strap b { color: #ffd0a4; font-size: clamp(8px,.75vw,11px); writing-mode: vertical-rl; }
.cache-strap strong { color: #fff; font-size: clamp(11px,1vw,15px); }
.formation-board { position: absolute; inset: 18% 27% 17% 22%; display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(2,1fr); gap: 4%; transform: skewY(-2deg); }
.formation-slot { position: relative; display: grid; place-items: center; color: #fff; font-size: clamp(8px,.85vw,13px); font-weight: 800; background: rgba(43,51,59,.78); border: 2px solid rgba(50,184,198,.75); }
.formation-slot.is-filled { background: rgba(40,122,90,.82); border-color: #7cccae; }
.formation-slot.is-risk { background: rgba(151,58,52,.82); border-color: #ff9892; }
.settings-stage { position: absolute; inset: 10% 5% 7%; display: grid; grid-template-columns: 26% 1fr; color: var(--ink-950); background: rgba(247,243,232,.96); border: 1px solid rgba(23,28,34,.28); }
.settings-nav { padding: 5%; background: var(--ink-800); color: #fff; }
.settings-nav strong { display: block; margin-bottom: 7%; font-size: clamp(10px,1vw,16px); }
.settings-nav span { display: block; padding: 5% 3%; font-size: clamp(8px,.8vw,13px); border-left: 3px solid transparent; }
.settings-nav span:first-of-type { border-left-color: var(--gold); background: rgba(255,255,255,.08); }
.settings-content { padding: 5%; overflow: hidden; }
.settings-content h3 { font-size: clamp(12px,1.4vw,22px); }
.setting-row { min-height: 15%; display: grid; grid-template-columns: 1fr 42%; align-items: center; gap: 5%; font-size: clamp(8px,.8vw,13px); border-bottom: 1px solid var(--line); }
.fake-slider { height: 6px; background: var(--paper-200); }
.fake-slider i { display: block; width: 65%; height: 100%; background: var(--command); }
.toggle { justify-self: end; width: 38px; height: 20px; padding: 2px; background: var(--jade); border-radius: 10px; }
.toggle::after { content: ""; display: block; width: 16px; height: 16px; margin-left: auto; background: #fff; border-radius: 50%; }
.records-stage { position: absolute; inset: 13% 5% 7%; display: grid; grid-template-columns: 48% 1fr; gap: 3%; }
.records-art { min-width: 0; overflow: hidden; background: rgba(247,243,232,.9); border: 1px solid rgba(23,28,34,.25); }
.records-art img { width: 100%; height: 100%; object-fit: contain; }
.records-list { padding: 5%; color: var(--ink-950); background: rgba(247,243,232,.96); }
.records-list strong { display: block; font-size: clamp(11px,1.3vw,20px); }
.records-list p { font-size: clamp(8px,.8vw,12px); }
.record-line { padding: 4% 0; font-size: clamp(8px,.75vw,12px); border-bottom: 1px solid var(--line); }
.stage-generic-panel { position: absolute; right: 4%; top: 17%; bottom: 16%; width: 32%; padding: 3%; background: rgba(23,28,34,.9); border-left: 4px solid var(--gold); }
.stage-generic-panel h3 { font-size: clamp(11px,1.3vw,20px); }
.stage-generic-panel p { color: #dce4e8; font-size: clamp(8px,.78vw,12px); }
.stage-list-row { padding: 5% 0; font-size: clamp(8px,.75vw,12px); border-bottom: 1px solid rgba(255,255,255,.15); }
.screen-meta { margin-top: 24px; padding: 0 2px; }
.screen-meta-head { display: flex; justify-content: space-between; gap: 16px; align-items: start; }
.screen-id { color: var(--copper); font-weight: 900; }
.screen-meta h2 { margin: 4px 0 8px; }
.spec-grid { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap: 1px; margin-top: 18px; background: var(--line); border: 1px solid var(--line); }
.spec-cell { min-height: 125px; padding: 16px; background: var(--white); }
.spec-cell span { display: block; margin-bottom: 8px; color: var(--copper); font-size: 10px; font-weight: 900; text-transform: uppercase; }
.spec-cell p { margin: 0; font-size: 13px; line-height: 1.55; }
.pager { display: flex; gap: 8px; }
.pager button, .text-button, .flow-action { min-height: 42px; padding: 8px 13px; color: var(--ink-950); background: var(--white); border: 1px solid var(--line); border-radius: 4px; }
.pager button:hover, .text-button:hover { background: #efe5ca; }
.chapter-tabs { display: grid; grid-template-columns: repeat(8,minmax(0,1fr)); gap: 1px; margin-bottom: 22px; border: 1px solid var(--line); background: var(--line); }
.chapter-tabs button { min-height: 48px; color: var(--ink-650); background: var(--white); border: 0; }
.chapter-tabs button.is-active { color: #fff; background: var(--ink-800); }
.chapter-layout { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(280px, .6fr); gap: 26px; align-items: start; }
.chapter-art { aspect-ratio: 16/9; background: #c9c4b8; border: 1px solid var(--line); box-shadow: var(--shadow); }
.chapter-art img { width: 100%; height: 100%; object-fit: contain; }
.chapter-copy { padding: 22px; background: var(--white); border-left: 5px solid var(--aether); }
.chapter-copy p { font-size: 13px; }
.chapter-states { display: grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap: 10px; margin-top: 18px; }
.chapter-state { padding: 13px; min-height: 105px; background: var(--white); border: 1px solid var(--line); }
.chapter-state b { display: block; margin-bottom: 6px; color: var(--copper); font-size: 11px; }
.chapter-state span { color: var(--ink-650); font-size: 12px; line-height: 1.45; }
.catalog-layout { display: grid; grid-template-columns: minmax(280px,.7fr) minmax(0,1.3fr); gap: 26px; align-items: start; }
.catalog-visual { position: sticky; top: calc(var(--topbar) + 20px); background: var(--white); border: 1px solid var(--line); }
.catalog-visual img { width: 100%; aspect-ratio: 1; object-fit: contain; }
.catalog-visual-copy { padding: 16px; border-top: 1px solid var(--line); }
.catalog-visual-copy p { margin: 0; font-size: 12px; }
.catalog-list { min-width: 0; display: grid; gap: 8px; }
.catalog-entry { display: grid; grid-template-columns: minmax(160px,.35fr) minmax(0,1fr); gap: 18px; padding: 16px; background: var(--white); border: 1px solid var(--line); border-left: 4px solid var(--paper-200); }
.catalog-entry:hover { border-left-color: var(--gold); }
.catalog-entry strong { display: block; margin-bottom: 4px; font-size: 13px; }
.catalog-entry small { color: var(--copper); font-weight: 800; }
.catalog-entry p { margin: 0; font-size: 12px; line-height: 1.5; }
.empty-state, .loading-state { padding: 46px; text-align: center; color: var(--ink-650); background: var(--white); border: 1px solid var(--line); }
.flow-shell { display: grid; grid-template-columns: minmax(0,1fr) 260px; gap: 24px; align-items: start; }
.flow-steps { display: grid; gap: 6px; }
.flow-steps button { min-height: 48px; display: grid; grid-template-columns: 26px 70px 1fr; align-items: center; gap: 8px; padding: 8px 10px; text-align: left; color: var(--ink-650); background: var(--white); border: 1px solid var(--line); border-left: 4px solid transparent; }
.flow-steps button.is-active { color: var(--ink-950); border-left-color: var(--gold); background: #f1ead8; }
.flow-steps button span { color: var(--copper); font-size: 11px; font-weight: 900; }
.flow-steps button b { overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
.flow-controls { display: flex; gap: 8px; margin-top: 16px; justify-content: flex-end; }
.flow-action { color: var(--ink-950); font-weight: 800; background: var(--gold); border-color: #c29428; }
.readiness-table { width: 100%; border-collapse: collapse; background: var(--white); }
.readiness-table th, .readiness-table td { padding: 13px 14px; text-align: left; vertical-align: top; border: 1px solid var(--line); font-size: 12px; line-height: 1.5; }
.readiness-table th { color: #fff; background: var(--ink-800); }
.state-open { color: #a54640; font-weight: 900; }
.state-ready { color: var(--secured); font-weight: 900; }
.stage-dialog { width: min(96vw, 1680px); max-width: none; padding: 54px 12px 12px; color: #fff; background: #090d10; border: 1px solid #4c5962; }
.stage-dialog::backdrop { background: rgba(5,8,10,.88); }
.dialog-close { position: absolute; right: 10px; top: 8px; width: 38px; height: 38px; color: #fff; font-size: 24px; background: transparent; border: 1px solid #64727b; border-radius: 4px; }
.stage-dialog .game-stage { width: min(100%, calc((100vh - 90px) * 16 / 9)); margin: auto; }
.nav-scrim { position: fixed; inset: 0; z-index: 35; background: rgba(10,14,17,.66); }
@media (max-width: 1100px) {
:root { --sidebar: 232px; }
.metric-row { grid-template-columns: repeat(2,minmax(0,1fr)); }
.thesis-grid, .production-grid { grid-template-columns: 1fr; }
.screen-layout { grid-template-columns: 220px minmax(0,1fr); }
.chapter-layout, .catalog-layout { grid-template-columns: 1fr; }
.catalog-visual { position: static; display: grid; grid-template-columns: minmax(220px,.6fr) 1fr; }
.catalog-visual img { max-height: 420px; }
.flow-shell { grid-template-columns: 1fr; }
.flow-steps { grid-template-columns: repeat(3,minmax(0,1fr)); }
.flow-steps button { grid-template-columns: 24px 1fr; }
.flow-steps button b { display: none; }
}
@media (max-width: 900px) {
:root { --topbar: 56px; }
.mobile-only { display: grid; }
.app-shell { display: block; }
.sidebar {
position: fixed;
left: 0;
top: 0;
width: min(88vw, 320px);
visibility: hidden;
pointer-events: none;
transform: translateX(calc(-100% - 2px));
transition: transform 180ms ease, visibility 0s linear 180ms;
}
.sidebar.is-open {
visibility: visible;
pointer-events: auto;
transform: translateX(0);
transition: transform 180ms ease, visibility 0s;
}
.brand-block .icon-button { margin-left: -4px; color: #fff; border-color: rgba(255,255,255,.2); }
.topbar { padding: 7px 12px; }
.top-actions .evidence-badge { display: none; }
main { padding: 20px 12px 34px; }
.view-heading { display: block; margin-bottom: 20px; }
h1 { font-size: 29px; }
.metric-row { grid-template-columns: repeat(2,minmax(0,1fr)); margin: 18px 0 24px; }
.metric { min-height: 92px; padding: 14px; }
.metric strong { font-size: 25px; }
.overview-hero-copy { position: static; width: auto; padding: 16px; }
.overview-hero { aspect-ratio: auto; }
.overview-hero img { aspect-ratio: 16/9; object-fit: contain; }
.screen-layout { display: block; }
.screen-index { position: static; display: flex; max-height: none; overflow-x: auto; margin-bottom: 14px; }
.screen-index button { flex: 0 0 150px; grid-template-columns: 1fr; border-right: 1px solid var(--line); border-bottom: 0; }
.screen-index button.is-active { box-shadow: inset 0 -4px var(--gold); }
.screen-index button b { white-space: normal; }
.screen-stage-wrap { padding: 5px; }
.screen-meta-head { display: block; }
.pager { margin-top: 12px; }
.spec-grid { grid-template-columns: 1fr; }
.spec-cell { min-height: 0; }
.chapter-tabs { display: flex; overflow-x: auto; }
.chapter-tabs button { flex: 0 0 70px; }
.chapter-copy { padding: 16px; }
.chapter-states { grid-template-columns: 1fr; }
.catalog-visual { display: block; }
.catalog-visual img { max-height: none; }
.catalog-entry { grid-template-columns: 1fr; gap: 8px; }
.flow-steps { display: flex; overflow-x: auto; }
.flow-steps button { flex: 0 0 130px; grid-template-columns: 1fr; }
.flow-steps button span { display: none; }
.stage-title p, .stage-generic-panel p { display: none; }
.records-stage { grid-template-columns: 42% 1fr; }
.setting-row { grid-template-columns: 1fr 35%; }
.readiness-table { display: block; overflow-x: auto; }
}
@media (orientation: landscape) and (max-height: 560px) {
.topbar { position: static; }
main { padding-top: 12px; }
.view-heading { margin-bottom: 12px; }
.screen-index { position: static; }
.stage-dialog { padding-top: 44px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: .001ms !important; animation-duration: .001ms !important; }
}
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env python3
"""Build the design prototype's read-only content index from Markdown authority."""
from __future__ import annotations
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "prototype" / "site" / "data" / "catalog.json"
def clean(value: str) -> str:
value = re.sub(r"\[([^]]+)\]\([^)]+\)", r"\1", value)
value = value.replace("**", "").replace("`", "")
return re.sub(r"\s+", " ", value).strip()
def rows(path: str, pattern: str) -> list[list[str]]:
result: list[list[str]] = []
for line in (ROOT / path).read_text().splitlines():
if re.match(pattern, line):
result.append([clean(cell) for cell in line.strip().strip("|").split("|")])
return result
def keyed(values: list[list[str]], keys: list[str]) -> list[dict[str, str]]:
result = []
for value in values:
assert len(value) == len(keys), (value[0], len(value), len(keys))
result.append(dict(zip(keys, value)))
return result
def main() -> None:
screens = keyed(
rows(
"docs/presentation/SCREEN_AND_STATE_MAP.md",
r"^\| `(?:SYS|GLD|WRK|REC|WLD|EXP|BAT|RES|SET)-\d{3}` \|",
),
["id", "name", "focal", "action", "variants"],
)
characters = keyed(
rows("docs/content/CHARACTER_BIBLE.md", r"^\| `CHR-\d{3}` "),
["identity", "gameplay", "story", "production"],
)
professions = keyed(
rows("docs/content/PROFESSION_AND_SKILL_MATRIX.md", r"^\| `PR-[BA]\d{2}` "),
["identity", "role", "skills", "logic", "build", "production"],
)
enemies = keyed(
rows("docs/content/ENEMY_BESTIARY.md", r"^\| `EN-[OESB]\d{3}` "),
["identity", "mechanic", "read", "progression", "production"],
)
items = keyed(
rows("docs/content/EQUIPMENT_CATALOG.md", r"^\| `IT-\d{3}` "),
["identity", "slot", "tier", "flag", "effect", "build", "source", "production"],
)
chapters = keyed(
rows("docs/content/CAMPAIGN_AND_CHAPTERS.md", r"^\| `CH-\d{2}` "),
["identity", "learning", "region", "route", "enemies", "boss", "change"],
)
achievements = keyed(
rows("docs/content/ACHIEVEMENTS_AND_COLLECTIONS.md", r"^\| `ACV-\d{3}` "),
["identity", "requirement", "meaning", "reward"],
)
collections = keyed(
rows("docs/content/ACHIEVEMENTS_AND_COLLECTIONS.md", r"^\| `COL-\d{3}` "),
["identity", "entries", "acquisition", "display"],
)
expected = {
"screens": 82,
"characters": 30,
"professions": 36,
"enemies": 100,
"items": 320,
"chapters": 8,
"achievements": 48,
"collections": 20,
}
payload = {
"version": 1,
"status": "design-specification-only",
"counts": expected,
"screens": screens,
"characters": characters,
"professions": professions,
"enemies": enemies,
"items": items,
"chapters": chapters,
"achievements": achievements,
"collections": collections,
}
actual = {key: len(payload[key]) for key in expected}
assert actual == expected, (actual, expected)
ids = [screen["id"] for screen in screens]
assert len(ids) == len(set(ids)), "duplicate screen ID"
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
print("ABG_PROTOTYPE_DATA_OK", actual, OUT.relative_to(ROOT))
if __name__ == "__main__":
main()
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env node
"use strict";
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const { chromium } = require("playwright");
const baseURL = process.env.ABG_PROTOTYPE_URL || "http://127.0.0.1:8766";
const outputDir = process.env.ABG_PROTOTYPE_EVIDENCE || "/tmp/aetherbound-prototype-review";
const browserPath = process.env.ABG_BROWSER_PATH || "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
async function clickView(page, view) {
await page.locator(`[data-view="${view}"]`).click();
await page.locator("main h1").waitFor({ state: "visible" });
}
async function noHorizontalOverflow(page, label) {
const dimensions = await page.evaluate(() => ({
scrollWidth: document.documentElement.scrollWidth,
clientWidth: document.documentElement.clientWidth,
}));
assert.ok(
dimensions.scrollWidth <= dimensions.clientWidth + 1,
`${label} overflow: ${JSON.stringify(dimensions)}`,
);
}
async function waitForMobileNavClosed(page) {
await page.waitForFunction(() => {
const sidebar = document.querySelector("#sidebar");
if (!sidebar || sidebar.classList.contains("is-open")) return false;
return sidebar.getBoundingClientRect().right <= 0.5;
});
}
async function main() {
fs.mkdirSync(outputDir, { recursive: true });
const browser = await chromium.launch({ headless: true, executablePath: browserPath });
try {
const errors = [];
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, deviceScaleFactor: 1 });
page.on("console", (message) => {
if (message.type() === "error") errors.push(`console: ${message.text()}`);
});
page.on("pageerror", (error) => errors.push(`pageerror: ${error.message}`));
await page.goto(baseURL, { waitUntil: "networkidle" });
assert.equal(await page.locator("#loading").count(), 0, "loading state must clear");
assert.equal(await page.locator(".nav-item").count(), 11, "primary view count");
await noHorizontalOverflow(page, "desktop overview");
await page.screenshot({ path: path.join(outputDir, "desktop-overview.png"), fullPage: true });
for (const view of [
"screens", "flow", "chapters", "characters", "professions", "enemies",
"items", "records", "production", "readiness",
]) {
await clickView(page, view);
assert.ok(await page.locator("main h1").isVisible(), `${view} heading`);
}
await clickView(page, "screens");
assert.equal(await page.locator(".screen-index button").count(), 82, "screen inventory count");
const ids = await page.locator(".screen-index button span").allTextContents();
assert.equal(new Set(ids).size, 82, "screen IDs must be unique");
await page.locator('[data-screen-id="BAT-002"]').first().click();
assert.ok(await page.locator('[data-stage-id="BAT-002"]').isVisible(), "battle screen stage");
assert.ok(await page.locator(".battle-telegraph").isVisible(), "battle telegraph");
await page.screenshot({ path: path.join(outputDir, "desktop-battle-screen.png"), fullPage: true });
await clickView(page, "flow");
assert.equal(await page.locator(".flow-steps button").count(), 17, "flow step count");
for (let index = 0; index < 17; index += 1) {
assert.equal(await page.locator(".flow-steps button.is-active").count(), 1, `active flow step ${index}`);
await page.locator(".flow-controls [data-flow-next]").click();
}
assert.ok(await page.locator('[data-stage-id="SYS-004"]').isVisible(), "flow wraps to title");
await clickView(page, "chapters");
assert.equal(await page.locator(".chapter-tabs button").count(), 8, "chapter count");
await page.locator('[data-chapter="7"]').click();
assert.match(await page.locator(".chapter-copy h2").textContent(), /A Concord of Many/);
assert.match(await page.locator(".chapter-art img").getAttribute("src"), /ch_ch08\.webp/);
await clickView(page, "items");
await page.locator('[data-catalog-search="items"]').fill("Aetherbound Guildcoat");
assert.equal(await page.locator(".catalog-entry").count(), 1, "item search result count");
assert.equal(await page.locator(".catalog-entry strong").textContent(), "Aetherbound Guildcoat");
await page.setViewportSize({ width: 1024, height: 768 });
await page.reload({ waitUntil: "networkidle" });
await clickView(page, "screens");
await page.locator('[data-screen-id="BAT-002"]').first().click();
const tabletStageBox = await page.locator(".game-stage").boundingBox();
assert.ok(
tabletStageBox && tabletStageBox.x >= 0 && tabletStageBox.x + tabletStageBox.width <= 1024.5,
`tablet stage box ${JSON.stringify(tabletStageBox)}`,
);
await noHorizontalOverflow(page, "tablet screens");
await page.screenshot({ path: path.join(outputDir, "tablet-battle-screen.png"), fullPage: true });
await page.setViewportSize({ width: 390, height: 844 });
await page.reload({ waitUntil: "networkidle" });
await page.locator("#open-nav").click();
assert.ok((await page.locator("#sidebar").getAttribute("class")).includes("is-open"), "mobile nav opens");
await page.locator('[data-view="screens"]').click();
assert.ok(!(await page.locator("#sidebar").getAttribute("class")).includes("is-open"), "mobile nav closes");
await waitForMobileNavClosed(page);
const stageBox = await page.locator(".game-stage").boundingBox();
assert.ok(stageBox && stageBox.x >= 0 && stageBox.x + stageBox.width <= 390.5, `mobile stage box ${JSON.stringify(stageBox)}`);
await noHorizontalOverflow(page, "phone portrait screens");
await page.screenshot({ path: path.join(outputDir, "phone-portrait-screens.png"), fullPage: true });
await page.setViewportSize({ width: 844, height: 390 });
await page.reload({ waitUntil: "networkidle" });
await page.locator("#open-nav").click();
await page.locator('[data-view="flow"]').click();
await waitForMobileNavClosed(page);
await noHorizontalOverflow(page, "phone landscape flow");
assert.ok(await page.locator(".game-stage").isVisible(), "landscape stage visible");
await page.screenshot({ path: path.join(outputDir, "phone-landscape-flow.png"), fullPage: true });
assert.deepEqual(errors, [], `browser errors: ${errors.join(" | ")}`);
console.log("ABG_PROTOTYPE_BROWSER_OK views=11 screens=82 flow_steps=17 chapters=8 responsive=desktop,tablet,phone_portrait,phone_landscape");
console.log(`evidence=${outputDir}`);
} finally {
await browser.close();
}
}
main().catch((error) => {
console.error(error.stack || error);
process.exitCode = 1;
});