From e23b00b200cda1e751ad1a93ba72a7184f78dc2d Mon Sep 17 00:00:00 2001 From: wusumac <736139669@qq.com> Date: Wed, 12 Aug 2026 09:42:49 +0800 Subject: [PATCH] feat: clarify recruit equipment and levels --- CURRENT_CHECKPOINT.md | 19 ++-- README.md | 7 ++ docs/README.md | 4 + docs/content/EQUIPMENT_CATALOG.md | 14 +-- docs/content/RECRUIT_GENERATION.md | 14 +-- docs/content/TRAIT_CATALOG.md | 2 +- .../PHASE7_1_RECRUIT_LOADOUT_LEVEL_GOAL.md | 54 ++++++++++++ .../ART_ASSET_AND_ANIMATION_CATALOG.md | 8 +- ...PHASE7_1_RECRUIT_LOADOUT_LEVEL_CONTRACT.md | 33 +++++++ docs/presentation/SCREEN_AND_STATE_MAP.md | 10 +-- docs/presentation/UI_AND_VISUAL_SYSTEM.md | 28 +++--- docs/product/ECONOMY_AND_BALANCE.md | 19 ++-- docs/product/GAME_PRODUCT_CONTRACT.md | 12 ++- docs/product/SAVE_AND_FAILURE_CONTRACT.md | 21 ++--- docs/product/SYSTEMS_AND_BATTLE.md | 43 ++++++---- .../PHASE7_1_RECRUIT_LOADOUT_LEVEL_LEDGER.md | 11 +++ prototype/site/app.js | 56 ++++++++---- prototype/site/data/catalog.json | 66 +++++++------- prototype/site/styles.css | 32 ++++++- prototype/site/verify-r7.js | 20 +++++ .../verify_recruit_loadout_level_contract.py | 86 +++++++++++++++++++ 21 files changed, 421 insertions(+), 138 deletions(-) create mode 100644 docs/goals/PHASE7_1_RECRUIT_LOADOUT_LEVEL_GOAL.md create mode 100644 docs/presentation/PHASE7_1_RECRUIT_LOADOUT_LEVEL_CONTRACT.md create mode 100644 docs/reviews/PHASE7_1_RECRUIT_LOADOUT_LEVEL_LEDGER.md create mode 100644 tools/verify_recruit_loadout_level_contract.py diff --git a/CURRENT_CHECKPOINT.md b/CURRENT_CHECKPOINT.md index a358dcc4..beb5d116 100644 --- a/CURRENT_CHECKPOINT.md +++ b/CURRENT_CHECKPOINT.md @@ -1,11 +1,10 @@ # Current Checkpoint -Active Goal: `docs/goals/PHASE7_UI_PRODUCTION_ATLAS_GOAL.md` -Active contract: `docs/presentation/PHASE7_UI_PRODUCTION_CONTRACT.md` -Active ledger: `docs/reviews/PHASE7_UI_PRODUCTION_LEDGER.md` -Baseline: `9a734e913cabc9846edca754c1666399ff501e71` -Candidate source: `7bfd7b5c248a7f02a7eefcfeecfe42f534a7c673` -Stage: `UI_PRODUCTION_ATLAS_CANDIDATE_READY` +Active Goal: `docs/goals/PHASE7_1_RECRUIT_LOADOUT_LEVEL_GOAL.md` +Active contract: `docs/presentation/PHASE7_1_RECRUIT_LOADOUT_LEVEL_CONTRACT.md` +Active ledger: `docs/reviews/PHASE7_1_RECRUIT_LOADOUT_LEVEL_LEDGER.md` +Baseline: `afc243a0071303f56ab99486fcabbbb7ae421c2d` +Stage: `RECRUIT_LOADOUT_LEVEL_CLARITY_ACTIVE` The Phase 6 battle, VFX and equipment direction is Owner-accepted. Phase 7 turns the existing 50-screen design inventory into an individually viewable, @@ -22,6 +21,8 @@ inventory SHA-256 is The bundle SHA-256 is `e54f910df77502d2ea8521829d38062501ba9fe861cb36017ceb22ec2d6990ee`. -Owner comprehension, visual acceptance and authorization for a Godot vertical -slice remain open. The shortest next action is Owner review of the published -Phase 7 Gallery atlas. +Owner feedback opens one bounded reconciliation: every Recruit has exactly one +active equipment slot and an individual run-scoped level. Phase 7.1 repairs +the inherited three-slot contradictions and makes these facts explicit in the +atlas. Owner comprehension, visual acceptance and authorization for a Godot +vertical slice remain open. diff --git a/README.md b/README.md index 756c940c..b32fd7fc 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,9 @@ balance, and interactive-design package. ## Current Product Boundary - Generated run-bound Recruits with no fixed story protagonists +- One active equipment slot per Recruit; item form controls eligibility rather + than adding simultaneous slots +- Individual run-scoped Recruit levels `1..10` with non-transferable XP - 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 @@ -43,6 +46,8 @@ use native HTML text rather than generated lettering. 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. + Recruit offers, Party, Roster, Equipment and loss/progression pages expose + individual level plus the one current equipment item. 5. **Reference map:** compare 15 proven functional structures with the current original Aetherbound composition; no commercial media is embedded. 6. **Review gates:** open the seven critical Owner review groups and keep human @@ -82,6 +87,8 @@ service, and writes ignored screenshots to a caller-selected evidence root. `python3 tools/verify_ui_asset_provenance.py` proves that all 34 character, equipment and VFX presentation copies are byte-identical to their recorded accepted candidate sources. +`python3 tools/verify_recruit_loadout_level_contract.py` proves the Phase 7.1 +one-item, individual-level contract and preserves all 320 equipment identities. ## Evidence Boundary diff --git a/docs/README.md b/docs/README.md index 2de431d1..bec66b6d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -48,6 +48,10 @@ authority after the direction is approved. and [`reviews/PHASE7_UI_PRODUCTION_LEDGER.md`](reviews/PHASE7_UI_PRODUCTION_LEDGER.md): current 50-page Chinese-first UI atlas scope, acceptance and append-only evidence history +- [`goals/PHASE7_1_RECRUIT_LOADOUT_LEVEL_GOAL.md`](goals/PHASE7_1_RECRUIT_LOADOUT_LEVEL_GOAL.md), + [`presentation/PHASE7_1_RECRUIT_LOADOUT_LEVEL_CONTRACT.md`](presentation/PHASE7_1_RECRUIT_LOADOUT_LEVEL_CONTRACT.md) + and [`reviews/PHASE7_1_RECRUIT_LOADOUT_LEVEL_LEDGER.md`](reviews/PHASE7_1_RECRUIT_LOADOUT_LEVEL_LEDGER.md): + current Owner-directed one-equipment-slot and individual-level clarification ## Prototype, Research And Review diff --git a/docs/content/EQUIPMENT_CATALOG.md b/docs/content/EQUIPMENT_CATALOG.md index 2e27aa91..3fd34ef9 100644 --- a/docs/content/EQUIPMENT_CATALOG.md +++ b/docs/content/EQUIPMENT_CATALOG.md @@ -343,7 +343,7 @@ silhouette (`I`); visible held/worn items add `O`; trigger feedback adds `V/S`. | `EQ-257` Schedule Needle | charm; timing | B | Pin one disclosed eight-tick schedule entry; wearer's action waits to resolve immediately before it. | If entry is skipped, action fizzles. | `I/V/S`: radial needle, schedule pin cue, clock prick. | | `EQ-258` Imported Rule Tag | kit; any | B | First enemy using an earlier-region mechanic is Marked for wearer. | Native-region enemies cannot be Marked by wearer that cadence. | `I/O/V/S`: eight-tab tag, reprise eye cue, motif stamp. | | `EQ-259` Boss Phase Slate | kit; any | B | At phase threshold, preserve wearer's reserved target through disclosed form change. | Wearer cannot retarget until action resolves. | `I/O/V/S`: split-phase slate, threshold tether, stone turn. | -| `EQ-260` Counterpair Compass | charm; any | B | After wearer uses two distinct preview response tags, refresh oldest item cooldown. | Repeating response resets pair. | `I/V/S`: two-needle compass, response pips, paired ticks. | +| `EQ-260` Counterpair Compass | charm; any | B | After wearer uses two distinct preview response tags, refresh this item's cooldown. | Repeating response resets pair. | `I/V/S`: two-needle compass, response pips, paired ticks. | ## `EQ-261..EQ-280` - Trigger-Weaving Tools @@ -352,17 +352,17 @@ silhouette (`I`); visible held/worn items add `O`; trigger feedback adds `V/S`. | `EQ-261` Before-After Switch | kit; any | B | Choose in Prepare whether item triggers before target selection or after target lock. | Choice cannot change in battle. | `I/O/V/S`: two-position switch, hook labels, hard click. | | `EQ-262` Once-More Seal | charm; any | B | Repeat first non-damaging item trigger on a different legal target. | Both triggers enter double cooldown. | `I/V/S`: doubled seal, second-target cue, echo stamp. | | `EQ-263` Dormant Gear Bell | charm; any | B | Two cadences with no legal item target retargets item to self if legal. | Self trigger applies Hindered. | `I/V/S`: sleeping gear bell, fallback-self cue, waking chime. | -| `EQ-264` Trigger Queue Comb | charm; any | B | When two items would trigger together, choose left-to-right equipped order. | Second trigger resolves one tick later. | `I/O/V/S`: toothed comb, ordered pips, two clicks. | +| `EQ-264` Trigger Queue Comb | charm; any | B | When wearer's item and one adjacent ally's item would trigger together, choose their left-to-right resolution order. | Second Recruit's trigger resolves one tick later. | `I/O/V/S`: toothed comb, two-Recruit order pips, two clicks. | | `EQ-265` Neighbor Proc Wire | kit; support | B | Wearer's first item trigger arms neighbor's different-tag item. | Armed item cannot trigger on its own condition first. | `I/O/V/S`: clip wire, item-to-item cue, electric snap. | | `EQ-266` Cancel Catch | guard; any | B | Cancelled profession action readies non-damaging item trigger. | Cancel gives no timing/resource refund. | `I/O/V/S`: hooked guard catch, cancel-to-item cue, latch. | | `EQ-267` Fizzle Lantern | kit; any | B | Item fizzle reveals nearest legal target for next profession action. | Next action must use revealed target if legal. | `I/O/V/S`: cracked lamp, fizzle-eye cue, lamp pop. | -| `EQ-268` Cooldown Bridge | charm; any | B | When one item readies, transfer one cooldown tick to another equipped item. | Ready item becomes unavailable one tick. | `I/V/S`: bridge clasp, clock exchange, bridge ping. | +| `EQ-268` Cooldown Bridge | charm; any | B | When this item readies, pull one cooldown tick from an adjacent ally's equipped item. | The ally's item gains that tick and this wearer becomes Hindered. | `I/V/S`: bridge clasp, neighbor clock exchange, bridge ping. | | `EQ-269` Shared Trigger Cup | kit; support | B | Effective heal lets recipient satisfy wearer's next on-heal equipment condition. | Wearer cannot satisfy it directly that cadence. | `I/O/V/S`: linked cup, proxy trigger cue, twin chime. | | `EQ-270` Marked Proc Quill | charm; any | B | Equipment effects on Marked target occur before its tag-expiry hooks. | Effect removes Mark afterward. | `I/V/S`: eye quill, hook-order cue, page scratch. | -| `EQ-271` Opposite Trigger Ring | charm; any | B | Direct-hit item proc also readies next area-trigger item, or reverse. | Same-family trigger chain is prohibited and Hinders wearer. | `I/V/S`: dot/ring token, cross-family arrow, two-tone ping. | +| `EQ-271` Opposite Trigger Ring | charm; any | B | Wearer's direct-hit item proc readies the nearest ally's area-tag item, or the reverse pairing. | Same-family pairing is prohibited and Hinders wearer. | `I/V/S`: dot/ring token, cross-Recruit arrow, two-tone ping. | | `EQ-272` Three-Source Locket | charm; any | B | Profession, trait, and equipment triggers in one cadence grant one Focus. | Duplicate source resets count. | `I/V/S`: three-compartment locket, source pips, triple click. | | `EQ-273` Triggerless Knife | armament; pressure | B | If no item triggered for three basics, third basic applies Exposed. | Any item trigger clears count. | `I/O/V/S`: plain knife, empty-clock count, dry cut. | -| `EQ-274` Delayed Item Strap | garb; any | B | Store first automatic item trigger and release on wearer command condition set in Prepare. | Stored trigger blocks all other item triggers. | `I/O/V/S`: strapped token, held-proc cue, buckle release. | +| `EQ-274` Delayed Item Strap | garb; any | B | Store this item's first automatic trigger and release it on a wearer condition set in Prepare. | While stored, the wearer's signature action cannot begin. | `I/O/V/S`: strapped token, held-proc cue, buckle release. | | `EQ-275` Source Filter | charm; any | B | Choose profession, trait, or enemy as only legal source for on-status item trigger. | Other-source statuses never trigger it. | `I/V/S`: three-slot filter, chosen source cue, filter turn. | | `EQ-276` Two-Battle Spring | kit; any | B | Unused once-per-battle trigger starts next battle ready with priority. | If used next battle, wearer begins Hindered. | `I/O/V/S`: double-coil spring, carryover cue, loaded twang. | | `EQ-277` Death-Proof Queue | charm; support | B | If item target dies during trigger, queue transfers to nearest living neighbor. | Transferred trigger becomes non-damaging. | `I/V/S`: queue bead, neighbor branch cue, bead slide. | @@ -416,8 +416,8 @@ Their rule transformations are strong but never universally correct. | `EQ-312` Last-Action Seal | charm; any | B | On declared retreat, repeat wearer's last non-damaging action before exit. | Retreat takes one tick longer. Unlock: retreat with all survivors twice. | `I/V/S`: closing card, exit replay cue, low stamp. | | `EQ-313` Neighborless Mantle | garb; any | B | With no living neighbor, wearer gains reach and can self-share resources. | Receiving a neighbor removes all stored resource. Unlock: survive alone for ten cadences. | `I/O/V/S`: open-side mantle, lone-band cue, cloth flare. | | `EQ-314` Crowded-Line Tabard | garb; any | B | With two neighbors, item triggers can choose either side regardless of facing. | Direct enemy targeting is restricted to foremost. Unlock: win at maximum deployment. | `I/O/V/S`: three-panel tabard, two-side selector, fabric snap. | -| `EQ-315` No-Item Oath | charm; any | B | Disable all other equipment effects; profession basic gains one alternate legal target rule. | This item occupies the only active equipment hook. Unlock: win one region without item triggers. | `I/V/S`: blank oath medallion, disabled-slot cue, solemn click. | -| `EQ-316` Item Chorus Frame | kit; any | B | Three different equipped trigger tags in one battle schedule a non-damaging repeat of oldest. | All three items gain cooldown. Unlock: activate eight distinct trigger tags. | `I/O/V/S`: three-hook frame, chorus pips, layered clicks. | +| `EQ-315` No-Item Oath | charm; any | B | This item has no trigger; profession basic gains one alternate legal target rule. | It occupies the Recruit's one equipment slot and supplies no item-trigger event. Unlock: win one region without item triggers. | `I/V/S`: blank oath medallion, empty-hook cue, solemn click. | +| `EQ-316` Item Chorus Frame | kit; any | B | Three different allied equipped-item trigger tags in one battle schedule a non-damaging repeat of the oldest. | The three contributing allied items each gain cooldown. Unlock: activate eight distinct trigger tags. | `I/O/V/S`: three-Recruit frame, chorus pips, layered clicks. | | `EQ-317` Exact Collapse Ring | charm; reorder | B | Choose whether first allied defeat collapses from front or rear direction. | Choice locks in Prepare and may expose opposite end. Unlock: review 20 collapse causes. | `I/V/S`: split-direction ring, collapse selector, stone slide. | | `EQ-318` Counterfactual Slate | kit; any | B | Training replay may swap this item for one cataloged alternative in the saved snapshot. | Replay grants no reward and slate has no live-battle trigger. Unlock: complete ten training comparisons. | `I/O/V/S`: two-column slate, A/B cue, chalk switch. | | `EQ-319` Eight-Region Compass | charm; any | B | Choose one mastered region tag; first matching enemy rule Marks its source. | Other region-rule sources prefer wearer. Unlock: clear all eight regions. | `I/V/S`: eight-point compass, chosen motif cue, radial chime. | diff --git a/docs/content/RECRUIT_GENERATION.md b/docs/content/RECRUIT_GENERATION.md index fd40abbb..e3aecdf1 100644 --- a/docs/content/RECRUIT_GENERATION.md +++ b/docs/content/RECRUIT_GENERATION.md @@ -15,7 +15,7 @@ display_name_tokens, pronoun_set, voice_effort_set body_frame, face_shape, skin_palette, hair_shape, hair_palette garment_cut, garment_palette, profession_overlay, detail_mark base_profession_id, attribute_vector, trait_id -starting_equipment_ids[], level, mastery_by_profession +starting_equipment_id, level, xp, mastery_by_profession quality_band, shop_cost_tier, duplicate_signature survival_battles, knockouts, scars[], acquired_tick ``` @@ -175,9 +175,9 @@ owns each actual rule and eligibility condition. |---|---|---:|---| | Opening | `EQ-001..EQ-040` | 1 | Must be legal for the rolled profession and cannot repeat another opening offer's item while alternatives exist | | Early | `EQ-001..EQ-080` | 1 | At least one offer per shop has a behavior-class `B` item | -| Middle | `EQ-041..EQ-200` | 1-2 | Two-item starts must expose any trigger conflict in preview | -| Late | `EQ-121..EQ-280` | 2 | Cannot begin with a catalog-declared self-conflict | -| Mastery/postgame | `EQ-161..EQ-320` | 2 | `EQ-281..EQ-320` appear only after their visible unlock deed | +| Middle | `EQ-041..EQ-200` | 1 | Item must expose its behavior hook and wearer tradeoff in preview | +| Late | `EQ-121..EQ-280` | 1 | Item must be legal for the rolled Profession and current ruleset | +| Mastery/postgame | `EQ-161..EQ-320` | 1 | `EQ-281..EQ-320` appear only after their visible unlock deed | Starting gear is part of the displayed price. Removing it before purchase is not allowed; after purchase, normal equip and salvage rules apply. @@ -186,9 +186,9 @@ not allowed; after purchase, normal equip and salvage rules apply. | Quality | Visible mark | Generation boundary | Cost consequence | Duplicate boundary | |---|---|---|---|---| -| Common | one notch | Even, Pointed, Durable, or Precise; one starter item | baseline cost tier | exact combat signature may recur after three other offers | -| Uncommon | two notches | Any non-Volatile template; one or two starter items | +1 tier | same profession+trait suppressed while a legal alternative exists | -| Rare | three notches | Includes Volatile; two starter items, one must be `B` | +2 tiers | exact combat signature once per run unless pool exhaustion is shown | +| Common | one notch | Even, Pointed, Durable, or Precise; one starter item from the common eligible band | baseline cost tier | exact combat signature may recur after three other offers | +| Uncommon | two notches | Any non-Volatile template; one starter item with a disclosed behavior hook | +1 tier | same profession+trait suppressed while a legal alternative exists | +| Rare | three notches | Includes Volatile; one behavior-class `B` starter item from the rare eligible band | +2 tiers | exact combat signature once per run unless pool exhaustion is shown | There are no higher recruit rarities. Quality does not grant a universal stat multiplier. Starting level is `max(1, region_floor - 1)` through diff --git a/docs/content/TRAIT_CATALOG.md b/docs/content/TRAIT_CATALOG.md index 52bf4dac..e92980b7 100644 --- a/docs/content/TRAIT_CATALOG.md +++ b/docs/content/TRAIT_CATALOG.md @@ -30,7 +30,7 @@ changes offer weights. | `TR-017` | Exacting | The first action each battle gains +1 reach only for completion validation against its reserved target; movement that remains inside that reach follows the same unit ID and records `TARGET_MOVED_STILL_LEGAL` on its terminal outcome. | If that reservation instead emits `AO_INVALID_REACH`, `AO_INVALID_UNTARGETABLE`, `AO_INVALID_FALLEN`, or `AO_INVALID_FILTER`, its policy is forced to `fizzle`, then emits `AO_FIZZLED` with zero resource or cooldown refund; it cannot retarget or ground-resolve. | Reservation frame shows the one-use extended reach; results show the invalid record, `AO_FIZZLED`, or the followed-target detail and accepted wager. | Ground-resolve equipment; `PF-A10`. | | `TR-018` | Thin Blood | Taking area damage advances next action by one tick. | Direct single-target healing is less effective. | Area-hit speed notch and cracked-drop badge; report quantifies lost care. | Area immunity equipment; `PF-B07`. | | `TR-019` | Backwatch | First enemy entering the allied rear is immediately Marked. | While no rear intruder exists, recruit's forward reach is reduced by one. | Rear eye badge; reach preview updates; intrusion mark logged. | `PF-B05`; reach-1 professions. | -| `TR-020` | Oathbound Tool | Choose one equipped item at battle start; its first trigger gains priority over profession action. | Other item triggers wait until the chosen trigger resolves or expires. | Chosen-slot ribbon and queue state; blocked triggers have reason. | No-trigger equipment sets; `PF-A24`. | +| `TR-020` | Oathbound Tool | The Recruit's equipped item's first trigger gains priority over its profession action. | Until that trigger resolves or expires, the Recruit's first technique waits. | One-slot oath ribbon and queue state; delayed technique has a reason. | Triggerless equipment; `PF-A24`. | | `TR-021` | Loose Formation | Gains one local resource after swapping with a different profession role. | Repeating a swap partner applies Hindered to self. | Partner-role bead track; repeat warning in formation preview. | `PF-H04`; swap-lock equipment. | | `TR-022` | Stone Count | Third hit from the same enemy grants Guard and clears its Mark. | Changing attackers resets count. | Attacker portrait with three stones; reset shown on target change. | Forced-target equipment; `PF-A01`. | | `TR-023` | Borrowed Pace | After receiving an action advance, copies the giver's cadence for one basic. | If copied cadence is slower, cannot be advanced again until that basic. | Donor-color-independent bar shape; report records copied band. | `PF-A21`; cadence-fixed equipment. | diff --git a/docs/goals/PHASE7_1_RECRUIT_LOADOUT_LEVEL_GOAL.md b/docs/goals/PHASE7_1_RECRUIT_LOADOUT_LEVEL_GOAL.md new file mode 100644 index 00000000..32fe85c9 --- /dev/null +++ b/docs/goals/PHASE7_1_RECRUIT_LOADOUT_LEVEL_GOAL.md @@ -0,0 +1,54 @@ +# Phase 7.1 Recruit Loadout And Level Clarity Goal + +## Owner Decision + +Every Recruit has exactly one active equipment slot and an individual level. +The player should be able to read both facts wherever a Recruit is evaluated, +ordered, equipped or reviewed. + +## Baseline + +`afc243a0071303f56ab99486fcabbbb7ae421c2d` + +## Owned Scope + +- Replace the inherited three-slot equipment model with one universal active + equipment slot per Recruit. +- Repair content rows that require multiple items on one Recruit so they remain + executable through one item, a Profession/Trait event or an allied Recruit's + one item. +- Preserve exactly 320 equipment identities and their behavior-first design. +- Keep Recruit level and XP individual, run-scoped and capped at level 10. +- Show level and the one current equipment slot in the relevant Phase 7 HTML + Recruit surfaces. +- Rebuild and verify the 50-page self-contained atlas. + +## Frozen + +- Profession, Trait, equipment, Artifact, enemy, boss and Region counts. +- Battle geometry, casualty permanence, campaign structure and economy values + unrelated to equipment-slot reconciliation. +- Godot/runtime code, audio, new generated art, device, packaging, stores and + release. + +## Acceptance + +1. Product, systems, economy, Recruit generation, save, presentation and + content authorities consistently state one equipment slot per Recruit. +2. No Recruit may own or activate two equipped items at once. +3. Replacing equipment returns the prior item to run inventory atomically. +4. The 320-row catalog contains no effect that requires multiple equipped items + on one Recruit. +5. Individual level, current XP progress and current equipment are visible in + Recruit inspect; compact Recruit surfaces show at least level and equipment + state without relying on color. +6. Static design validation and the complete R7 browser suite pass from one + fixed clean/upstream source. + +## Legal Terminal + +`Aetherbound Guild Phase 7.1 loadout and level clarity candidate ready` + +or + +`Aetherbound Guild Phase 7.1 redesign required: ` diff --git a/docs/presentation/ART_ASSET_AND_ANIMATION_CATALOG.md b/docs/presentation/ART_ASSET_AND_ANIMATION_CATALOG.md index a42b47f2..1c2c1af5 100644 --- a/docs/presentation/ART_ASSET_AND_ANIMATION_CATALOG.md +++ b/docs/presentation/ART_ASSET_AND_ANIMATION_CATALOG.md @@ -78,7 +78,7 @@ be replaced only by the corresponding stable IDs from the merged authorities. | `CONTENT.PROFESSION.ADVANCED[01..24]` | Twenty-four advanced profession rows; two resolve to each base row. | | `CONTENT.PROFESSION.HIDDEN[01..06]` | Six hidden hybrid profession rows and their prerequisite pairings. | | `CONTENT.TRAIT[01..36]` | Thirty-six trait rows, each exposing benefit, cost, trigger, and visibility. | -| `CONTENT.EQUIPMENT[001..320]` | Three hundred twenty equipment rows, including slot, behavior, anchor need, and manifestation class. | +| `CONTENT.EQUIPMENT[001..320]` | Three hundred twenty equipment rows, including form, one-slot behavior, anchor need, and manifestation class. | | `CONTENT.ARTIFACT[01..60]` | Sixty shared-rule artifact rows and their trigger/conflict states. | | `CONTENT.ENEMY[001..100]` | One hundred ordinary or elite enemy identities, including tier and region. | | `CONTENT.BOSS[01..16]` | Sixteen boss identities, including region, action set, and phase needs. | @@ -89,7 +89,7 @@ be replaced only by the corresponding stable IDs from the merged authorities. | `UX.SCREEN.*` | Player-facing screen and state IDs requiring art support. | Unresolved bindings remain visible in the manifest. Art production cannot -silently assign a profession mechanic, item slot, enemy tier, region placement, +silently assign a profession mechanic, item form, enemy tier, region placement, screen count, or battle formula. ### 2.2 Stable Art IDs @@ -258,8 +258,8 @@ view from stored/runtime state without creating another option authority. | `base_profession_id` | Content profession identity | Must resolve to `ART-PRO-B01..B12` at generation through the profession catalog | Rules, text, and initial overlay use one profession binding; art cannot select a different kit. | | `attribute_vector` | Five content grades | No body, face, palette, or garment binding | Engine-rendered grades and sourced comparison markers only. | | `trait_id` | One of 36 traits | `ART-TRT-01..36` from Section 6 | Compound emblem and bounded event badge; never changes the stored visual recipe. | -| `starting_equipment_ids[]` | Content equipment IDs | `ART-EQP-001..320` plus declared attachment points | Shop/result objects and battle attachments derive from equipment ownership. | -| `level` | System/content progression value | No visual option | Engine-rendered level and sourced condition/readiness states only. | +| `starting_equipment_id` | One content equipment ID | `ART-EQP-001..320` plus its declared attachment point | Shop/result objects and the single battle attachment derive from equipment ownership. | +| `level`, `xp` | Individual system/content progression values | No visual option | Engine-rendered `LV 1..10` and exact current/next XP progress; never a Company-wide level. | | `mastery_by_profession` | System progression map | No implicit overlay change | UI progress output; a profession overlay changes only through an authoritative profession-state update. | | `quality_band` | Disclosed content band | No palette, material, body, or effect binding | Engine-rendered band notation only. | | `shop_cost_tier` | Economy-derived tier | No art option | Engine-rendered cost treatment around the same portrait assembly. | diff --git a/docs/presentation/PHASE7_1_RECRUIT_LOADOUT_LEVEL_CONTRACT.md b/docs/presentation/PHASE7_1_RECRUIT_LOADOUT_LEVEL_CONTRACT.md new file mode 100644 index 00000000..78bfa200 --- /dev/null +++ b/docs/presentation/PHASE7_1_RECRUIT_LOADOUT_LEVEL_CONTRACT.md @@ -0,0 +1,33 @@ +# Phase 7.1 Recruit Loadout And Level Contract + +## Equipment Rule + +- Each living Recruit owns zero or one `equipped_item_id`. +- Item forms such as armament, guard, garb, charm and kit are eligibility and + presentation families, not simultaneous body slots. +- Equip is a replacement transaction: candidate enters the one active slot and + the prior item, if any, returns to run inventory in the same receipt. +- An item can interact with a neighbor's item or a shared event, but no effect + may require its wearer to hold a second item. +- Artifacts remain run-wide rules and never consume the Recruit's item slot. + +## Level Rule + +- Every Recruit stores its own `level` and `xp`. +- Level range is `1..10`; XP is earned only by the living deployed Recruit who + qualifies under the economy authority. +- Reserve and Fallen Recruits gain no battle XP. +- Hiring, dismissal, death, promotion and run closure never transfer level or + XP to another Recruit. +- Promotion preserves that Recruit's level and XP; it does not create a shared + Company level. + +## UI Read + +- Offer, Roster, Recruit detail, Equipment, Party and Result surfaces identify + level as text. +- Recruit detail exposes exact current XP/next-level progress. +- Equipment comparison shows exactly `Current slot -> Candidate`, the returned + item and the behavior delta. +- Empty slot, incompatible item and replacement states remain distinguishable + without color. diff --git a/docs/presentation/SCREEN_AND_STATE_MAP.md b/docs/presentation/SCREEN_AND_STATE_MAP.md index 7d558d91..30dccb5f 100644 --- a/docs/presentation/SCREEN_AND_STATE_MAP.md +++ b/docs/presentation/SCREEN_AND_STATE_MAP.md @@ -47,8 +47,8 @@ UI must not substitute a guessed number, price, formula, or probability. | `BIND:RECRUIT_TRANSACTION` | price, capacity effect, receipt | recruitment/economy | review skeleton with no commit | | `BIND:DISMISSAL_ELIGIBILITY` | total live Recruits across deployed and reserve, operation restrictions, and disabled reason | recruitment/core systems | dismissal remains disabled until eligibility is verified | | `BIND:DISMISSAL_OUTCOME` | retained, returned, or lost value | recruitment/economy | consequence field marked unresolved | -| `BIND:RECRUIT_SCHEMA` | name, profession, trait, attributes, equipment, level, history | content/core systems | field labels remain; missing fields show `Pending` | -| `BIND:EQUIPMENT_RULES` | slots, eligibility, conflicts, equip result | equipment/content | incompatible reason requires source data | +| `BIND:RECRUIT_SCHEMA` | name, profession, trait, attributes, one equipped item or null, individual level/XP, history | content/core systems | field labels remain; missing fields show `Pending` | +| `BIND:EQUIPMENT_RULES` | one universal slot, forms, eligibility, conflicts, replacement result | equipment/content | incompatible reason requires source data | | `BIND:ARTIFACT_RULES` | active shared modifiers, acquisition, conflicts, and lifecycle | artifacts/core systems | ledger remains read-only until resolved | | `BIND:PARTY_CAPACITY` | current simultaneous deployment and growth | progression/simulation | count area displays binding, never a guessed cap | | `BIND:PARTY_ORDER_EFFECTS` | exposure, reach, protection, movement, replacement | battle/core systems | preview lists only sourced effects | @@ -224,7 +224,7 @@ Required invalid and interruption states: | `HUB-001` Guild desk / `NAV` | Show current pressure, preparation readiness, latest outcome, resources, and all preparation destinations. | Open Risk Board when ready; navigation itself changes nothing. | First run uses a compact opening decision prompt. Loading each status region leaves destinations usable. | Missing mandatory binding blocks only its dependent destination and names the owner. No confirmation. | Suspend records no new transaction. Child pages return here with the changed field highlighted. | | `HUB-002` Forecast / `LIST` | Explain upcoming known threats, opportunities, deadline, and information confidence. | Track pressure; changes the Guild desk summary only. | No optional forecast still shows the mandatory next pressure. Loading preserves last verified forecast with timestamp. | Stale or incomplete forecast labels uncertainty instead of inventing certainty. Tracking needs no confirmation. | Suspend preserves filters. Back returns to Guild desk. | | `SHP-001` Shop / `LIST` | Compare `BIND:SHOP_OFFERS`, affordability, roster/equipment fit, and refresh consequence. | Inspect offer; purchase is never performed from the list row. | Sold out explains next sourced restock. No matching filter offers Clear Filters. Loading keeps balances visible and rows inert. | Stale, duplicate, or unavailable offers refresh before review. Refresh and any paid action require exact cost confirmation. | Suspend cancels no verified purchase and polls receipts on resume. Back returns to Guild desk. | -| `SHP-002` Recruit review / `SELECT` | Evaluate generated name, profession, trait benefit/cost, rolled attributes, equipment eligibility, level, survival history, price, and party impact. | Recruit; commits `BIND:RECRUIT_TRANSACTION` after review. | Missing roster comparison states which binding is pending. Loading shows stable recruit identity, never a substitute recruit. | Offer expired, insufficient value, invalid capacity, or duplicate receipt keeps the prior state. Confirmation states exact recruit, cost, capacity effect, and undo policy. | Suspend after commit checks receipt before retry. Cancel returns to Shop on the same stable offer ID. | +| `SHP-002` Recruit review / `SELECT` | Evaluate generated name, profession, trait benefit/cost, rolled attributes, one starting item, individual level/XP, survival history, price, and party impact. | Recruit; commits `BIND:RECRUIT_TRANSACTION` after review. | Missing roster comparison states which binding is pending. Loading shows stable recruit identity, never a substitute recruit. | Offer expired, insufficient value, invalid capacity, or duplicate receipt keeps the prior state. Confirmation states exact Recruit, level, starting item, cost, capacity effect, and undo policy. | Suspend after commit checks receipt before retry. Cancel returns to Shop on the same stable offer ID. | | `SHP-003` Item offer review / `SELECT` | Explain item behavior, eligible recruits, owned conflicts, price, and likely party effect. | Buy; adds the item according to sourced inventory rules. | No eligible recruit is informative, not an error. Loading retains item identity and offer price timestamp. | Expired, unaffordable, full-policy, or duplicate claim states preserve balance. Confirmation lists cost, destination, and conflict. | Suspend reconciles the transaction receipt. Cancel returns to Shop on the offer. | | `REC-001` Roster / `LIST` | Scan all cumulative Recruits, deployed status, profession, trait, readiness, equipment issue, and survival history. | Inspect Recruit; changes nothing. | Zero live Recruits is shown only while resolving casualty recovery or Company collapse; it routes to the sourced recovery/closure state, never an ordinary Shop. No filter match offers Clear Filters. Loading uses stable roster order. | Binding failure preserves last verified summary and blocks dependent edits. No confirmation. | Suspend preserves selection and filters. Back returns to Guild desk or the required recovery/closure screen. | | `REC-002` Recruit detail / `NAV` | Read one Recruit's sourced identity, mechanical history, current role, equipment, position, and legal actions. | Open Equip or Party Line; navigation changes nothing. Dismiss is a separate destructive action. | Missing optional history states `No recorded battles`. Loading keeps name, stable ID, and the last verified `BIND:DISMISSAL_ELIGIBILITY`. | Deleted/stale Recruit returns to Roster with explanation. When total live Recruits across deployed and reserve equals one, Dismiss remains visible but disabled with `One member must remain`; its explanation directs the player to the separate Run Abandonment Review. | Suspend preserves the selected tab. Back returns to Roster and its prior scroll position. | @@ -234,8 +234,8 @@ Required invalid and interruption states: | ID / screen / input | Purpose | Primary action and consequence | Empty / loading | Error / confirmation | Interruption / return | |---|---|---|---|---|---| -| `EQP-001` Equipment workbench / `LIST` | Equip a selected recruit while showing eligible inventory and sourced behavior changes. | Equip selected item; changes loadout only after comparison review. | No owned items points to Shop. No eligible item explains profession/slot requirements. Loading retains current loadout. | Incompatible, already used, stale inventory, or ruleset error blocks commit with reason. Confirmation states replaced item, conflicts, and before/after behavior. | Suspend before receipt keeps the old loadout; resume reconciles receipt. Back returns to Recruit Detail or Party Line caller. | -| `EQP-002` Item comparison / `SELECT` | Compare current and candidate item in the vocabulary used by party preview, battle, and results. | Apply candidate; returns to workbench with changed effects highlighted. | Empty candidate keeps current item and offers Choose Item. Loading never clears current values. | Unknown derived effect is labeled unresolved and blocks Apply when consequence-critical. Confirmation lists all sourced changes and affected saved plan references. | Suspend keeps comparison but performs no equip. Back returns to Equipment with selection intact. | +| `EQP-001` Equipment workbench / `LIST` | Equip a selected Recruit while showing individual level/XP, its one universal slot, eligible inventory and sourced behavior changes. | Equip selected item; atomically returns the prior item or null and changes the one slot only after comparison review. | No owned items points to Shop. No eligible item explains Profession/form requirements. Loading retains the current slot. | Incompatible, already used, stale inventory, or ruleset error blocks commit with reason. Confirmation states the returned prior item, one-slot result, conflicts, and before/after behavior. | Suspend before receipt keeps the old item; resume reconciles receipt. Back returns to Recruit Detail or Party Line caller. | +| `EQP-002` Item comparison / `SELECT` | Compare the one current item and candidate in the vocabulary used by party preview, battle, and results. | Apply candidate; returns to workbench with changed effects and returned prior item highlighted. | Empty candidate keeps current item and offers Choose Item. Empty current slot is explicit. Loading never clears current values. | Unknown derived effect is labeled unresolved and blocks Apply when consequence-critical. Confirmation lists the exact old/new item IDs and affected saved plan references. | Suspend keeps comparison but performs no equip. Back returns to Equipment with selection intact. | | `ART-001` Active artifact ledger / `LIST` | Inspect run-wide artifact rules, acquisition source, interactions, conflicts, and lifecycle without treating them as recruit equipment. | Inspect artifact; changes nothing unless `BIND:ARTIFACT_RULES` supplies a separate legal action. | No active artifacts explains their sourced acquisition route. Loading retains verified active rules. | Missing or conflicting artifact references identify stable IDs and block dependent commitment. Any lifecycle-changing action requires a separate sourced confirmation. | Suspend preserves selection. Back returns to Guild desk or the Reward caller. | | `PTY-001` Party line / `ORDER` | Select simultaneous Recruits on one visual line from protected rear at left to exposed front at right, with position `0` at the front and larger numbers extending leftward; preview sourced targeting, protection, reach, healing, movement, and replacement changes. | Save order; changes the prepared party snapshot but does not start battle. | Zero live Recruits is a casualty/Company-collapse settlement state and routes to sourced recovery or run close; ordinary dismissal never creates it. No eligible deployable Recruit explains the recovery or roster reason. Loading preserves last committed order and rear-left/front-right endpoints. | Invalid count, duplicate Recruit, incompatible state, or stale capacity identifies the exact numbered position. Confirmation is required only when removing a currently prepared Recruit has a sourced consequence. | Suspend stores only the last committed order; uncommitted moves are offered to Restore or Discard. Back returns to Guild desk. | | `PTY-002` Readiness review / `SELECT` | Summarize the frozen rear-left/front-right order, position numbers, protection contributors toward the right/front, equipment issues, known pressure coverage, Reserve state, and `BIND:PARTY_CAPACITY`. | Accept preparation; records a reviewable battle-plan snapshot. | Empty party blocks acceptance and points to Add Recruits. Loading verifies all stable IDs without reversing visual order. | Invalid or outdated effect opens the owning edit screen. Confirmation states warnings that are allowed but does not overstate outcome prediction. | Suspend preserves the last accepted snapshot. Back returns to Party Line. | diff --git a/docs/presentation/UI_AND_VISUAL_SYSTEM.md b/docs/presentation/UI_AND_VISUAL_SYSTEM.md index 8ad58e64..4e3fe6e8 100644 --- a/docs/presentation/UI_AND_VISUAL_SYSTEM.md +++ b/docs/presentation/UI_AND_VISUAL_SYSTEM.md @@ -142,9 +142,9 @@ readable silhouette. | Decision surface | Primary | Secondary | Deferred to inspect | |---|---|---|---| | Guild desk | upcoming pressure, latest change, preparation readiness | sourced resources and open decisions | complete run history | -| New run setup | four stable provisional identities, Profession job, Trait benefit/cost, selected count | rolled distribution, default loadout, Contract/difficulty | unrelated catalog and future progression | +| New run setup | four stable provisional identities, Profession job, Trait benefit/cost, selected count | individual level, one starting item, rolled distribution, Contract/difficulty | unrelated catalog and future progression | | Shop | offer behavior, price, immediate fit | future eligibility and refresh consequence | full discovery provenance | -| Recruit review | profession responsibility, trait benefit/cost, rolled constraint | equipment eligibility, level, survival history | full attribute derivation | +| Recruit review | profession responsibility, trait benefit/cost, rolled constraint | individual level/XP, one current item, survival history | full attribute derivation | | Dismissal/abandonment | eligibility and affected scope | returned equipment, rebate or run-close consequence | unrelated Recruit history | | Equipment | changed behavior, wearer, replacement, conflict | secondary derived effects and acquisition | unrelated collection record | | Party line | position, exposure, target/protection/reach change | alternate order and unresolved warning | cosmetic identity detail | @@ -326,7 +326,8 @@ focuses it and opens no destructive state. Controller focus enters offers in stable source order; Select toggles the focused offer, while Inspect opens and closes its full details without changing selection. Compare is a separate command: it pins offer `A`, then offer `B`, and opens an aligned Profession, -Trait, attribute, and default-loadout comparison. The A/B pair may use any two +Trait, attribute, individual level, and one-starting-item comparison. The A/B +pair may use any two offer IDs and never changes the two Company checkboxes. The selected-count component has fixed dimensions and explicit states: @@ -366,9 +367,9 @@ The component has four canonical scales supplied by | Scale | Required reads | |---|---| -| Marker | stable portrait/silhouette, profession emblem, trait seal, condition | -| Roster | generated name, profession, trait tradeoff, readiness, deployed state | -| Planning figure | full silhouette, equipment read, order number, health/condition | +| Marker | stable portrait/silhouette, profession emblem, trait seal, level, condition | +| Roster | generated name, profession, level, one-item state, trait tradeoff, readiness, deployed state | +| Planning figure | full silhouette, level, one equipped-item read, order number, health/condition | | Battle actor | profession/tool silhouette, target/protection relation, health, urgent status, death/exit | The generated name and stable ID are text. Profession is never inferred from @@ -378,14 +379,15 @@ marks and never becomes a fixed personal-story track. ### 8.6 Equipment Row And Comparison -An item row shows object, name, equipment position from `BIND:EQUIPMENT_RULES`, -current wearer, one behavior sentence, eligibility, and relevant sourced delta. -Comparison aligns current and candidate by mechanical field; unchanged fields -remain visible but quiet. Conflicts name the other item or rule. +An item row shows object, name, form from `BIND:EQUIPMENT_RULES`, current wearer, +one behavior sentence, eligibility, and relevant sourced delta. Every Recruit +has one universal slot; form does not create another slot. Comparison aligns +current and candidate by mechanical field; unchanged fields remain visible but +quiet. Conflicts name the wearer, Profession, position or shared rule. -Apply does not enable until replacement, returned item, invalidated reference, -and reversibility are known. Raw power score, rarity, or price is never the sole -or largest comparison read. +Apply does not enable until the one-slot replacement, returned prior item or +null, invalidated reference, and reversibility are known. Raw power score, +rarity, or price is never the sole or largest comparison read. ### 8.7 Dismissal And Run Abandonment diff --git a/docs/product/ECONOMY_AND_BALANCE.md b/docs/product/ECONOMY_AND_BALANCE.md index 62b6b28d..0cb57e7b 100644 --- a/docs/product/ECONOMY_AND_BALANCE.md +++ b/docs/product/ECONOMY_AND_BALANCE.md @@ -54,7 +54,8 @@ A run begins with: - `52 Coin`; - zero Replacement Credit; - Line capacity four; -- no equipment beyond the provisional Profession defaults; +- exactly one starting item on each provisional Recruit and no extra equipped + slots; - no active Artifact. The first Market contains at least two base-Profession Recruit offers at level @@ -410,10 +411,12 @@ uses the same persistent breadth already owned. ## 10. Equipment And Artifact Distribution The 320 equipment entries are divided into eight Region bands of 40. Every band -contains rows for all three equipment slots, all primary responsibilities, and -at least three opposed order/reach/protection plans. Tier describes price and -availability, not strict power. A lower tier must lead one fixed comparison in -which it is the rational choice. +contains all five item forms (armament, guard, garb, charm, and kit), all +primary responsibilities, and at least three opposed order/reach/protection +plans. Every Recruit has one universal active slot, so form controls eligibility +and presentation rather than granting simultaneous slots. Tier describes price +and availability, not strict power. A lower tier must lead one fixed comparison +in which it is the rational choice. Each item has a numeric support budget separate from its required behavior: @@ -432,9 +435,9 @@ may be generic HP, power, Guard, or Ward. Remaining numeric support uses: | +1 Potency or Tenacity | 1 | | +0.20 Tempo or Initiative | 1 | -Fractions sum across equipped items before derived-stat rounding. Region band -does not increase this budget; later bands introduce new interactions rather -than automatic vertical power. +The one equipped item's numeric support is added before derived-stat rounding. +Region band does not increase this budget; later bands introduce new +interactions rather than automatic vertical power. All 320 entries pass the systems behavior fixture. No scalar-only allowance or minimum subset exists. diff --git a/docs/product/GAME_PRODUCT_CONTRACT.md b/docs/product/GAME_PRODUCT_CONTRACT.md index 59d87ddc..0946c2e1 100644 --- a/docs/product/GAME_PRODUCT_CONTRACT.md +++ b/docs/product/GAME_PRODUCT_CONTRACT.md @@ -103,7 +103,7 @@ same meaning. | Term | Definition | |---|---| | Guild | Persistent profile containing unlock breadth, region progress, records, settings, and collection knowledge | -| Recruit | One generated participant in the current run, built from a generated name, modular appearance seed, fixed profession, rolled attributes, exactly one Trait, equipment, level, and survival history | +| Recruit | One generated participant in the current run, built from a generated name, modular appearance seed, fixed profession, rolled attributes, exactly one Trait, zero or one equipped item, an individual level/XP record, and survival history | | Profession | One fixed reusable combat ruleset from the authored catalog | | Trait | One short mechanical modifier with a visible benefit and cost; each Recruit has exactly one | | Party line | The ordered simultaneous deployment from front to rear | @@ -293,11 +293,16 @@ playable state. Ordinary New Run remains available after either choice. - All rolled attribute totals at a given offer level use the same budget; distribution, Profession, Trait, order, and price create value differences. - A Recruit has exactly one Trait for its entire lifetime. +- A Recruit has exactly one universal equipment slot. Item form is eligibility + and presentation, not an additional simultaneous slot; equipping a candidate + atomically returns the previous item, if any, to run inventory. +- Level and XP belong to one Recruit, are bounded to levels `1..10`, and never + become a shared Company level or transfer to another Recruit. - Dismissal is legal only in a Market when the canonical `live_recruit_count >= 2`, returns equipment, removes the Recruit, and requires review when survival history is nonzero. Preview and commit both revalidate the count. -- Death never selects or destroys equipped items; they return after outcome. +- Death never selects or destroys the equipped item; it returns after outcome. - Death grants no same-battle respawn, resurrection timer, extra life, or automatic Reserve substitution. - Fallen Recruits do not return later in the run through reload, training, @@ -482,7 +487,8 @@ fixed validation fixtures and expected event differences ``` A Recruit offer additionally declares Profession, attribute budget/distribution, -one Trait, level, generated identity seed, list price, and offer provenance. An +one Trait, individual level/XP, one starting item, generated identity seed, +list price, and offer provenance. An opening provisional offer also declares its run creation draft ID, fixed index `0..3`, deterministic offer ID, and canonical output hash. A Profession changes battlefield responsibility or resource/timing/target/order diff --git a/docs/product/SAVE_AND_FAILURE_CONTRACT.md b/docs/product/SAVE_AND_FAILURE_CONTRACT.md index 0add2f12..29ed55ad 100644 --- a/docs/product/SAVE_AND_FAILURE_CONTRACT.md +++ b/docs/product/SAVE_AND_FAILURE_CONTRACT.md @@ -27,8 +27,9 @@ 5. A Fallen Recruit never respawns in the same battle and never returns to the same run through Reserve substitution, reload, backup choice, rehearsal, migration, reward, promotion, difficulty, or clock manipulation. -6. Death removes the Recruit but returns exactly the Recruit's equipped item IDs - to run inventory. Neither half of that result can commit alone. +6. Death removes the Recruit but returns exactly the Recruit's optional + `equipped_item_id` to run inventory. Neither half of that result can commit + alone; an empty slot returns nothing. 7. Offline gameplay advances by zero simulation ticks and grants zero Coin, XP, Renown, offers, healing, or progress. 8. Migration is copy-first, deterministic, sequential, and reversible until a @@ -214,13 +215,13 @@ that lacks a valid target after recovery becomes rejected with no deltas. | Buy Recruit | offer ID, recruit output hash, Credit choice, expected price | Coin/Credit cost, offer removal, Recruit ownership | | Buy/sell equipment | offer/item ID and expected receipt | Coin delta and exactly one ownership change | | Lock/refresh | Market hash, selected locks, expected cost | Coin cost, counters, complete fixed offer board | -| Equip/reorder | Recruit/item/order IDs | complete legal inventory and Party-line state | +| Equip/reorder | Recruit, candidate item, expected prior item or null, and/or order IDs | one-slot replacement receipt, complete legal inventory and Party-line state | | Promote | Recruit, old/new Profession, eligibility proof | one Profession replacement and derived-state refresh | -| Dismiss | Recruit, equipment set, rebate preview, expected `live_recruit_count` | Recruit removal, equipment return, Coin rebate, post-count at least one | +| Dismiss | Recruit, optional equipped item ID, rebate preview, expected `live_recruit_count` | Recruit removal, optional item return, Coin rebate, post-count at least one | | Commit battle | battle offer, lineup, stats, seed, risk | immutable encounter start snapshot | | Start retreat | encounter, accepted tick | one retreat timestamp; no duplicate start | | Lock outcome | objective and final state hashes | immutable victory/defeat/timeout/retreat result | -| Apply casualties | exact Recruit/equipment/Credit set | all Recruit removals, all item returns, all Credit grants | +| Apply casualties | exact Recruit/optional-item/Credit set | all Recruit removals, all present item returns, all Credit grants | | Claim reward | fixed draft/card/disposition | one reward and draft removal | | Close run | terminal cause and persistent grants | Coin/Credit clear, run result, Records/Renown once | | Change difficulty/law | Guild-only selected rule set | next-run configuration only | @@ -379,8 +380,8 @@ Run ownership changes only in the casualty settlement after outcome lock. The pending intent lists, for every casualty: ```text -recruit_id, final Profession/level/Trait/history hash -equipped item IDs +recruit_id, final Profession/level/XP/Trait/history hash +equipped_item_id or null original list price, Coin paid, computed Credit grant death tick and causal event ID ``` @@ -389,7 +390,7 @@ Applied settlement must satisfy: ```text all casualty Recruit IDs absent from living Company and Reserve -all casualty equipment IDs present exactly once in run inventory +every non-null casualty equipped_item_id present exactly once in run inventory Replacement Credit equals the capped sum fallen total increases by exact casualty count no non-casualty Recruit or item changes ownership @@ -745,14 +746,14 @@ receipt and cannot dismiss, grant, overwrite, close, or delete twice. | Pending purchase | crash after pending pointer | zero cost until deterministic applied child; one Recruit after retry | | Applied purchase | duplicate same operation ID | one cost, one Recruit, same receipt | | Refresh crash | crash after Coin validation | old complete board or new complete board and cost; never mixed | -| Legal dismiss duplicate | start with two live Recruits; repeat confirm | one Recruit removal, one equipment return set, one rebate; final live Recruit remains | +| Legal dismiss duplicate | start with two live Recruits; repeat confirm | one Recruit removal, zero-or-one item return, one rebate; final live Recruit remains | | Final dismiss preview | one live Recruit, low Coin, no protected offer | `FINAL_LIVE_RECRUIT`; no operation ID or mutation; `Abandon Run` remains separate | | Stale final dismiss | preview at two, another serialized dismissal leaves one, submit old confirm | zero-delta `FINAL_LIVE_RECRUIT`; no rebate, protected offer, or count change | | Interrupted final dismiss | recover pending intent against active count one | reject without apply/receipt beyond rejected intent response; one Recruit remains | | Battle suspend | suspend during projectile at tick 417 | identical next event/draw counters/final hash | | Death snapshot | crash one tick after front Recruit falls | replay contains same death and collapse | | Outcome crash | crash before casualty settlement | locked result reopens; settlement applies exact set once | -| Casualty partial candidate | omit one equipped item in candidate | validation rejects; no Recruit removal/Credit commits | +| Casualty partial candidate | omit the non-null equipped item in candidate | validation rejects; no Recruit removal/Credit commits | | Reward duplicate | double-tap selected card | one item/Artifact/Coin result and draft removal | | Boss grant duplicate | replay same boss-clear result | one clear fact and Renown grant | | Three deaths | fixed economy fixture | Recruits removed, all equipment returned, 54 Credit capped | diff --git a/docs/product/SYSTEMS_AND_BATTLE.md b/docs/product/SYSTEMS_AND_BATTLE.md index acfc8f88..71450869 100644 --- a/docs/product/SYSTEMS_AND_BATTLE.md +++ b/docs/product/SYSTEMS_AND_BATTLE.md @@ -56,7 +56,8 @@ Recruit recruit_id, generated_name_parts, rendered_name_snapshot, appearance_seed origin_profession_id, current_profession_id, promotion_state level, xp, five base attributes, Trait id - equipment ids, effective stats, survived battles, victories, hire receipt + equipped_item_id or null, effective stats, survived battles, victories, + hire receipt encounter encounter_id, battle_offer_id, root_seed, phase, tick @@ -181,14 +182,16 @@ equipped behavior tags, current Artifact tags, adjacent deployed Profession families, and cleared Contract facts. A predicate cannot read generated name, appearance, playtime, wall clock, or social state. -Promotion preserves level, HP percentage outside battle, Trait, equipment, -survival history, and recruit ID. It replaces current Profession, action table, -resource meter, and derived Profession bases atomically. It is not a free job -switch or reversible respec. +Promotion preserves that Recruit's individual level and XP, HP percentage +outside battle, Trait, equipped item, survival history, and recruit ID. It +replaces current Profession, action table, resource meter, and derived +Profession bases atomically. It is not a free job switch or reversible respec. ### 5.3 Automatic Action Unlocks -A Profession action kit unlocks by Recruit level: +A Profession action kit unlocks by that Recruit's individual level. Levels are +bounded to `1..10`; there is no Company level and another Recruit's XP cannot +unlock these rules: | Recruit level | Available automatic rules | |---:|---| @@ -764,18 +767,21 @@ new hard-control duration by 75%. No actor may be unable to act for more than ## 17. Equipment, Artifacts, And Hooks -Each Recruit has three equipment slots: +Each Recruit has exactly one universal active equipment slot. An item may be +presented as armament, guard, garb, charm, or kit, but those forms describe +eligibility, attachment and visual language rather than simultaneous body +slots. A Recruit therefore owns zero or one `equipped_item_id`. -| Slot | Main scope | -|---|---| -| Weapon | Power source, basic/technique behavior, reach or projectile rule | -| Guard | HP, defense, protection, movement, or death-risk tradeoff | -| Charm | Status, resource, timing, target, healing, or promotion interaction | +Equipping a candidate is one atomic replacement: the candidate leaves run +inventory, the previous item returns to inventory if present, and the Recruit's +single slot points to the candidate. An Artifact is a shared run rule and never +occupies this slot. An item may react to an allied Recruit's item event, but no +effect may require its wearer to hold a second item. All 320 equipment rows must alter a decision and declare one named hook. Scalar -budget may support the hook but cannot be the row's only identity. A Recruit -cannot equip duplicate stable IDs. Company-unique effects declare a unique -group; later copies retain local behavior but show the shared portion dormant. +budget may support the hook but cannot be the row's only identity. One item ID +cannot be owned by two Recruits. Company-unique effects declare a unique group; +later copies retain local behavior but show the shared portion dormant. Artifacts are shared run rules. A run holds at most three active Artifacts. A fourth acquisition requires replacing one or declining the new row after an @@ -807,7 +813,8 @@ At zero HP, a non-summon Recruit becomes Fallen: - it is removed for line collapse; - no timer, item, Profession, Artifact, difficulty rule, or Reserve state can respawn, resurrect, or substitute for it during the encounter; -- its recruit ID and equipment IDs enter the pending outcome casualty set; +- its recruit ID and optional equipped item ID enter the pending outcome + casualty set; - it remains visible in diagnosis with the preceding five seconds of causes. At outcome application, each casualty and all returned equipment commit with @@ -949,7 +956,7 @@ fixed input and expected event output ### 22.2 Equipment Row ```text -equipment_id, slot, acquisition band, list-price inputs +equipment_id, form, acquisition band, list-price inputs eligible Profession/action tags changed decision and named behavior hook stat support budget, trigger, cooldown, recursion @@ -1018,7 +1025,7 @@ expected duration, profile assertion, causal-report requirements | Retreat | confirm at tick 50, no terminal predicate | outcome retreat after tick 70 batch; later duplicate has no effect | | Speed parity | identical battle at 1x and 4x | all authoritative hashes equal | | Timeout | objective incomplete at tick 1200/1500 | Overtime applies, then timeout defeat at tick 1500 | -| Casualty equipment | Fallen Recruit owns three items | Recruit removed and all three item IDs returned exactly once | +| Casualty equipment | Fallen Recruit owns one item | Recruit removed and that item ID returned exactly once; null slot returns nothing | | Final dismissal | one living Recruit across deployed plus Reserve | preview/commit reject `FINAL_LIVE_RECRUIT`; no state or recovery-offer change | | Moved target remains legal | Reserved target changes depth but remains targetable and in reach | same ID receives `AO_APPLIED`; detail `TARGET_MOVED_STILL_LEGAL`; no refund | | Moved target leaves reach | Reserved target moves from distance 2 to 3 against reach 2; policy `fizzle` | `AO_INVALID_REACH` then `AO_FIZZLED`; declared fizzle refund once | diff --git a/docs/reviews/PHASE7_1_RECRUIT_LOADOUT_LEVEL_LEDGER.md b/docs/reviews/PHASE7_1_RECRUIT_LOADOUT_LEVEL_LEDGER.md new file mode 100644 index 00000000..3f6ea21c --- /dev/null +++ b/docs/reviews/PHASE7_1_RECRUIT_LOADOUT_LEVEL_LEDGER.md @@ -0,0 +1,11 @@ +# Phase 7.1 Recruit Loadout And Level Ledger + +Append-only record. Evidence levels remain separate. + +| Date | Revision/run | Result | Evidence and boundary | +|---|---|---|---| +| 2026-08-12 | baseline `afc243a` | `OPEN` | Owner confirmed that every Recruit should have at most one equipment item and an individual level. Existing UI implies one current item, but systems authority still defines three slots and Recruit generation can create two-item starts. Runtime, human, device and release gates remain open. | +| 2026-08-12 | development tree | `RULE_PREFLIGHT_PASS` | `ABG_RECRUIT_LOADOUT_LEVEL_OK equipment_slots=1 level_scope=individual level_cap=10 equipment=320`; complete pre-development validator reports errors `0`. Product, systems, economy, Recruit generation, save, presentation and affected catalog rows agree on one universal item slot. | +| 2026-08-12 | `r7-1-development-preflight-01` | `DEVELOPMENT_PREFLIGHT_PASS` | Existing R7 browser suite passed after adding individual level/XP and item reads. This dirty-tree attempt is not candidate evidence. | +| 2026-08-12 | `r7-1-development-preflight-02` | `DEVELOPMENT_VISUAL_FAIL` | Browser assertions passed, but agent visual review found the portrait-phone Equipment page hid its fourth candidate and clipped the primary Apply action. No candidate judgment was made. | +| 2026-08-12 | `r7-1-development-preflight-03` | `DEVELOPMENT_PREFLIGHT_PASS` | Repaired portrait-phone Equipment composition keeps four candidate rows, the one-slot label, old-item return and Apply action inside the 16:9 stage. Full R7 browser suite passes; fixed-source evidence remains required. | diff --git a/prototype/site/app.js b/prototype/site/app.js index e99bf6a3..8488bf2a 100644 --- a/prototype/site/app.js +++ b/prototype/site/app.js @@ -542,12 +542,29 @@ const vfxAssets = { }; const generatedOffers = [ - { id: "OFF-01", name: "Elian Rook", profession: "Hearthguard", trait: "Narrow Guard", read: "Intercepts a neighbor / slower steps", mark: "HG", art: actorAssets.Hearthguard }, - { id: "OFF-02", name: "Nia Orrow", profession: "Farstring", trait: "Patient Start", read: "Rear pressure / delayed first basic", mark: "FS", art: actorAssets.Farstring }, - { id: "OFF-03", name: "Tomas Vale", profession: "Wayrunner", trait: "Open Stance", read: "Reorders safely / briefly Exposed", mark: "WR", art: actorAssets.Wayrunner }, - { id: "OFF-04", name: "Sable Kest", profession: "Kindhand", trait: "Shared Cup", read: "Restores a neighbor / weaker self-heal", mark: "KH", art: actorAssets.Kindhand }, + { id: "OFF-01", name: "Elian Rook", profession: "Hearthguard", trait: "Narrow Guard", read: "Intercepts a neighbor / slower steps", mark: "HG", art: actorAssets.Hearthguard, level: 1, xp: 0, xpNext: 30, itemId: "EQ-002", item: "Frontpin Greaves" }, + { id: "OFF-02", name: "Nia Orrow", profession: "Farstring", trait: "Patient Start", read: "Rear pressure / delayed first basic", mark: "FS", art: actorAssets.Farstring, level: 1, xp: 0, xpNext: 30, itemId: "EQ-014", item: "Quiet Rearplate" }, + { id: "OFF-03", name: "Tomas Vale", profession: "Wayrunner", trait: "Open Stance", read: "Reorders safely / briefly Exposed", mark: "WR", art: actorAssets.Wayrunner, level: 1, xp: 0, xpNext: 30, itemId: "EQ-001", item: "Stepback Soles" }, + { id: "OFF-04", name: "Sable Kest", profession: "Kindhand", trait: "Shared Cup", read: "Restores a neighbor / weaker self-heal", mark: "KH", art: actorAssets.Kindhand, level: 1, xp: 0, xpNext: 30, itemId: "EQ-009", item: "Rescue Loop" }, ]; +const currentRecruitProfiles = { + "OFF-01": { level: 4, xp: 36, xpNext: 60, itemId: "EQ-002", item: "Frontpin Greaves" }, + "OFF-02": { level: 3, xp: 44, xpNext: 50, itemId: "EQ-014", item: "Quiet Rearplate" }, + "OFF-03": { level: 2, xp: 18, xpNext: 40, itemId: "EQ-001", item: "Stepback Soles" }, + "OFF-04": { level: 2, xp: 32, xpNext: 40, itemId: "EQ-009", item: "Rescue Loop" }, +}; + +function recruitProfile(offer, current = false) { + return currentRecruitProfiles[offer.id] && current + ? { ...offer, ...currentRecruitProfiles[offer.id] } + : { level: 1, xp: 0, xpNext: 30, itemId: null, item: null, ...offer }; +} + +function compactRecruitBuild(profile) { + return `${bi("LV", "等级")} ${profile.level} · ${profile.itemId || bi("EMPTY ITEM", "无装备")}`; +} + const stageDetails = { "SYS-001": ["Verifying local rules", "Content revision 69f671", "Save lineage healthy"], "SYS-002": ["Language: English", "Text: 115%", "Captions: On"], @@ -745,14 +762,16 @@ function renderOpening(screen, variant = "offers2") { return frame(screen, body, { variant, resources: false }); } -function recruitMarker(offer, selected = false) { - return `
${actorImage(offer, "recruit-marker-art")}
${offer.id}${localizedText(offer.name)}${localizedText(offer.profession)}
`; +function recruitMarker(offer, selected = false, current = false) { + const profile = recruitProfile(offer, current); + return `
${actorImage(offer, "recruit-marker-art")}
${offer.id}${localizedText(offer.name)}${localizedText(offer.profession)} · ${compactRecruitBuild(profile)}
`; } function recruitOffer(offer, selected) { + const profile = recruitProfile(offer); return `
${actorImage(offer, "offer-character")}${offer.mark}${offer.id}
-
${localizedText(offer.name)}${localizedText(offer.profession)}

