Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e83ca442d4 |
@@ -2,22 +2,21 @@
|
||||
|
||||
An original landscape 2D Japanese hand-drawn guild-expedition strategy game.
|
||||
|
||||
This repository is currently in **complete pre-production design**. Formal game
|
||||
This repository is currently in **pre-development design review**. Formal game
|
||||
implementation, bulk final-asset production, packaging, and publishing are
|
||||
forbidden until the owner approves the complete product, content, presentation,
|
||||
balance, and interactive-design package.
|
||||
|
||||
## Current Product Boundary
|
||||
|
||||
- 30 named recruitable characters
|
||||
- 12 base professions and 24 advanced professions
|
||||
- 100 enemy combat identities across 8 chapters
|
||||
- 320 equipment and item entries
|
||||
- 25-35 hour first campaign, 60-80 hour build completion, roughly 100 hour
|
||||
collection and high-difficulty completion target
|
||||
- Generated run-bound Recruits with no fixed story protagonists
|
||||
- 12 base, 24 advanced, and 6 hidden Professions; 36 mechanical Traits
|
||||
- 320 equipment identities and 60 run-wide Artifacts
|
||||
- 100 ordinary/elite enemies and 16 bosses across 8 Regions
|
||||
- 15-25 hour first clear and 60+ hour mastery breadth
|
||||
- Landscape 16:9, premium cross-platform target, English and Simplified Chinese
|
||||
- 2D Japanese hand-drawn presentation using layered rigs, equipment layers,
|
||||
authored key poses, and unique portraits/illustrations
|
||||
- 2D Japanese hand-drawn presentation using modular Recruit assemblies,
|
||||
profession/equipment overlays, authored key poses, and readable silhouettes
|
||||
|
||||
The game may learn structural lessons from authorized private reference
|
||||
research, but its setting, characters, enemies, names, art, audio, UI,
|
||||
@@ -25,22 +24,24 @@ progression, formulas, and expression must be original.
|
||||
|
||||
## Review The Design
|
||||
|
||||
The current owner-facing HTML in [`prototype/site/`](prototype/site/) is a
|
||||
**coverage audit**, not a completed UI prototype. Owner review on 2026-08-10
|
||||
proved that generic specification templates were not understandable as game
|
||||
screens. Its page browser now distinguishes one dedicated page, 40 shared
|
||||
family mockups, and 41 pages whose UI is still missing.
|
||||
The owner-facing HTML in [`prototype/site/`](prototype/site/) is the current
|
||||
responsive landscape design prototype. It replaces the superseded coverage
|
||||
audit and reads the current authorities directly when the Gallery bundle is
|
||||
built.
|
||||
|
||||
1. **Flow specification:** inspect the required 17-step title-to-restoration
|
||||
order; do not treat it as a validated clickable experience.
|
||||
2. **Eight chapters:** inspect route, battle, and restored-world direction for
|
||||
every chapter.
|
||||
3. **Page coverage:** inspect all 82 requirements and identify which screens
|
||||
still need a dedicated composition.
|
||||
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.
|
||||
1. **First play:** inspect 23 states from Contract setup through two-of-four
|
||||
run creation, two separate Market hires, party ordering, risk, automatic
|
||||
battle, diagnosis, reward, equipment adaptation, casualty, and recovery.
|
||||
2. **Screen atlas:** inspect exactly 50 current page/state IDs. Every ID has a
|
||||
page-specific composition plus loading/empty, error/confirmation, and
|
||||
interruption/return variants; `SYS-005` exposes its five staged states.
|
||||
3. **Regions:** inspect all eight Region directions, encounter pressure, enemy
|
||||
identities, and two bosses per Region.
|
||||
4. **Catalogs:** search 42 Professions, 36 Traits, 320 equipment items, 60
|
||||
Artifacts, 100 ordinary/elite enemies, and 16 bosses.
|
||||
5. **Review gates:** open the six critical Owner review groups and keep human
|
||||
comprehension, visual, fun, listening, and device approval separate from
|
||||
deterministic HTML checks.
|
||||
|
||||
Run it locally from the repository root:
|
||||
|
||||
@@ -59,12 +60,20 @@ relative CSS, JavaScript, JSON, or image requests:
|
||||
python3 tools/build_gallery_bundle.py
|
||||
```
|
||||
|
||||
The build command regenerates `prototype/site/data/catalog.json`, verifies the
|
||||
current exact counts and unique IDs, and writes
|
||||
`prototype/gallery/aetherbound-guild-prototype.html`. Browser verification is
|
||||
in `prototype/site/verify-r6.js`; it requires an already-provisioned Playwright
|
||||
Node module and Chrome, starts no external service, and writes screenshots to
|
||||
`prototype/gallery/evidence/r6/`.
|
||||
|
||||
## 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.
|
||||
The HTML is an interactive **design prototype**, not a playable build. Existing
|
||||
image boards are labeled concept direction only. The current one-line party,
|
||||
transactions, readable UI, stable IDs, and text are independently authored in
|
||||
HTML; no 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)
|
||||
|
||||
@@ -1,141 +1,217 @@
|
||||
# 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.
|
||||
> Scope: owner-reviewable, pre-development landscape HTML.
|
||||
>
|
||||
> Authority: current product contract, `SCREEN_AND_STATE_MAP`,
|
||||
> `UI_AND_VISUAL_SYSTEM`, content catalogs, and art/audio production catalogs.
|
||||
>
|
||||
> Boundary: design prototype only. It contains no battle simulation, save
|
||||
> implementation, engine runtime, final media, packaging, store, or release
|
||||
> work.
|
||||
|
||||
## 1. Review Decisions
|
||||
## 1. Owner Decisions
|
||||
|
||||
The prototype must let the owner decide whether:
|
||||
The prototype gives the Owner one visible artifact for deciding 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 requirement inventory and eight chapters are present, while
|
||||
unfinished UI is labeled as unfinished rather than presented as 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.
|
||||
1. the opening Company choice is understandable and does not recommend a build;
|
||||
2. Market valuation, formation order, equipment deltas, risk, and automatic
|
||||
battle causality read as one complete game loop;
|
||||
3. death, equipment return, Replacement Credit, recovery, dismissal, and run
|
||||
abandonment remain distinct;
|
||||
4. the 50 current player-facing page/state IDs look like game screens rather
|
||||
than a requirements-card audit;
|
||||
5. the eight Region directions and complete authored catalogs support the
|
||||
promised breadth; and
|
||||
6. any owner-visible boundary requires redesign before formal development.
|
||||
|
||||
Owner approval of the HTML is design approval only. It does not approve engine
|
||||
implementation, paid bulk art/audio, packaging, publishing, or release.
|
||||
HTML approval remains design approval only. Comprehension, visual quality, fun,
|
||||
listening, device ergonomics, packaging, store, and release stay separate human
|
||||
gates.
|
||||
|
||||
## 2. Information Architecture
|
||||
## 2. Gallery 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.
|
||||
The gallery exposes one primary subject at a time:
|
||||
|
||||
```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
|
||||
First play (23 states)
|
||||
Screen atlas (50 IDs plus required state variants)
|
||||
Regions (RG-01..RG-08)
|
||||
Catalogs (Profession / Trait / Equipment / Artifact / Enemy / Boss)
|
||||
Review gates (six critical groups and evidence boundary)
|
||||
```
|
||||
|
||||
## 3. Game Screen Presentation
|
||||
Desktop and tablet use a persistent left rail. Phone layouts use an off-canvas
|
||||
drawer. The game canvas remains a stable `16:9` composition and the viewer
|
||||
provides 75-200% zoom without altering the source state.
|
||||
|
||||
Every screen entry shows:
|
||||
## 3. Current Screen Inventory
|
||||
|
||||
- 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 prototype contains exactly the 50 IDs in `SCREEN_AND_STATE_MAP` Section 5:
|
||||
|
||||
Owner review on 2026-08-10 rejected the shared-template result as
|
||||
incomprehensible. The coverage audit found `1` dedicated page, `40` shared
|
||||
family mockups, and `41` pages without an individual UI composition. A shared
|
||||
template is not a completed page. Until this gap is closed, the HTML may expose
|
||||
requirements and coverage but must not call itself a complete UI prototype.
|
||||
| Family | IDs | Count |
|
||||
|---|---|---:|
|
||||
| System and run entry | `SYS-001..SYS-008` | 8 |
|
||||
| Guild and forecast | `HUB-001..HUB-002` | 2 |
|
||||
| Market | `SHP-001..SHP-003` | 3 |
|
||||
| Roster and dismissal | `REC-001..REC-003` | 3 |
|
||||
| Equipment and Artifacts | `EQP-001..EQP-002`, `ART-001` | 3 |
|
||||
| Party and risk | `PTY-001..PTY-002`, `RSK-001..RSK-002` | 4 |
|
||||
| Automatic battle | `BAT-001..BAT-006` | 6 |
|
||||
| Result and recovery | `OUT-001..OUT-006` | 6 |
|
||||
| Progression and postgame | `PRO-001..PRO-003`, `PST-001..PST-003` | 6 |
|
||||
| Settings and data | `SET-001..SET-009` | 9 |
|
||||
| **Total** | | **50** |
|
||||
|
||||
## 4. Compressed Click Path
|
||||
Every ID has a canonical composition and inspectable loading/empty,
|
||||
error/confirmation, and interruption/return overlays sourced from its authority
|
||||
row. `SYS-005` remains one page ID while also exposing Contract, four-offer
|
||||
choice, creation review, pending creation, and receipt/handoff states.
|
||||
|
||||
The clickable experience uses these states:
|
||||
## 4. Critical Review Groups
|
||||
|
||||
### 4.1 Opening Two Of Four
|
||||
|
||||
`SYS-005` shows four equal-size stable generated Recruit offers. Each offer
|
||||
names its stable ID, base Profession, Trait benefit/cost read, and no-Coin
|
||||
status. Select and Inspect are separate. The Company begins with no selection;
|
||||
Review Company enables only at `2/2`; creation review keeps the unchosen IDs
|
||||
visible as `Not retained`; pending input is fixed-size; the receipt separates
|
||||
`Company 2` from `Line capacity 4`.
|
||||
|
||||
### 4.2 Market Hire
|
||||
|
||||
`SHP-001/002` expose three Recruit offers, four equipment offers, and three
|
||||
battle offers through separate tabs. The first Market shows affordable base
|
||||
offers without a recommendation treatment. Two distinct hire reviews and
|
||||
receipts advance `Company 2 -> 3 -> 4` while Line capacity remains `4`.
|
||||
|
||||
### 4.3 Formation And Equipment
|
||||
|
||||
`EQP-001/002` lead with the behavior-changing before/after comparison.
|
||||
`PTY-001/002` stage one horizontal cord from protected rear at left to exposed
|
||||
front at right; position `0` is the rightmost occupied marker. Reorder works by
|
||||
select-then-destination semantics; drag is never required.
|
||||
|
||||
### 4.4 Horizontal Automatic Battle
|
||||
|
||||
`BAT-001..006` show the allied rear-left/front-right order, enemy side, action
|
||||
gap, urgent forecast, objective, pause, `1x/2x/4x`, inspect, event log, captions,
|
||||
retreat review, and static resume review. No control inserts an attack or skill
|
||||
into the deterministic timeline.
|
||||
|
||||
### 4.5 Result, Reward, And Recovery
|
||||
|
||||
`OUT-001` leads with the first meaningful divergence on an expected/actual
|
||||
timeline. `OUT-002` keeps the generated Recruit readable while naming permanent
|
||||
loss, equipment return, party gap, and Credit. `OUT-003` presents equal reward
|
||||
regions. `OUT-004` presents complete recovery transactions. Run end and
|
||||
voluntary abandonment remain separate in `OUT-005/006`.
|
||||
|
||||
### 4.6 Catalog And Settings
|
||||
|
||||
`PRO-003` and the Gallery Catalogs expose stable IDs, names, mechanics,
|
||||
tradeoffs, counterplay, and exact counts. `SET-001..009` use segmented controls,
|
||||
switches, menus, remap rows, and destructive review. English and Simplified
|
||||
Chinese, 100/115/130% text state, high contrast, reduced motion, captions, and
|
||||
low-power state are interactive.
|
||||
|
||||
## 5. Complete First-Play Chain
|
||||
|
||||
The 23 review states cover the real first-play handoffs:
|
||||
|
||||
```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
|
||||
SYS-005 Contract
|
||||
-> four stable offers 0/2 -> 1/2 -> 2/2
|
||||
-> creation review -> pending -> receipt
|
||||
-> SHP-001 Company 2/4
|
||||
-> SHP-002 hire 1 -> SHP-001 Company 3/4
|
||||
-> SHP-002 hire 2 -> SHP-001 Company 4/4
|
||||
-> PTY-001 reorder
|
||||
-> RSK-001 compare -> RSK-002 commit
|
||||
-> BAT-001 opening -> BAT-002 live -> BAT-003 inspect
|
||||
-> OUT-001 diagnosis -> OUT-003 reward
|
||||
-> EQP-002 adaptation
|
||||
-> OUT-002 casualty -> OUT-004 recovery
|
||||
```
|
||||
|
||||
This currently proves only the required node order. It does not yet prove
|
||||
navigation comprehension or information hierarchy because several core nodes
|
||||
still lack their own page design. It also does not simulate combat formulas,
|
||||
persistence, pacing, audio, touch feel, or fun.
|
||||
The chain is a design interaction, not a gameplay simulation. It makes the
|
||||
intended decision and transaction boundaries visible without inventing prices,
|
||||
combat formulas, save behavior, or random results.
|
||||
|
||||
## 5. Visual Assets In This Prototype
|
||||
## 6. Content And Region Scope
|
||||
|
||||
Four paid image calls are the fixed task ceiling and have been used:
|
||||
`tools/build_gallery_bundle.py` parses the current Markdown tables and fails on
|
||||
any missing or duplicate identity. The generated payload contains:
|
||||
|
||||
- CH-01 battle concept;
|
||||
- CH-01..04 environment grid;
|
||||
- CH-05..08 environment grid;
|
||||
- character/profession/enemy/item element grid.
|
||||
| Catalog | Exact total |
|
||||
|---|---:|
|
||||
| Professions | 42 (`12` base, `24` advanced, `6` hidden) |
|
||||
| Traits | 36 |
|
||||
| Equipment | 320 |
|
||||
| Artifacts | 60 |
|
||||
| Ordinary/elite enemies | 100 |
|
||||
| Bosses | 16 |
|
||||
| Regions | 8 |
|
||||
|
||||
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.
|
||||
Region pages expose `RG-01..RG-08`, current pressure, encounter grammar,
|
||||
ordinary/elite assignments, and two bosses each. Existing image boards remain
|
||||
labeled visual direction and never override current Region names or mechanics.
|
||||
|
||||
## 6. Responsive Contract
|
||||
## 7. Responsive And Interaction 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.
|
||||
- Desktop: `1440x1000` evidence with persistent navigation and full game stage.
|
||||
- Tablet: `1024x768` evidence with persistent navigation and a fully visible
|
||||
battle composition.
|
||||
- Phone portrait: `390x844` evidence; navigation is a drawer and the complete
|
||||
four-offer canvas fits the content width.
|
||||
- Phone landscape: `844x390` evidence; gallery chrome compresses and the entire
|
||||
battle canvas fits the first viewport.
|
||||
- Page switching, group filters, previous/next, first-play states, catalog
|
||||
search/filter, Region tabs, EN/zh_CN, settings states, viewer open/close, and
|
||||
viewer zoom are keyboard/pointer/touch-compatible HTML controls.
|
||||
- No action depends on hover, precision drag, audio, color, rapid input, or an
|
||||
uncapturable animation state.
|
||||
|
||||
## 7. Prototype Acceptance
|
||||
## 8. Build And Evidence
|
||||
|
||||
- [x] All 82 screen IDs appear exactly once in the screen selector.
|
||||
- [x] Coverage is reported truthfully as 1 dedicated / 40 family / 41 missing.
|
||||
- [ ] All 82 requirements have page-specific, player-readable UI compositions.
|
||||
- [ ] All 17 core-flow nodes have dedicated UI and pass owner comprehension.
|
||||
- [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 comprehension failed on the generic screen presentation; visual,
|
||||
fun, audio, device, and release gates remain explicitly open.
|
||||
```sh
|
||||
python3 tools/build_gallery_bundle.py
|
||||
```
|
||||
|
||||
This regenerates `prototype/site/data/catalog.json`, validates exact counts,
|
||||
embeds CSS, JavaScript, JSON, favicon, and concept images, and writes:
|
||||
|
||||
```text
|
||||
prototype/gallery/aetherbound-guild-prototype.html
|
||||
```
|
||||
|
||||
Browser verification is implemented in `prototype/site/verify-r6.js`. With an
|
||||
already-provisioned Playwright Node module and Chrome:
|
||||
|
||||
```sh
|
||||
NODE_PATH=<existing-node_modules> node prototype/site/verify-r6.js
|
||||
```
|
||||
|
||||
It checks all 50 IDs, critical compositions, first-play interactions, catalog
|
||||
search, Region images, locale/settings state, viewer zoom, desktop/tablet/phone
|
||||
layout, browser errors, and offline Gallery loading. Evidence is written under:
|
||||
|
||||
```text
|
||||
prototype/gallery/evidence/r6/
|
||||
```
|
||||
|
||||
## 9. Acceptance State
|
||||
|
||||
- [x] Exactly 50 current IDs with generated-Recruit and current catalog scope;
|
||||
no superseded screen-count or authored-cast claim.
|
||||
- [x] Six critical review groups use player-readable landscape compositions.
|
||||
- [x] Every remaining page has a family-appropriate, page-specific composition
|
||||
and explicit global state variants.
|
||||
- [x] The 23-state first-play chain reaches recovery without a dead control.
|
||||
- [x] All eight current Regions and all current catalog counts are searchable or
|
||||
directly inspectable.
|
||||
- [x] Desktop, tablet, phone portrait, and phone landscape browser checks pass.
|
||||
- [x] The self-contained Gallery bundle opens offline with no external request.
|
||||
- [x] Existing concept boards remain labeled and no paid/final media was added.
|
||||
- [ ] Owner comprehension, visual acceptance, fun, listening, and device gates.
|
||||
- [ ] Formal development, runtime media, packaging, store, and release approval.
|
||||
|
||||
Vendored
+714
-538
File diff suppressed because it is too large
Load Diff
+5137
-5050
File diff suppressed because it is too large
Load Diff
+35
-28
@@ -1,64 +1,71 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="en">
|
||||
<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>
|
||||
<meta name="theme-color" content="#182127" />
|
||||
<title>Aetherbound Guild - Final Landscape Prototype</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>
|
||||
<a class="skip-link" href="#content">Skip to prototype</a>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar" id="sidebar" aria-label="设计导航">
|
||||
<aside class="sidebar" id="sidebar" aria-label="Prototype navigation">
|
||||
<div class="brand-block">
|
||||
<button class="icon-button mobile-only" id="close-nav" type="button" aria-label="关闭导航">×</button>
|
||||
<button class="icon-button mobile-only" id="close-nav" type="button" aria-label="Close navigation">×</button>
|
||||
<span class="brand-mark" aria-hidden="true">AG</span>
|
||||
<div>
|
||||
<strong>Aetherbound Guild</strong>
|
||||
<span>裂脉远征团 · 开发前设计</span>
|
||||
<span data-copy="brandSubtitle">Final design prototype</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>
|
||||
<button class="nav-item is-active" data-view="firstplay" type="button"><span>01</span><b data-copy="navFirst">First play</b><em>23</em></button>
|
||||
<button class="nav-item" data-view="screens" type="button"><span>02</span><b data-copy="navScreens">Screen atlas</b><em>50</em></button>
|
||||
<button class="nav-item" data-view="regions" type="button"><span>03</span><b data-copy="navRegions">Regions</b><em>8</em></button>
|
||||
<button class="nav-item" data-view="catalogs" type="button"><span>04</span><b data-copy="navCatalogs">Catalogs</b><em>574</em></button>
|
||||
<button class="nav-item" data-view="review" type="button"><span>05</span><b data-copy="navReview">Review gates</b><em>6</em></button>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<span class="status-dot"></span>
|
||||
<div><strong>DESIGN SPEC</strong><span>运行时开发仍冻结</span></div>
|
||||
<span class="status-dot" aria-hidden="true"></span>
|
||||
<div><strong>DESIGN PROTOTYPE</strong><span data-copy="runtimeFrozen">Runtime development frozen</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>
|
||||
<button class="icon-button mobile-only" id="open-nav" type="button" aria-label="Open navigation">☰</button>
|
||||
<div class="breadcrumbs"><span>Aetherbound Guild</span><b>/</b><strong id="view-title">First play</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 class="locale-control" aria-label="Language">
|
||||
<button class="is-active" type="button" data-locale="en">EN</button>
|
||||
<button type="button" data-locale="zh_CN">简中</button>
|
||||
</div>
|
||||
<span class="evidence-badge" data-copy="conceptBadge">Concept direction</span>
|
||||
<button class="icon-button" id="fullscreen-current" type="button" title="Open stage viewer" aria-label="Open stage viewer">⛶</button>
|
||||
</div>
|
||||
</header>
|
||||
<main id="content" tabindex="-1">
|
||||
<div class="loading-state" id="loading">正在载入完整设计索引…</div>
|
||||
<div class="loading-state" id="loading">Loading current design authority...</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 class="stage-dialog" id="stage-dialog" aria-label="Game stage viewer">
|
||||
<div class="viewer-toolbar">
|
||||
<span id="viewer-label">Game stage</span>
|
||||
<div>
|
||||
<button class="icon-button" id="zoom-out" type="button" aria-label="Zoom out" title="Zoom out">−</button>
|
||||
<button class="viewer-zoom" id="zoom-reset" type="button">100%</button>
|
||||
<button class="icon-button" id="zoom-in" type="button" aria-label="Zoom in" title="Zoom in">+</button>
|
||||
<button class="icon-button" id="close-dialog" type="button" aria-label="Close viewer" title="Close">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="viewer-viewport"><div id="dialog-stage"></div></div>
|
||||
</dialog>
|
||||
<div class="sr-only" id="live-region" aria-live="polite"></div>
|
||||
<script src="app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+894
-309
File diff suppressed because it is too large
Load Diff
Vendored
+235
@@ -0,0 +1,235 @@
|
||||
#!/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 root = path.resolve(__dirname, "../..");
|
||||
const baseURL = process.env.ABG_PROTOTYPE_URL || "http://127.0.0.1:8766";
|
||||
const outputDir = process.env.ABG_PROTOTYPE_EVIDENCE || path.join(root, "prototype/gallery/evidence/r6");
|
||||
const browserPath = process.env.ABG_BROWSER_PATH || "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
||||
|
||||
const criticalScreens = {
|
||||
"SYS-005": ".opening-composition",
|
||||
"SHP-001": ".market-composition",
|
||||
"SHP-002": ".market-review",
|
||||
"EQP-002": ".equipment-composition",
|
||||
"PTY-001": ".party-composition",
|
||||
"BAT-002": ".battle-composition",
|
||||
"OUT-001": ".result-composition",
|
||||
"OUT-003": ".reward-composition",
|
||||
"OUT-004": ".recovery-composition",
|
||||
"PRO-003": ".records-composition",
|
||||
"SET-002": ".settings-composition",
|
||||
};
|
||||
|
||||
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}: ${JSON.stringify(dimensions)}`);
|
||||
}
|
||||
|
||||
async function stageFitsViewport(page, label, requireVertical = false) {
|
||||
const metrics = await page.locator(".game-stage").first().evaluate((stage) => {
|
||||
const box = stage.getBoundingClientRect();
|
||||
return { left: box.left, right: box.right, top: box.top, bottom: box.bottom, width: box.width, height: box.height, viewportWidth: innerWidth, viewportHeight: innerHeight };
|
||||
});
|
||||
assert.ok(metrics.left >= -0.5 && metrics.right <= metrics.viewportWidth + 0.5, `${label} horizontal stage: ${JSON.stringify(metrics)}`);
|
||||
if (requireVertical) assert.ok(metrics.top >= -0.5 && metrics.bottom <= metrics.viewportHeight + 0.5, `${label} vertical stage: ${JSON.stringify(metrics)}`);
|
||||
assert.ok(Math.abs(metrics.width / metrics.height - 16 / 9) < 0.02, `${label} aspect ratio: ${JSON.stringify(metrics)}`);
|
||||
}
|
||||
|
||||
async function openView(page, view) {
|
||||
const desktopButton = page.locator(`[data-view="${view}"]`);
|
||||
if (!(await desktopButton.isVisible())) {
|
||||
await page.locator("#open-nav").click();
|
||||
}
|
||||
await desktopButton.click();
|
||||
await page.locator("main h1").waitFor({ state: "visible" });
|
||||
}
|
||||
|
||||
async function waitForMobileNavClosed(page) {
|
||||
await page.waitForFunction(() => {
|
||||
const sidebar = document.querySelector("#sidebar");
|
||||
return sidebar && !sidebar.classList.contains("is-open") && sidebar.getBoundingClientRect().right <= 0.5;
|
||||
});
|
||||
}
|
||||
|
||||
async function selectScreen(page, id) {
|
||||
await openView(page, "screens");
|
||||
const button = page.locator(`.screen-index [data-screen-id="${id}"]`);
|
||||
await button.evaluate((element) => element.click());
|
||||
await page.locator(`[data-stage-id="${id}"]`).waitFor({ state: "visible" });
|
||||
}
|
||||
|
||||
async function screenshot(page, name) {
|
||||
const destination = path.join(outputDir, name);
|
||||
await page.screenshot({ path: destination, fullPage: true });
|
||||
return destination;
|
||||
}
|
||||
|
||||
async function desktopAudit(browser, evidence) {
|
||||
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 clears");
|
||||
assert.equal(await page.locator(".nav-item").count(), 5, "gallery view count");
|
||||
assert.equal(await page.locator(".flow-progress button").count(), 23, "complete first-play state count");
|
||||
assert.equal(await page.locator(".recruit-offer").count(), 4, "four provisional offers");
|
||||
assert.equal(await page.locator(".recruit-offer.is-selected").count(), 0, "opening build is not preselected");
|
||||
await noHorizontalOverflow(page, "desktop opening");
|
||||
await stageFitsViewport(page, "desktop opening");
|
||||
evidence.push(await screenshot(page, "desktop-opening.png"));
|
||||
|
||||
await page.locator('[data-offer-id="OFF-01"]').click();
|
||||
assert.equal(await page.locator(".recruit-offer.is-selected").count(), 1, "first provisional selection");
|
||||
await page.locator('[data-offer-id="OFF-03"]').click();
|
||||
assert.equal(await page.locator(".recruit-offer.is-selected").count(), 2, "second provisional selection");
|
||||
assert.ok(await page.locator(".opening-footer .stage-primary").isEnabled(), "Review Company enabled at 2/2");
|
||||
|
||||
await page.locator('[data-flow-index="7"]').click();
|
||||
assert.equal(await page.locator('[data-stage-id="SHP-001"] .company-meter strong').first().textContent(), "2", "first Market Company count");
|
||||
evidence.push(await screenshot(page, "desktop-market.png"));
|
||||
await page.locator('[data-flow-index="12"]').click();
|
||||
assert.equal(await page.locator('[data-stage-id="PTY-001"] .party-actor').count(), 4, "party line actor count");
|
||||
assert.equal(await page.locator('[data-stage-id="PTY-001"] .party-actor').last().locator("> b").textContent(), "0", "rightmost position is 0");
|
||||
evidence.push(await screenshot(page, "desktop-formation.png"));
|
||||
await page.locator('[data-flow-index="16"]').click();
|
||||
assert.ok(await page.locator('[data-stage-id="BAT-002"] .forecast-arrow').isVisible(), "battle forecast visible");
|
||||
assert.equal(await page.locator('[data-stage-id="BAT-002"] .allied-line .party-actor').count(), 4, "battle party line count");
|
||||
evidence.push(await screenshot(page, "desktop-battle.png"));
|
||||
await page.locator('[data-flow-index="18"]').click();
|
||||
assert.ok(await page.locator('[data-stage-id="OUT-001"] .is-divergence').isVisible(), "result divergence visible");
|
||||
evidence.push(await screenshot(page, "desktop-result.png"));
|
||||
await page.locator('[data-flow-index="22"]').click();
|
||||
assert.equal(await page.locator('[data-stage-id="OUT-004"] .recovery-path').count(), 3, "recovery paths visible");
|
||||
evidence.push(await screenshot(page, "desktop-recovery.png"));
|
||||
|
||||
await openView(page, "screens");
|
||||
assert.equal(await page.locator(".screen-index button").count(), 50, "screen inventory count");
|
||||
const ids = await page.locator(".screen-index button span").allTextContents();
|
||||
assert.equal(new Set(ids).size, 50, "screen IDs unique");
|
||||
for (const id of ids) {
|
||||
const button = page.locator(`.screen-index [data-screen-id="${id}"]`);
|
||||
await button.evaluate((element) => element.click());
|
||||
assert.equal(await page.locator(".game-stage").first().getAttribute("data-stage-id"), id, `${id} owns its composition`);
|
||||
assert.equal(await page.locator(".prototype-missing").count(), 0, `${id} has no missing placeholder`);
|
||||
}
|
||||
for (const [id, selector] of Object.entries(criticalScreens)) {
|
||||
await selectScreen(page, id);
|
||||
assert.ok(await page.locator(`[data-stage-id="${id}"] ${selector}`).isVisible(), `${id} critical composition`);
|
||||
}
|
||||
await page.locator('[data-screen-variant="error"]').click();
|
||||
assert.ok(await page.locator(".state-variant-overlay.error").isVisible(), "screen error variant inspectable");
|
||||
|
||||
await openView(page, "regions");
|
||||
assert.equal(await page.locator(".region-tabs button").count(), 8, "region count");
|
||||
for (let index = 0; index < 8; index += 1) {
|
||||
await page.locator(`[data-region-index="${index}"]`).click();
|
||||
await page.waitForFunction(() => {
|
||||
const image = document.querySelector(".region-stage img");
|
||||
return image && image.complete && image.naturalWidth > 0;
|
||||
});
|
||||
}
|
||||
evidence.push(await screenshot(page, "desktop-regions.png"));
|
||||
|
||||
await openView(page, "catalogs");
|
||||
assert.equal(await page.locator(".catalog-tabs button").count(), 6, "searchable catalog family count");
|
||||
await page.locator('[data-catalog-type="professions"]').click();
|
||||
await page.locator('[data-catalog-filter="base"]').click();
|
||||
assert.equal(await page.locator(".catalog-entry").count(), 12, "base profession count");
|
||||
await page.locator('[data-catalog-type="equipment"]').click();
|
||||
await page.locator("[data-catalog-search]").fill("Stepback Soles");
|
||||
assert.equal(await page.locator(".catalog-entry").count(), 1, "equipment search result");
|
||||
assert.equal(await page.locator(".catalog-entry strong").textContent(), "Stepback Soles", "equipment search identity");
|
||||
evidence.push(await screenshot(page, "desktop-catalog.png"));
|
||||
|
||||
await selectScreen(page, "SET-002");
|
||||
await page.locator('[data-text-scale="130"]').click();
|
||||
assert.equal(await page.locator("body").getAttribute("data-text-scale"), "130", "130% state applies");
|
||||
await page.locator('[data-setting-key="contrast"]').click();
|
||||
assert.match(await page.locator("body").getAttribute("class"), /high-contrast/, "high-contrast state applies");
|
||||
await page.locator('[data-locale="zh_CN"]').first().click();
|
||||
assert.equal(await page.locator("html").getAttribute("lang"), "zh-CN", "Simplified Chinese state applies");
|
||||
assert.match(await page.locator("main h1").textContent(), /显示与文字/, "selected screen localizes");
|
||||
evidence.push(await screenshot(page, "desktop-settings-zh-130.png"));
|
||||
await page.locator('[data-text-scale="100"]').click();
|
||||
|
||||
await page.locator("#fullscreen-current").click();
|
||||
assert.ok(await page.locator("#stage-dialog").evaluate((dialog) => dialog.open), "stage viewer opens");
|
||||
await page.locator("#zoom-in").click();
|
||||
assert.equal(await page.locator("#zoom-reset").textContent(), "125%", "stage viewer zooms");
|
||||
await page.locator("#close-dialog").click();
|
||||
|
||||
assert.deepEqual(errors, [], `desktop browser errors: ${errors.join(" | ")}`);
|
||||
await page.close();
|
||||
}
|
||||
|
||||
async function responsiveAudit(browser, evidence) {
|
||||
const fixtures = [
|
||||
{ name: "tablet-battle.png", viewport: { width: 1024, height: 768 }, view: "screens", screen: "BAT-002", vertical: false },
|
||||
{ name: "phone-portrait-opening.png", viewport: { width: 390, height: 844 }, view: "firstplay", vertical: true },
|
||||
{ name: "phone-landscape-battle.png", viewport: { width: 844, height: 390 }, view: "firstplay", flow: 16, vertical: true },
|
||||
];
|
||||
for (const fixture of fixtures) {
|
||||
const page = await browser.newPage({ viewport: fixture.viewport, deviceScaleFactor: 1 });
|
||||
const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
await page.goto(`${baseURL}#${fixture.view}`, { waitUntil: "networkidle" });
|
||||
if (fixture.screen) await selectScreen(page, fixture.screen);
|
||||
if (Number.isInteger(fixture.flow)) await page.locator(`[data-flow-index="${fixture.flow}"]`).click();
|
||||
if (fixture.viewport.width < 880) {
|
||||
await page.locator("#open-nav").click();
|
||||
assert.match(await page.locator("#sidebar").getAttribute("class"), /is-open/, `${fixture.name} drawer opens`);
|
||||
await page.locator("#close-nav").click();
|
||||
assert.doesNotMatch(await page.locator("#sidebar").getAttribute("class"), /is-open/, `${fixture.name} drawer closes`);
|
||||
await waitForMobileNavClosed(page);
|
||||
}
|
||||
await noHorizontalOverflow(page, fixture.name);
|
||||
await stageFitsViewport(page, fixture.name, fixture.vertical);
|
||||
evidence.push(await screenshot(page, fixture.name));
|
||||
assert.deepEqual(errors, [], `${fixture.name} browser errors: ${errors.join(" | ")}`);
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
|
||||
async function bundleAudit(browser) {
|
||||
const bundle = path.join(root, "prototype/gallery/aetherbound-guild-prototype.html");
|
||||
assert.ok(fs.statSync(bundle).size > 1_000_000, "Gallery bundle contains embedded media");
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
|
||||
const external = [];
|
||||
page.on("request", (request) => {
|
||||
if (!request.url().startsWith("file:") && !request.url().startsWith("data:")) external.push(request.url());
|
||||
});
|
||||
await page.goto(`file://${bundle}`, { waitUntil: "load" });
|
||||
assert.equal(await page.locator(".recruit-offer").count(), 4, "Gallery bundle initializes offline");
|
||||
assert.deepEqual(external, [], `Gallery external requests: ${external.join(" | ")}`);
|
||||
await page.close();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
const browser = await chromium.launch({ headless: true, executablePath: browserPath });
|
||||
const evidence = [];
|
||||
try {
|
||||
await desktopAudit(browser, evidence);
|
||||
await responsiveAudit(browser, evidence);
|
||||
await bundleAudit(browser);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
console.log("ABG_R6_BROWSER_OK screens=50 flow_states=23 regions=8 catalogs=42,36,320,60,100,16 responsive=desktop,tablet,phone_portrait,phone_landscape bundle=offline");
|
||||
console.log(`evidence=${evidence.join(",")}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.stack || error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
+193
-15
@@ -1,18 +1,200 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a self-contained HTML artifact for sandboxed Gallery viewers."""
|
||||
"""Build current prototype data and a self-contained Gallery artifact."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SITE = ROOT / "prototype" / "site"
|
||||
DATA_PATH = SITE / "data" / "catalog.json"
|
||||
DEFAULT_OUTPUT = ROOT / "prototype" / "gallery" / "aetherbound-guild-prototype.html"
|
||||
|
||||
|
||||
def clean(value: str) -> str:
|
||||
value = re.sub(r"\[([^]]+)\]\([^)]+\)", r"\1", value)
|
||||
value = value.replace("**", "").replace("`", "")
|
||||
return re.sub(r"\s+", " ", value).strip()
|
||||
|
||||
|
||||
def table_rows(path: Path, id_pattern: str) -> list[list[str]]:
|
||||
matcher = re.compile(id_pattern)
|
||||
rows: list[list[str]] = []
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
if not line.startswith("|"):
|
||||
continue
|
||||
cells = [clean(cell) for cell in line.strip().strip("|").split("|")]
|
||||
if cells and matcher.match(cells[0]):
|
||||
rows.append(cells)
|
||||
return rows
|
||||
|
||||
|
||||
def split_identity(value: str, pattern: str) -> tuple[str, str]:
|
||||
match = re.match(pattern, value)
|
||||
if not match:
|
||||
raise ValueError(f"invalid catalog identity: {value}")
|
||||
return match.group(1), match.group(2)
|
||||
|
||||
|
||||
def require_unique(entries: list[dict[str, str]], expected: int, label: str) -> None:
|
||||
ids = [entry["id"] for entry in entries]
|
||||
if len(entries) != expected or len(set(ids)) != expected:
|
||||
raise ValueError(f"{label}: expected {expected} unique rows, received {len(entries)} / {len(set(ids))}")
|
||||
|
||||
|
||||
def build_catalog() -> dict[str, object]:
|
||||
screen_rows = table_rows(
|
||||
ROOT / "docs/presentation/SCREEN_AND_STATE_MAP.md",
|
||||
r"^(?:SYS|HUB|SHP|REC|EQP|ART|PTY|RSK|BAT|OUT|PRO|PST|SET)-\d{3}\b",
|
||||
)
|
||||
screens = []
|
||||
for row in screen_rows:
|
||||
match = re.match(r"^([A-Z]+-\d{3}) (.+) / ([A-Z]+)$", row[0])
|
||||
if not match or len(row) != 6:
|
||||
raise ValueError(f"invalid screen row: {row[0]}")
|
||||
screens.append(
|
||||
{
|
||||
"id": match.group(1),
|
||||
"name": match.group(2),
|
||||
"input": match.group(3),
|
||||
"purpose": row[1],
|
||||
"action": row[2],
|
||||
"emptyLoading": row[3],
|
||||
"errorConfirmation": row[4],
|
||||
"interruptionReturn": row[5],
|
||||
}
|
||||
)
|
||||
|
||||
profession_rows = table_rows(ROOT / "docs/content/PROFESSION_CATALOG.md", r"^PF-[BAH]\d{2}$")
|
||||
professions = []
|
||||
for row in profession_rows:
|
||||
if len(row) < 6:
|
||||
continue
|
||||
family = {"B": "base", "A": "advanced", "H": "hidden"}[row[0][3]]
|
||||
identity = row[1].split(";", 1)
|
||||
cadence_index = 4 if len(row) == 7 else 3
|
||||
strategy_index = 5 if len(row) == 7 else 4
|
||||
professions.append(
|
||||
{
|
||||
"id": row[0],
|
||||
"name": identity[0].strip(),
|
||||
"family": family,
|
||||
"role": identity[1].strip() if len(identity) > 1 else family,
|
||||
"rules": row[2],
|
||||
"cadence": row[cadence_index],
|
||||
"strategy": row[strategy_index],
|
||||
"production": row[-1],
|
||||
}
|
||||
)
|
||||
|
||||
traits = [
|
||||
{
|
||||
"id": row[0],
|
||||
"name": row[1],
|
||||
"benefit": row[2],
|
||||
"cost": row[3],
|
||||
"evidence": row[4],
|
||||
"prohibited": row[5],
|
||||
}
|
||||
for row in table_rows(ROOT / "docs/content/TRAIT_CATALOG.md", r"^TR-\d{3}$")
|
||||
]
|
||||
|
||||
equipment = []
|
||||
for row in table_rows(ROOT / "docs/content/EQUIPMENT_CATALOG.md", r"^EQ-\d{3}\b"):
|
||||
item_id, name = split_identity(row[0], r"^(EQ-\d{3}) (.+)$")
|
||||
equipment.append(
|
||||
{
|
||||
"id": item_id,
|
||||
"name": name,
|
||||
"fit": row[1],
|
||||
"class": row[2],
|
||||
"effect": row[3],
|
||||
"tradeoff": row[4],
|
||||
"production": row[5],
|
||||
}
|
||||
)
|
||||
|
||||
artifacts = []
|
||||
for row in table_rows(ROOT / "docs/content/ARTIFACT_CATALOG.md", r"^AR-\d{3}\b"):
|
||||
artifact_id, name = split_identity(row[0], r"^(AR-\d{3}) (.+)$")
|
||||
artifacts.append(
|
||||
{
|
||||
"id": artifact_id,
|
||||
"name": name,
|
||||
"rule": row[1],
|
||||
"tradeoff": row[2],
|
||||
"production": row[3],
|
||||
}
|
||||
)
|
||||
|
||||
enemies = []
|
||||
bosses = []
|
||||
for row in table_rows(ROOT / "docs/content/ENEMY_CATALOG.md", r"^(?:EN-R\d-\d{2}|BO-R\d-\d{2})$"):
|
||||
enemy_class, name = (part.strip() for part in row[1].split(";", 1))
|
||||
entry = {
|
||||
"id": row[0],
|
||||
"name": name,
|
||||
"class": enemy_class,
|
||||
"rule": row[2],
|
||||
"responses": row[3],
|
||||
"production": row[4],
|
||||
}
|
||||
(bosses if row[0].startswith("BO-") else enemies).append(entry)
|
||||
|
||||
regions = []
|
||||
for row in table_rows(ROOT / "docs/content/REGION_CATALOG.md", r"^RG-\d{2}$"):
|
||||
identity = row[1].split(";", 1)
|
||||
regions.append(
|
||||
{
|
||||
"id": row[0],
|
||||
"name": identity[0].strip(),
|
||||
"framing": identity[1].strip() if len(identity) > 1 else "",
|
||||
"pressure": row[2],
|
||||
"progression": row[3],
|
||||
"assignment": row[4],
|
||||
"production": row[5],
|
||||
}
|
||||
)
|
||||
|
||||
expected = {
|
||||
"screens": 50,
|
||||
"professions": 42,
|
||||
"traits": 36,
|
||||
"equipment": 320,
|
||||
"artifacts": 60,
|
||||
"enemies": 100,
|
||||
"bosses": 16,
|
||||
"regions": 8,
|
||||
}
|
||||
entries = {
|
||||
"screens": screens,
|
||||
"professions": professions,
|
||||
"traits": traits,
|
||||
"equipment": equipment,
|
||||
"artifacts": artifacts,
|
||||
"enemies": enemies,
|
||||
"bosses": bosses,
|
||||
"regions": regions,
|
||||
}
|
||||
for label, count in expected.items():
|
||||
require_unique(entries[label], count, label)
|
||||
|
||||
payload: dict[str, object] = {
|
||||
"version": 2,
|
||||
"status": "design-prototype-current-authority",
|
||||
"authorityRevision": "recruit-line-design-r3",
|
||||
"counts": expected,
|
||||
**entries,
|
||||
}
|
||||
DATA_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
DATA_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
return payload
|
||||
|
||||
|
||||
def data_uri(path: Path) -> str:
|
||||
mime = "image/webp" if path.suffix == ".webp" else "application/octet-stream"
|
||||
encoded = base64.b64encode(path.read_bytes()).decode("ascii")
|
||||
@@ -20,10 +202,10 @@ def data_uri(path: Path) -> str:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
catalog = build_catalog()
|
||||
index = (SITE / "index.html").read_text(encoding="utf-8")
|
||||
styles = (SITE / "styles.css").read_text(encoding="utf-8")
|
||||
app = (SITE / "app.js").read_text(encoding="utf-8")
|
||||
catalog = json.loads((SITE / "data" / "catalog.json").read_text(encoding="utf-8"))
|
||||
|
||||
asset_paths = sorted((SITE / "assets" / "web").glob("*.webp"))
|
||||
asset_paths.append(SITE / "favicon.webp")
|
||||
@@ -33,28 +215,24 @@ def main() -> None:
|
||||
index = index.replace(relative, uri)
|
||||
app = app.replace(relative, uri)
|
||||
|
||||
catalog_json = json.dumps(catalog, ensure_ascii=False, separators=(",", ":"))
|
||||
catalog_json = catalog_json.replace("<", "\\u003c")
|
||||
catalog_json = json.dumps(catalog, ensure_ascii=False, separators=(",", ":")).replace("<", "\\u003c")
|
||||
app = app.replace("</script", "<\\/script")
|
||||
styles = styles.replace("</style", "<\\/style")
|
||||
|
||||
index = index.replace(
|
||||
'<link rel="stylesheet" href="styles.css" />',
|
||||
f"<style>\n{styles}\n</style>",
|
||||
)
|
||||
index = index.replace('<link rel="stylesheet" href="styles.css" />', f"<style>\n{styles}\n</style>")
|
||||
index = index.replace(
|
||||
'<script src="app.js" defer></script>',
|
||||
f'<script id="prototype-data" type="application/json">{catalog_json}</script>\n'
|
||||
f"<script>\n{app}\n</script>",
|
||||
f'<script id="prototype-data" type="application/json">{catalog_json}</script>\n<script>\n{app}\n</script>',
|
||||
)
|
||||
|
||||
if 'href="styles.css"' in index or 'src="app.js"' in index:
|
||||
raise RuntimeError("external stylesheet or script remains in bundle")
|
||||
if "assets/web/" in index:
|
||||
raise RuntimeError("external prototype asset remains in bundle")
|
||||
forbidden = ('href="styles.css"', 'src="app.js"', "assets/web/")
|
||||
remaining = [token for token in forbidden if token in index]
|
||||
if remaining:
|
||||
raise RuntimeError(f"external prototype dependency remains: {remaining}")
|
||||
|
||||
DEFAULT_OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
DEFAULT_OUTPUT.write_text(index, encoding="utf-8")
|
||||
counts = catalog["counts"]
|
||||
print(f"ABG_PROTOTYPE_DATA_OK counts={json.dumps(counts, separators=(',', ':'))}")
|
||||
print(f"ABG_GALLERY_BUNDLE_OK bytes={DEFAULT_OUTPUT.stat().st_size} output={DEFAULT_OUTPUT}")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user