${localizedText(offer.trait)} - ${localizedText(offer.read)}

+
${localizedText(offer.name)}${localizedText(offer.profession)}
${bi("LV", "等级")} ${profile.level}${profile.itemId} · ${localizedText(profile.item)}

${localizedText(offer.trait)} - ${localizedText(offer.read)}

${t("noCoin")}${iconButton("i", `${t("inspect")} ${offer.name}`)}
`; } @@ -769,9 +788,11 @@ function renderMarket(screen, variant) { const isItem = screen.id === "SHP-003"; if (isRecruit) { const offer = variant === "hire2" ? generatedOffers[2] : generatedOffers[0]; + const profile = recruitProfile(offer); const body = `
${recruitMarker(offer, true)}⬡
RECRUIT REVIEW

${localizedText(offer.name)}

${localizedText(offer.profession)} · ${localizedText(offer.trait)}

+
${bi("INDIVIDUAL LEVEL", "角色独立等级")} · LV ${profile.level}XP ${profile.xp} / ${profile.xpNext}${bi("ONE EQUIPMENT SLOT", "唯一装备槽")} · ${profile.itemId} ${localizedText(profile.item)}
VIT APACE CREACH ${offer.profession === "Hearthguard" ? "1" : "2"}GUARD B

${localizedText(offer.read)}。${state.locale === "zh_CN" ? "随机生成身份;每人只有一个机械特性,不包含角色关系或个人剧情。" : "Generated identity, exactly one Trait, no authored personal arc."}

PARTY IMPACT${t("company")} ${count} → ${Math.min(4, count + 1)}

${offer.profession === "Hearthguard" ? "Adds a visible front intercept." : "Adds position movement and target change."}

Price 46 Coin · Operation HIRE-${variant === "hire2" ? "002" : "001"}
@@ -789,9 +810,9 @@ function renderMarket(screen, variant) { const body = `
${bi("INTEGRATED MARKET", "一体化集市")}

${bi("Company, offers and inventory stay visible", "队伍、报价与库存保持同屏")}

${t("company")}${count}/${t("capacity")}4
-
${bi("COMPANY BENCH", "队伍席位")}${bi("Tap Recruit to inspect or reorder", "点击队员查看或调整顺序")}
${company.map((offer, index) => ``).join("")}${Array.from({ length: 4 - count }, (_, index) => ``).join("")}
+
${bi("COMPANY BENCH", "队伍席位")}${bi("Tap Recruit to inspect or reorder", "点击队员查看或调整顺序")}
${company.map((offer, index) => { const profile = recruitProfile(offer, true); return ``; }).join("")}${Array.from({ length: 4 - count }, (_, index) => ``).join("")}
-
${offers.map((offer, index) => ``).join("")}
+
${offers.map((offer, index) => { const profile = recruitProfile(offer); return ``; }).join("")}
${bi("RUN INVENTORY", "本轮库存")}4 / 12 · ${bi("Tap item to compare", "点击物品进行比较")}
${Array.from({ length: 12 }, (_, index) => { const item = inventory[index]; const inventoryNames = ["Stepback Soles", "Frontpin Greaves", "Rescue Loop", "Open-Line Cord"]; return ``; }).join("")}
@@ -804,7 +825,7 @@ function partyActors() { ["3", "Nia", generatedOffers[1], "Rear"], ["2", "Sable", generatedOffers[3], "Protected"], ["1", "Tomas", generatedOffers[2], "Center"], ["0", "Elian", generatedOffers[0], "Exposed"], ]; const swapped = [original[0], original[2], original[1], original[3]]; - return (state.partySwapped ? swapped : original).map(([position, name, offer, status], index) => ``).join(""); + return (state.partySwapped ? swapped : original).map(([position, name, offer, status], index) => { const profile = recruitProfile(offer, true); return ``; }).join(""); } function renderPreparation(screen, variant) { @@ -813,8 +834,8 @@ function renderPreparation(screen, variant) { const equipmentRows = [["Stepback Soles", "EQ-001"], ["Frontpin Greaves", "EQ-002"], ["Rescue Loop", "EQ-009"], ["Open-Line Cord", "EQ-012"]]; const body = `
ELIGIBLE INVENTORY${equipmentRows.map(([name, id], index) => ``).join("")}
-
${actorImage(generatedOffers[1], "equipment-wearer-art")}Nia OrrowFarstring · position 3
Current: Quiet Rearplate
-
BEHAVIOR DELTA

${comparison ? "Quiet Rearplate → Stepback Soles" : "Stepback Soles"}

After first hitStep rear
Target relationFront → Center
Next basicWait +1

At full rear the trigger is dormant. Apply remains reversible until readiness commit.

+
${bi("ONE EQUIPMENT SLOT", "唯一装备槽")}${actorImage(generatedOffers[1], "equipment-wearer-art")}Nia OrrowFarstring · LV 3 · XP 44/50 · position 3
${bi("CURRENT SLOT", "当前装备槽")}${localizedText("Quiet Rearplate")} · EQ-014
+
BEHAVIOR DELTA

${comparison ? "Quiet Rearplate → Stepback Soles" : "Stepback Soles"}

${bi("Slot after commit", "确认后的装备槽")}EQ-001 · ${localizedText("Stepback Soles")}
${bi("Prior item", "原装备")}${bi("EQ-014 returns to inventory", "EQ-014 退回本轮库存")}
After first hitStep rear
Next basicWait +1

At full rear the trigger is dormant. Apply remains reversible until readiness commit.

`; return frame(screen, body, { variant }); } @@ -875,7 +896,7 @@ function renderOutcome(screen, variant) { return frame(screen, body, { variant }); } if (screen.id === "OUT-002") { - const body = `
${actorImage(generatedOffers[1], "fallen-actor-art")}FALLEN
COMMITTED LOSS

Nia Orrow

Farstring · Patient Start · Level 3 · survived 4 battles

EquipmentReturned to inventory
Party gapRear pressure lost
Replacement Credit+1 tier
Death is permanent for this run. No relationship or personal-arc system is implied.
${actionDock("Acknowledge loss", "Inspect receipt", "data-flow-next")}
`; + const body = `
${actorImage(generatedOffers[1], "fallen-actor-art")}FALLEN
COMMITTED LOSS

Nia Orrow

Farstring · Patient Start · LV 3 · XP 44/50 · survived 4 battles

${bi("Individual progress lost", "失去的个人成长")}LV 3 · XP 44/50
${bi("One equipped item", "唯一装备")}EQ-014 ${bi("returned to inventory", "退回本轮库存")}
Party gapRear pressure lost
Replacement Credit+1 tier
Death is permanent for this run. No relationship or personal-arc system is implied.
${actionDock("Acknowledge loss", "Inspect receipt", "data-flow-next")}
`; return frame(screen, body, { variant }); } if (screen.id === "OUT-003") { @@ -898,12 +919,13 @@ function renderOutcome(screen, variant) { function renderRoster(screen, variant) { if (screen.id === "REC-003") { - const body = `
${recruitMarker(generatedOffers[2], true)}
DESTRUCTIVE REVIEW

Dismiss Tomas Vale?

Equipment returns to run inventory
No Replacement Credit
Living Company 4 → 3

Eligibility is revalidated at commit. The last living recruit can never be dismissed.

${actionDock("Dismiss Recruit", "Cancel")}
`; + const body = `
${recruitMarker(generatedOffers[2], true, true)}
DESTRUCTIVE REVIEW

Dismiss Tomas Vale?

LV 2 · XP 18/40 is lost
One equipped item · EQ-001 returns
No Replacement Credit
Living Company 4 → 3

Eligibility is revalidated at commit. The last living recruit can never be dismissed.

${actionDock("Dismiss Recruit", "Cancel")}
`; return frame(screen, body, { variant }); } - const roster = generatedOffers.concat([{ id: "RC-017", name: "Miro Sen", profession: "Coilsmith", trait: "Deep Pocket", mark: "CS" }]); + const roster = generatedOffers.concat([{ id: "RC-017", name: "Miro Sen", profession: "Coilsmith", trait: "Deep Pocket", mark: "CS", level: 2, xp: 6, xpNext: 40, itemId: null, item: null }]); const focusedOffer = screen.id === "REC-002" ? generatedOffers[1] : generatedOffers[3]; - const body = `
LIVING COMPANY · 5${roster.map((offer, index) => ``).join("")}
`; + const focusedProfile = recruitProfile(focusedOffer, true); + const body = `
LIVING COMPANY · 5${roster.map((offer, index) => ``).join("")}
`; return frame(screen, body, { variant }); } @@ -999,7 +1021,7 @@ function renderProgress(screen, variant) { if (screen.id === "PST-002") return renderOathSeason(screen, variant); if (screen.id === "PST-003") return renderOathResult(screen, variant); if (screen.id === "PRO-001") { - const body = `
${bi("RUN GROWTH · ONE DECISION PENDING", "本轮成长 · 一项决策待定")}

${bi("Elian can change how the front is held", "Elian 可以改变前排的承伤方式")}

${bi("Promotion preserves level, Trait, equipment and current HP percentage.", "晋升会保留等级、特性、装备与当前生命比例。")}

LV 6
${actorImage(generatedOffers[0], "growth-actor-art")}PF-B01${bi("Hearthguard", "护心卫士")}

${bi("Intercept adjacent direct pressure", "拦截相邻队员承受的直接压力")}

`; + const body = `
${bi("RUN GROWTH · ONE DECISION PENDING", "本轮成长 · 一项决策待定")}

${bi("Elian can change how the front is held", "Elian 可以改变前排的承伤方式")}

${bi("Promotion preserves this Recruit's level, XP, Trait, one equipped item and current HP percentage.", "晋升会保留这名角色自己的等级、经验、特性、唯一装备与当前生命比例。")}

LV 6XP 12/80
${actorImage(generatedOffers[0], "growth-actor-art")}PF-B01${bi("Hearthguard", "护心卫士")}

${bi("Intercept adjacent direct pressure", "拦截相邻队员承受的直接压力")}

`; return frame(screen, body, { variant, art: false }); } if (screen.id === "PRO-003") { diff --git a/prototype/site/data/catalog.json b/prototype/site/data/catalog.json index 224c0dcc..2cdbca10 100644 --- a/prototype/site/data/catalog.json +++ b/prototype/site/data/catalog.json @@ -127,10 +127,10 @@ "id": "SHP-002", "name": "Recruit review", "input": "SELECT", - "purpose": "Evaluate generated name, profession, trait benefit/cost, rolled attributes, equipment eligibility, level, survival history, price, and party impact.", + "purpose": "Evaluate generated name, profession, trait benefit/cost, rolled attributes, one starting item, individual level/XP, survival history, price, and party impact.", "action": "Recruit; commits BIND:RECRUIT_TRANSACTION after review.", "emptyLoading": "Missing roster comparison states which binding is pending. Loading shows stable recruit identity, never a substitute recruit.", - "errorConfirmation": "Offer expired, insufficient value, invalid capacity, or duplicate receipt keeps the prior state. Confirmation states exact recruit, cost, capacity effect, and undo policy.", + "errorConfirmation": "Offer expired, insufficient value, invalid capacity, or duplicate receipt keeps the prior state. Confirmation states exact Recruit, level, starting item, cost, capacity effect, and undo policy.", "interruptionReturn": "Suspend after commit checks receipt before retry. Cancel returns to Shop on the same stable offer ID." }, { @@ -177,20 +177,20 @@ "id": "EQP-001", "name": "Equipment workbench", "input": "LIST", - "purpose": "Equip a selected recruit while showing eligible inventory and sourced behavior changes.", - "action": "Equip selected item; changes loadout only after comparison review.", - "emptyLoading": "No owned items points to Shop. No eligible item explains profession/slot requirements. Loading retains current loadout.", - "errorConfirmation": "Incompatible, already used, stale inventory, or ruleset error blocks commit with reason. Confirmation states replaced item, conflicts, and before/after behavior.", - "interruptionReturn": "Suspend before receipt keeps the old loadout; resume reconciles receipt. Back returns to Recruit Detail or Party Line caller." + "purpose": "Equip a selected Recruit while showing individual level/XP, its one universal slot, eligible inventory and sourced behavior changes.", + "action": "Equip selected item; atomically returns the prior item or null and changes the one slot only after comparison review.", + "emptyLoading": "No owned items points to Shop. No eligible item explains Profession/form requirements. Loading retains the current slot.", + "errorConfirmation": "Incompatible, already used, stale inventory, or ruleset error blocks commit with reason. Confirmation states the returned prior item, one-slot result, conflicts, and before/after behavior.", + "interruptionReturn": "Suspend before receipt keeps the old item; resume reconciles receipt. Back returns to Recruit Detail or Party Line caller." }, { "id": "EQP-002", "name": "Item comparison", "input": "SELECT", - "purpose": "Compare current and candidate item in the vocabulary used by party preview, battle, and results.", - "action": "Apply candidate; returns to workbench with changed effects highlighted.", - "emptyLoading": "Empty candidate keeps current item and offers Choose Item. Loading never clears current values.", - "errorConfirmation": "Unknown derived effect is labeled unresolved and blocks Apply when consequence-critical. Confirmation lists all sourced changes and affected saved plan references.", + "purpose": "Compare the one current item and candidate in the vocabulary used by party preview, battle, and results.", + "action": "Apply candidate; returns to workbench with changed effects and returned prior item highlighted.", + "emptyLoading": "Empty candidate keeps current item and offers Choose Item. Empty current slot is explicit. Loading never clears current values.", + "errorConfirmation": "Unknown derived effect is labeled unresolved and blocks Apply when consequence-critical. Confirmation lists the exact old/new item IDs and affected saved plan references.", "interruptionReturn": "Suspend keeps comparison but performs no equip. Back returns to Equipment with selection intact." }, { @@ -1092,10 +1092,10 @@ { "id": "TR-020", "name": "Oathbound Tool", - "benefit": "Choose one equipped item at battle start; its first trigger gains priority over profession action.", - "cost": "Other item triggers wait until the chosen trigger resolves or expires.", - "evidence": "Chosen-slot ribbon and queue state; blocked triggers have reason.", - "prohibited": "No-trigger equipment sets; PF-A24." + "benefit": "The Recruit's equipped item's first trigger gains priority over its profession action.", + "cost": "Until that trigger resolves or expires, the Recruit's first technique waits.", + "evidence": "One-slot oath ribbon and queue state; delayed technique has a reason.", + "prohibited": "Triggerless equipment; PF-A24." }, { "id": "TR-021", @@ -3563,7 +3563,7 @@ "name": "Counterpair Compass", "fit": "charm; any", "class": "B", - "effect": "After wearer uses two distinct preview response tags, refresh oldest item cooldown.", + "effect": "After wearer uses two distinct preview response tags, refresh this item's cooldown.", "tradeoff": "Repeating response resets pair.", "production": "I/V/S: two-needle compass, response pips, paired ticks." }, @@ -3599,9 +3599,9 @@ "name": "Trigger Queue Comb", "fit": "charm; any", "class": "B", - "effect": "When two items would trigger together, choose left-to-right equipped order.", - "tradeoff": "Second trigger resolves one tick later.", - "production": "I/O/V/S: toothed comb, ordered pips, two clicks." + "effect": "When wearer's item and one adjacent ally's item would trigger together, choose their left-to-right resolution order.", + "tradeoff": "Second Recruit's trigger resolves one tick later.", + "production": "I/O/V/S: toothed comb, two-Recruit order pips, two clicks." }, { "id": "EQ-265", @@ -3635,9 +3635,9 @@ "name": "Cooldown Bridge", "fit": "charm; any", "class": "B", - "effect": "When one item readies, transfer one cooldown tick to another equipped item.", - "tradeoff": "Ready item becomes unavailable one tick.", - "production": "I/V/S: bridge clasp, clock exchange, bridge ping." + "effect": "When this item readies, pull one cooldown tick from an adjacent ally's equipped item.", + "tradeoff": "The ally's item gains that tick and this wearer becomes Hindered.", + "production": "I/V/S: bridge clasp, neighbor clock exchange, bridge ping." }, { "id": "EQ-269", @@ -3662,9 +3662,9 @@ "name": "Opposite Trigger Ring", "fit": "charm; any", "class": "B", - "effect": "Direct-hit item proc also readies next area-trigger item, or reverse.", - "tradeoff": "Same-family trigger chain is prohibited and Hinders wearer.", - "production": "I/V/S: dot/ring token, cross-family arrow, two-tone ping." + "effect": "Wearer's direct-hit item proc readies the nearest ally's area-tag item, or the reverse pairing.", + "tradeoff": "Same-family pairing is prohibited and Hinders wearer.", + "production": "I/V/S: dot/ring token, cross-Recruit arrow, two-tone ping." }, { "id": "EQ-272", @@ -3689,8 +3689,8 @@ "name": "Delayed Item Strap", "fit": "garb; any", "class": "B", - "effect": "Store first automatic item trigger and release on wearer command condition set in Prepare.", - "tradeoff": "Stored trigger blocks all other item triggers.", + "effect": "Store this item's first automatic trigger and release it on a wearer condition set in Prepare.", + "tradeoff": "While stored, the wearer's signature action cannot begin.", "production": "I/O/V/S: strapped token, held-proc cue, buckle release." }, { @@ -4058,18 +4058,18 @@ "name": "No-Item Oath", "fit": "charm; any", "class": "B", - "effect": "Disable all other equipment effects; profession basic gains one alternate legal target rule.", - "tradeoff": "This item occupies the only active equipment hook. Unlock: win one region without item triggers.", - "production": "I/V/S: blank oath medallion, disabled-slot cue, solemn click." + "effect": "This item has no trigger; profession basic gains one alternate legal target rule.", + "tradeoff": "It occupies the Recruit's one equipment slot and supplies no item-trigger event. Unlock: win one region without item triggers.", + "production": "I/V/S: blank oath medallion, empty-hook cue, solemn click." }, { "id": "EQ-316", "name": "Item Chorus Frame", "fit": "kit; any", "class": "B", - "effect": "Three different equipped trigger tags in one battle schedule a non-damaging repeat of oldest.", - "tradeoff": "All three items gain cooldown. Unlock: activate eight distinct trigger tags.", - "production": "I/O/V/S: three-hook frame, chorus pips, layered clicks." + "effect": "Three different allied equipped-item trigger tags in one battle schedule a non-damaging repeat of the oldest.", + "tradeoff": "The three contributing allied items each gain cooldown. Unlock: activate eight distinct trigger tags.", + "production": "I/O/V/S: three-Recruit frame, chorus pips, layered clicks." }, { "id": "EQ-317", diff --git a/prototype/site/styles.css b/prototype/site/styles.css index 06fdf7c3..1a7928cb 100644 --- a/prototype/site/styles.css +++ b/prototype/site/styles.css @@ -119,6 +119,8 @@ main { min-width: 0; padding: 24px; } .offer-copy { min-width: 0; padding: 5px 7px; } .offer-copy strong { display: block; overflow-wrap: anywhere; font-size: 11px; } .profession-seal { display: inline-block; margin-top: 2px; padding: 2px 4px; color: var(--white); font-size: 8px; font-weight: 800; background: var(--route-teal); border-radius: 2px; } +.offer-loadout { display: flex; gap: 3px; margin-top: 3px; } +.offer-loadout span { min-width: 0; padding: 2px 3px; color: var(--ink-950); font-size: 6px; font-weight: 800; background: var(--mist-180); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .offer-copy p { margin: 3px 0 0; color: var(--ink-700); font-size: 8px; line-height: 1.25; } .offer-actions { min-height: 29px; display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 3px; padding: 4px 5px; border-top: 1px solid var(--line); } .offer-actions > span { font-size: 7px; font-weight: 800; } @@ -239,6 +241,9 @@ main { min-width: 0; padding: 24px; } .review-facts h2 { margin: 3px 0; font-size: 21px; } .review-facts h3 { margin: 0 0 7px; color: var(--route-teal); font-size: 11px; } .review-facts p { font-size: 9px; line-height: 1.4; } +.recruit-level-strip { display: grid; grid-template-columns: 1.1fr .65fr 1.4fr; gap: 3px; margin-bottom: 5px; } +.recruit-level-strip > * { padding: 4px; font-size: 6px; background: var(--mist-120); } +.recruit-level-strip strong { border-left: 3px solid var(--work-gold); } .stat-radar { display: grid; grid-template-columns: repeat(4, 1fr); gap: 3px; } .stat-radar span { padding: 5px; font-size: 7px; background: var(--mist-120); } .stat-radar b { display: block; margin-top: 2px; font-size: 12px; } @@ -269,6 +274,12 @@ main { min-width: 0; padding: 24px; } .recruit-detail .large-figure { width: 58px; height: 72px; } .recruit-detail h2 { margin: 5px 0; font-size: 18px; } .recruit-detail p { margin: 0 0 7px; font-size: 9px; } +.level-progress { position: relative; display: grid; grid-template-columns: 1fr auto; gap: 1px 7px; margin-bottom: 5px; padding: 6px 7px 10px; background: var(--mist-120); border-left: 4px solid var(--work-gold); } +.level-progress span { grid-column: 1 / 3; font-size: 6px; font-weight: 900; } +.level-progress strong { font-size: 13px; } +.level-progress b { align-self: end; font-size: 7px; } +.level-progress i { position: absolute; left: 7px; right: 7px; bottom: 4px; height: 3px; background: var(--mist-180); } +.level-progress i::after { content: ""; display: block; width: var(--xp); height: 100%; background: var(--safe-jade); } .detail-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 4px; } .detail-grid span { padding: 5px; font-size: 7px; background: var(--mist-120); } .detail-grid b { display: block; margin-top: 2px; font-size: 9px; } @@ -291,10 +302,12 @@ main { min-width: 0; padding: 24px; } .equipment-row small { font-size: 7px; } .equipment-row em { color: var(--ink-700); font-size: 6px; font-style: normal; } .equipment-wearer { display: grid; place-items: center; align-content: center; text-align: center; } +.one-slot-badge { margin-bottom: 4px; padding: 3px 5px; color: var(--ink-950); font-size: 6px; font-weight: 900; background: var(--work-gold); } .equipment-wearer .large-figure { width: 68px; height: 86px; font-size: 16px; } .equipment-wearer strong { margin-top: 5px; font-size: 12px; } .equipment-wearer small { font-size: 8px; } .equipped-slot { margin-top: 8px; padding: 5px; font-size: 7px; background: var(--mist-120); } +.equipped-slot b { display: block; margin-bottom: 2px; color: var(--route-teal); font-size: 6px; } .equipment-delta { border-top: 4px solid var(--safe-jade); } .equipment-delta h2 { margin: 4px 0 8px; font-size: 14px; } .equipment-delta p { font-size: 8px; line-height: 1.35; } @@ -315,6 +328,7 @@ main { min-width: 0; padding: 24px; } .party-actor-art { width: 100%; height: 100%; object-fit: contain; object-position: center bottom; filter: drop-shadow(0 2px 2px rgba(12,25,29,.22)); } .party-actor > b { width: 20px; height: 20px; display: grid; place-items: center; color: var(--white); font-size: 9px; background: var(--ink-950); border-radius: 2px; } .party-actor > strong { justify-self: end; font-size: 8px; } +.party-actor > strong small { display: block; color: var(--ink-700); font-size: 5px; font-weight: 700; text-align: right; } .party-actor > em { grid-column: 1 / 3; color: var(--ink-700); font-size: 7px; font-style: normal; } .cord-label { z-index: 2; width: 62px; font-size: 8px; font-weight: 900; text-align: center; } .cord-label.rear { color: #b7e6e0; } @@ -607,6 +621,7 @@ main { min-width: 0; padding: 24px; } .run-growth-composition > header { grid-column: 1 / 3; display: flex; justify-content: space-between; align-items: start; } .run-growth-composition header p { margin: 2px 0; font-size: 8px; } .growth-level { min-width: 58px; padding: 8px; color: var(--ink-950); font-size: 15px; font-weight: 900; text-align: center; background: var(--work-gold); } +.growth-level small { display: block; margin-top: 2px; font-size: 6px; } .growth-tree { min-height: 0; display: grid; grid-template-columns: .75fr 24px 1fr; grid-template-rows: 1fr 1fr; gap: 6px; } .growth-origin { grid-row: 1 / 3; display: grid; place-items: center; align-content: center; padding: 8px; background: var(--white); border-top: 4px solid var(--route-teal); } .growth-actor-art { width: 82px; height: 110px; object-fit: contain; object-position: center bottom; } @@ -1052,12 +1067,23 @@ main { min-width: 0; padding: 24px; } .item-review p, .item-review aside span { display: none; } .equipment-composition { grid-template-columns: .9fr .55fr 1fr; gap: 2px; padding: 2%; } .equipment-list, .equipment-wearer, .equipment-delta { padding: 4px; } - .equipment-row { min-height: 28px; } - .equipment-row:nth-of-type(n+4) { display: none; } + .equipment-row { min-height: 22px; gap: 2px; } + .equipment-row > span { width: 24px; } + .equipment-row-art { width: 22px; height: 22px; } + .equipment-row strong { font-size: 6px; } + .equipment-row small { font-size: 5px; } + .equipment-row em { display: none; } .equipment-wearer .large-figure { width: 36px; height: 44px; } .equipment-wearer strong { font-size: 7px; } - .equipped-slot, .equipment-delta p { display: none; } + .equipment-wearer small { font-size: 5px; } + .one-slot-badge { padding: 2px; font-size: 4px; } + .equipped-slot { margin-top: 2px; padding: 2px; font-size: 5px; } + .equipped-slot b { font-size: 4px; } + .equipment-delta p { display: none; } .equipment-delta h2 { font-size: 8px; } + .equipment-delta .delta-row { padding: 3px; font-size: 5px; } + .equipment-delta .delta-row:nth-of-type(n+3) { display: none; } + .equipment-delta .stage-primary { min-height: 20px; padding: 2px; font-size: 5px; } .party-title .stage-kicker, .party-title p, .cord-label { display: none; } .party-title h2 { font-size: 9px; } .party-cord { top: 24%; height: 74px; } diff --git a/prototype/site/verify-r7.js b/prototype/site/verify-r7.js index 1b1956ab..73eb81bd 100644 --- a/prototype/site/verify-r7.js +++ b/prototype/site/verify-r7.js @@ -103,6 +103,7 @@ async function desktopAudit(browser, evidence) { 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(".offer-character").count(), 4, "four profession identity anchors"); + assert.equal(await page.locator(".offer-loadout").count(), 4, "four offers expose individual level and one starting item"); assert.deepEqual(await page.locator(".offer-character").first().evaluate((image) => [image.complete, image.naturalWidth > 0]), [true, true], "first profession identity loads"); assert.equal(await page.locator(".recruit-offer.is-selected").count(), 0, "opening build is not preselected"); await noHorizontalOverflow(page, "desktop opening"); @@ -124,6 +125,7 @@ async function desktopAudit(browser, evidence) { 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 > em').count(), 4, "party line exposes four level and one-item summaries"); 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(); @@ -158,6 +160,14 @@ async function desktopAudit(browser, evidence) { await selectScreen(page, id); assert.ok(await page.locator(`[data-stage-id="${id}"] ${selector}`).isVisible(), `${id} critical composition`); } + await selectScreen(page, "REC-002"); + assert.match(await page.locator(".level-progress").textContent(), /LV 3.*XP 44 \/ 50/s, "Recruit detail exposes individual level and XP"); + assert.match(await page.locator(".detail-grid").textContent(), /EQ-014/, "Recruit detail exposes the one equipped item"); + evidence.push(await screenshot(page, "desktop-recruit-level.png")); + await selectScreen(page, "EQP-002"); + assert.match(await page.locator(".one-slot-badge").textContent(), /ONE EQUIPMENT SLOT|唯一装备槽/, "equipment page names one universal slot"); + assert.match(await page.locator(".equipment-delta").textContent(), /EQ-014 returns to inventory|EQ-014 退回本轮库存/, "replacement returns the prior item"); + evidence.push(await screenshot(page, "desktop-equipment-one-slot.png")); await page.locator('[data-screen-variant="error"]').click(); assert.ok(await page.locator(".state-variant-overlay.error").isVisible(), "screen error variant inspectable"); @@ -251,6 +261,7 @@ async function responsiveAudit(browser, evidence) { { 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 }, { name: "phone-portrait-final-choice.png", viewport: { width: 390, height: 844 }, view: "screens", screen: "PST-001", vertical: true }, + { name: "phone-portrait-equipment.png", viewport: { width: 390, height: 844 }, view: "screens", screen: "EQP-002", vertical: true }, { name: "phone-landscape-charter.png", viewport: { width: 844, height: 390 }, view: "screens", screen: "PRO-002", vertical: true }, { name: "phone-portrait-reference-market.png", viewport: { width: 390, height: 844 }, view: "reference", reference: 2, vertical: false }, { name: "phone-landscape-reference-battle.png", viewport: { width: 844, height: 390 }, view: "reference", reference: 7, vertical: false }, @@ -275,6 +286,15 @@ async function responsiveAudit(browser, evidence) { } await noHorizontalOverflow(page, fixture.name); await stageFitsViewport(page, fixture.name, fixture.vertical); + if (fixture.screen === "EQP-002") { + assert.equal(await page.locator(".equipment-row:visible").count(), 4, "phone equipment keeps all four candidate rows visible"); + const actionBounds = await page.locator('[data-stage-id="EQP-002"] .equipment-delta .stage-primary').evaluate((button) => { + const buttonBox = button.getBoundingClientRect(); + const stageBox = button.closest(".game-stage").getBoundingClientRect(); + return { buttonTop: buttonBox.top, buttonBottom: buttonBox.bottom, stageTop: stageBox.top, stageBottom: stageBox.bottom }; + }); + assert.ok(actionBounds.buttonTop >= actionBounds.stageTop && actionBounds.buttonBottom <= actionBounds.stageBottom, `phone equipment action fits stage: ${JSON.stringify(actionBounds)}`); + } evidence.push(await screenshot(page, fixture.name)); assert.deepEqual(errors, [], `${fixture.name} browser errors: ${errors.join(" | ")}`); await page.close(); diff --git a/tools/verify_recruit_loadout_level_contract.py b/tools/verify_recruit_loadout_level_contract.py new file mode 100644 index 00000000..9964a7b0 --- /dev/null +++ b/tools/verify_recruit_loadout_level_contract.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Verify the Phase 7.1 single-item and individual-level design contract.""" + +from __future__ import annotations + +import re +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + +AUTHORITY_PATHS = [ + "docs/product/GAME_PRODUCT_CONTRACT.md", + "docs/product/SYSTEMS_AND_BATTLE.md", + "docs/product/ECONOMY_AND_BALANCE.md", + "docs/product/SAVE_AND_FAILURE_CONTRACT.md", + "docs/content/RECRUIT_GENERATION.md", + "docs/content/TRAIT_CATALOG.md", + "docs/content/EQUIPMENT_CATALOG.md", + "docs/presentation/SCREEN_AND_STATE_MAP.md", + "docs/presentation/UI_AND_VISUAL_SYSTEM.md", + "docs/presentation/ART_ASSET_AND_ANIMATION_CATALOG.md", +] + +FORBIDDEN = [ + r"three equipment slots", + r"all three equipment slots", + r"starting_equipment_ids\[\]", + r"one or two starter items", + r"two starter items", + r"two-item starts", + r"Fallen Recruit owns three items", + r"all three item IDs returned", + r"another equipped item", + r"three different equipped trigger tags", +] + + +def main() -> int: + errors: list[str] = [] + corpus = "\n".join( + (ROOT / path).read_text(encoding="utf-8") for path in AUTHORITY_PATHS + ) + lowered = corpus.lower() + + for pattern in FORBIDDEN: + if re.search(pattern, lowered): + errors.append(f"forbidden multi-item contract remains: {pattern}") + + required = { + "one universal active equipment slot": "systems one-slot authority", + "starting_equipment_id, level, xp": "Recruit schema", + "level cap 10": "individual XP authority", + "one-slot replacement receipt": "save transaction", + "角色独立等级": "Chinese UI level label", + "唯一装备槽": "Chinese UI equipment label", + "XP 44/50": "visible current XP progress", + "EQ-014 退回本轮库存": "visible replacement return", + } + ui_text = (ROOT / "prototype/site/app.js").read_text(encoding="utf-8") + combined = f"{corpus}\n{ui_text}" + for phrase, label in required.items(): + if phrase not in combined: + errors.append(f"missing {label}: {phrase}") + + equipment = (ROOT / "docs/content/EQUIPMENT_CATALOG.md").read_text( + encoding="utf-8" + ) + ids = re.findall(r"^\| `EQ-(\d{3})`", equipment, flags=re.MULTILINE) + if len(ids) != 320 or len(set(ids)) != 320: + errors.append(f"equipment identity count changed: rows={len(ids)} unique={len(set(ids))}") + + if errors: + for error in errors: + print(f"ERROR: {error}") + return 1 + + print( + "ABG_RECRUIT_LOADOUT_LEVEL_OK " + "equipment_slots=1 level_scope=individual level_cap=10 equipment=320" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())