docs+fix: sidequest close-out — T-1095 head tint, design doc, wardrobe docs, changelog (T-1088/T-1089)

T-1095 fixed: template heads now receive the body's skin tone (same
treatment as seg_head) — _load_head passes tone.lit with a full-white
fallback mask through _attach_to_bone; without a mask sidecar the tint was
silently ignored, so untinted templates read grey against mid/dark tones.
Verified by lookbook render (head_001 on child, tone 6 — face matches arms).

Docs: the T-1088 locomotion design document committed to docs/design/ (the
sandbox code's 'design §' references now resolve in-repo); character-asset-
organization gains §11 Wardrobe Pipeline and sheds three stale claims (the
never-built indexed-greyscale mask plan, single-mask-per-asset for clothing,
the resolved multi-region open question); CHANGELOG carries the sidequest
entry; D-250 amended with the shipped cutaway-modes verdict (mode 2 default,
user-decided live).

Tickets: T-1088, T-1089, T-1095 closed; T-1096 filed (S9 live feel-tuning,
needs the user). The Fable-5 cascade exception is closed — the cascade
resumes at Phase 4 (T-750).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 14:15:56 +02:00
co-authored by Claude Fable 5
parent e709462a7b
commit 3d6dcc9d05
8 changed files with 684 additions and 14 deletions
+81
View File
@@ -976,3 +976,84 @@ Basic-set EXPANSION (user, 2026-07-06 bedtime directive: ''full set of the basic
Fill-in-the-blanks addendum (user bedtime directive 2 + lead judgment): wave 2 grows to 12 adds sweater_modern (crew pullover), cargo_pants (utility/cyberpunk staple), parka_thrds (cold-weather was absent entirely; ships as the FIRST CANON-BRANDED garment thrds is Braemar''s cold-weather fiber brand per wiki; hip-length to avoid the parked skirt-rigging tier), slides (swim footwear, near-free). PLUS outfit presets: data file of named complete looks (casual/business/sport/beach/worker/sleep as tint-palette presets over garments) authored by the lead at integration makes the catalogue usable for NPC dressing and the shop as OUTFITS, not just items. Post-wave-2 catalogue: 21 modern pieces (tops 8 / bottoms 6 / feet 4 / full-body 2 / swim 3). Hats/accessories remain the deliberate daylight deferral (T-721).', NULL, '2026-07-06 21:20:30', '2026-07-06 21:20:30.325', '2026-07-06 21:20:30.325', NULL, '844a61fd59dbff78bb63a90f4399d5c9', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKCPBF41RWK9PW9M22Q7961C', 'status', 'backlog', 'review', NULL, '2026-07-07 09:23:38', '2026-07-07 09:23:38.268', '2026-07-07 09:23:38.268', NULL, 'dbfc4dd044d0fd988d9b5ff5e2514d7e', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKCPBF41RWK9PW9M22Q7961C', 'description', 'Fable-5-window sidequest workstream 2 (user directive 2026-07-06, extends T-1088 scope family): build the clothing ENGINE and a basic set now, park variety for later. Target bar: RimWorld''s original apparel breadth — varied enough to not feel limiting. Goal experience: being able to pick and SHOP for clothes — colorize + slap logos on casualwear. Basic set: t-shirt (logo-capable), hoodie (logo-capable), button-down shirt, shorts, jeans, formal pants, jacket, colorable uniform, black suit (moved up so officials are includable). Skipped by explicit call: capes, dresses/skirts (parked — difficulty tier). Engine deliverables: (1) multi-region tint masks — requires Quaternius Source tier files (USD5 Patreon month, one-time download, CC0 retained — user purchase action); (2) logo decal region on garment chest UV — flat 2D artwork per D-244, diegetic hook into economy generated brands (wiki/economics/corporations/generated_brands.toml) for brand-logo shirts; (3) manifest + CharacterVisualDescriptor category coverage (slots/tints already generic); (4) batch-fit each authored garment to all 11 body types via the validated Sprint-28 Blender Surface Deform pipeline (tooling/blender_*.py). Authoring routes per garment TBD by feasibility pass: derive from owned Fantasy pack meshes (62 parts, e.g. tunic->tshirt sleeve cut, pants->shorts hem cut) vs hand-author low-poly on the 65-bone rig (Garment Tool). Related: T-1088 locomotion sandbox doubles as the garment-under-animation QA rig (cycle outfits through Walk/Sprint/Crouch_Fwd).
Correction (2026-07-06): Source tier is PARTIALLY owned already docs/assets/downloads/ (gitignored vendor archives, present in both checkouts) holds Universal Base Characters[Source] (630MB: full .blend files for all 6 base bodies incl. FullBody + OnlyHead variants, hairstyle .blends origin-at-0 + rigged-to-head-bone, texture masters, Godot engine project) plus UBC[Standard], UAL 1+2[Standard], Modular Character Outfits Fantasy[Standard]. NOT owned: Fantasy Outfits[Source] the garment recolor masks + named vertex groups. Consequence: authoring NEW garments is fully unblocked (author against source body .blends, create our own tint masks/vertex groups no purchase needed); only pack-DERIVED garments'' clean masks would want one more USD5 Patreon month, decision deferred until the feasibility route table says how much derivation we actually use.
Art direction (user, 2026-07-06): garments read CYBERPUNK/MODERN near-future everyday civilian + corporate wear NOT fantasy/medieval. The owned Fantasy pack is at most a topology donor (low-poly base shapes like pants/shirts are genre-neutral; genre lives in silhouette details, textures, trims, and accessories) no medieval trim/textures may survive into shipped garments. Weight the authoring routes accordingly: texture identity matters as much as mesh. Canon hook: the wiki already ships a garment brand thrds (wiki/star-systems/GJ-475/index.md, Braemar fiber cooperative, corridor fashion markets) so branded clothing connects to Phase-1 lore, not only generated_brands.toml. This emphasis also feeds the Quaternius-pivot evaluation: commercial alternatives skew sci-fi/urban while Quaternius clothing is fantasy-only.
Nuance (user, 2026-07-06): the cyberpunk/modern direction is the DEFAULT, not a prohibition fashion in the Reach is cyclical, so fantasy/medieval-flavored garments may appear here and there as deliberate fashion statements (subculture pieces, luxury/revival trims, statement outerwear). Supersedes the previous note''s hard ''no medieval trim may survive'' line. Practical consequence: a small number of Fantasy-pack pieces may ship near-as-is where they read as intentional fashion; the basic set itself stays modern.
Art direction CONSOLIDATED (user, 2026-07-06 supersedes the two notes above): the Reach has no single clothing style ALL styles coexist, with modern/cyberpunk probably the most prevalent. Style is a prevalence DISTRIBUTION, not a genre rule, and may eventually vary by culture/settlement (fits the simulation DNA; fashion-as-cultural-data is a later phase concern, not this ticket). Consequences now: (1) the engine is style-agnostic by construction (slots/tints/logos carry no genre); (2) the basic set targets the prevalent center modern casual + corporate because it dresses the most NPCs and the shopping experience; (3) garments get a lightweight style tag in the manifest (e.g. style: modern|formal|fantasy|utility) as the cheap seam so per-culture distributions can be authored later without retrofitting; (4) Fantasy-pack pieces are legitimate catalogue members under their own tag, prevalence-weighted low.
Scope pin (user, 2026-07-06, final word on style): for simplicity, THIS ticket ships ONE style only cyberpunk/modern. The pluralistic distribution model above remains the long-term truth (keep the manifest style tag as the seam), but the v1 basic set is single-style; other styles are catalogue growth, not scope.
Route challenge (user, 2026-07-06): not sold on staying Quaternius-primary ''Quaternius is limited and will move the wall at too slow a pace; I''d rather pick something that has what we need now.'' Fresh store verification (2026-07-06): Sidekick packs are one-time USD199.99, perpetual, engine-agnostic license (Godot legal, unsupported); NO raw FBX extraction from unitypackage (community-proven: unitypackage_godot, third-party Sidekick Creator Godot plugin incl. body blend shapes); ''Sold out'' labels are a broken-theme artifact per Shopify JSON (needs one browser cart check); SyntyPass rejected (assets stop being developable after cancel). Relevant packs: Starter (FREE, 57+91 parts), Modern Civilians (165 parts = v1 basic set territory), Sci-Fi Civilians (103 parts, cyberpunk accents), Modern Police (uniforms/officials). DECISION NOW GATED ON THE FREE SPIKE: user downloads free Starter Pack -> Bridge-C spike (extract garment -> Robust Weight Transfer onto our rig -> 11-body batch -> animation QA in T-1088 sandbox). Pass -> buy Modern Civilians, Synty = primary garment supply (+UAL1/UAL2 Source ~USD30 for clips no Synty substitute exists for Godot animations; Fantasy Outfits Source drops to low priority under the single-style pin). Fail -> hand-author route stands. Stay-vs-pivot governance record HELD until spike evidence lands.
Scope guard (user concern, 2026-07-06): Synty intake must NOT displace the create-stuff-yourself pipeline the user''s goal includes in-house garment creation capability, not just swapping vendor catalogs. Resolution: three-route portfolio sharing one engine (a) Synty intake = variety bootstrap only; (b) derivation scripting (cuts/merges/retexture, agent-automatable) = create-yourself lite, turns any donor topology into original designs; (c) TRUE AUTHORING on owned UBC Source body .blends = the pipeline proper. GUARANTEE: the colorable uniform and black suit (no donor exists in any pack) are built via route (c) as the authoring pipeline''s end-to-end proof non-negotiable deliverable of this ticket, so the pipeline exists even if Synty covers everything else. Spike status: extraction done inline by lead (158 FBX, sci-fi civilian theme confirmed); DECISIVE finding Sidekick rig is UE-Mannequin-named, 52/65 bone-name overlap with our rig, sample garment binds to 12 vgroups of which 11 match ours exactly (only head->Head case rename) => transplant likely rename-and-rebind, far cheaper than the weight-transfer the pivot eval assumed; body blend shapes travel in the FBX. Transplant phase parked pending API recovery / token-window reset.
ROUTE DECISION (user, 2026-07-06, final): Synty intake REJECTED despite the spike''s technical PASS (1) USD199.99/pack is too expensive; (2) Sidekick modularity is baked body-segment swap (clothes fused to body chunks), incompatible with our layered clothing-over-body system (11 bodies, skin tones, slot swapping); (3) user dislikes the Synty style. The spike still paid: transplant/batch scripts are source-agnostic and become the G1-family production pipeline; T-1090 (5 bodies broken bare) discovered; the Synty question is closed with evidence, not parked. OPERATIVE ROUTE: (b) derive from owned Fantasy parts + (c) hand-author with a new preferred authoring technique: OFFSET-SHELL FROM OUR OWN BODY MESHES (extract body segment surface, offset outward into garment shell, cut neckline/hem/sleeves; weights inherited by construction, style fully ours, USD0). Suit + uniform remain the full hand-author proof. Purchases still open (separate concerns): UAL1/UAL2 Source ~USD30 for animation clips; Fantasy Outfits Source USD20 low-priority convenience.
Chromakey garment-QA harness DELIVERED (2026-07-06, commits eaca6c8c4/74fa16260/b09a50efb): two-pass depth-proximity clip detection pass A keys covered body segments magenta, pass B re-renders garment-only with a depth-only 3cm epsilon bias toward camera; analyzer splits key pixels into exposed_skin (informational sleeveless arms, collars) vs clip_through (gating skin within 3cm in front of cloth). Run: tooling/garment-qa/run-garment-qa <config>. Verified on the peasant set (avg_m/f x Walk/Sprint/Crouch_Fwd x 3 frames x 4 yaws): 0/18 front-view false positives; genuine findings = sleeveless armhole seam (avg_f, sides), deep-crouch waist gap (worst 150px, back), collar nape. First real fit observations already on record for the peasant set. Sensitivity: clip_epsilon_m + --min-pixels, to calibrate against the first modern garments. This is the acceptance gate for every garment the wardrobe pipeline produces; pair with the T-1088 sandbox for in-world walk QA.
ENGINE DELIVERED (2026-07-06, commits e22ea0fa4 + 278e8b7c2; earlier eaca6c8c4/74fa16260/b09a50efb chromakey, b54b8189d bodies): G1 skinned batch-fit (blender_batch_fit_skinned.py), offset-shell authoring (blender_author_offset_shell.py route c create-yourself, weights by construction, procedural RGBA masks, UV2 chest channel), toon_garment.gdshader (channel-blended 4-region tint + logo after-tint/before-toon), compositor plumb (multi_region coverage flag -> garment shader; descriptor clothing_logos: per-character brand choice), thrds logo stub, manifest style tag + logo_capable. PROOF: tshirt_modern on 6 healthy bodies, 18-assertion suite green, 216-capture chromakey QA. ROUTE GUIDANCE from QA evidence (Q-060): single-reference SD-fit of offset-shells degrades with girth divergence (muscular_m worst 859px clip at 24mm standoff; reference body clean) offset-shell garments should be authored PER BODY (needs per-body masks + runtime <body>_mask.png loading, small change); SD-fit remains right for derived/hand-authored garments sharing one mask (suit, uniform, Fantasy cuts). REMAINING for this ticket: refit tshirt to the 5 repaired forks (T-1090 done add to --only and rerun), per-body shell mode, then the basic set: hoodie, button-down, shorts, jeans, formal pants, jacket + the suit/uniform hand-author proof; creation-screen try-on UI for tints/logos.
Basic-set EXPANSION (user, 2026-07-06 bedtime directive: ''full set of the basics across the board''): wave 2 after the in-flight wave 1 FOOTWEAR: sneakers_modern (trainers, everyday default), shoes_formal (suit pairing), boots_modern (ankle height); SPORT: tank_top (sleeveless, logo-capable), joggers_modern (cuffed, track-stripe tint region), track_jacket (with joggers = the training suit, logo-capable); SWIM: swim_trunks (bright), swimsuit_onepiece (color-block regions). Foot garments follow the peasant_shoes both-feet convention; sole geometry as an offset-shell param extension. Total modern catalogue after wave 2: 17 garments across tops/bottoms/feet/full-body x casual/formal/sport/swim. DEFERRED deliberately: hats/accessories accessory slot path is unexercised and T-721 flags a filter bug; needs daylight, not a 3am debug. Overnight sequence: wave 1 -> wave 2 -> full garment x 11-body fitting matrix (the ''T-1090 fittings'' check, fork bodies especially) -> commits -> gated pushes -> morning report.
Fill-in-the-blanks addendum (user bedtime directive 2 + lead judgment): wave 2 grows to 12 adds sweater_modern (crew pullover), cargo_pants (utility/cyberpunk staple), parka_thrds (cold-weather was absent entirely; ships as the FIRST CANON-BRANDED garment thrds is Braemar''s cold-weather fiber brand per wiki; hip-length to avoid the parked skirt-rigging tier), slides (swim footwear, near-free). PLUS outfit presets: data file of named complete looks (casual/business/sport/beach/worker/sleep as tint-palette presets over garments) authored by the lead at integration makes the catalogue usable for NPC dressing and the shop as OUTFITS, not just items. Post-wave-2 catalogue: 21 modern pieces (tops 8 / bottoms 6 / feet 4 / full-body 2 / swim 3). Hats/accessories remain the deliberate daylight deferral (T-721).', 'Fable-5-window sidequest workstream 2 (user directive 2026-07-06, extends T-1088 scope family): build the clothing ENGINE and a basic set now, park variety for later. Target bar: RimWorld''s original apparel breadth varied enough to not feel limiting. Goal experience: being able to pick and SHOP for clothes colorize + slap logos on casualwear. Basic set: t-shirt (logo-capable), hoodie (logo-capable), button-down shirt, shorts, jeans, formal pants, jacket, colorable uniform, black suit (moved up so officials are includable). Skipped by explicit call: capes, dresses/skirts (parked difficulty tier). Engine deliverables: (1) multi-region tint masks requires Quaternius Source tier files (USD5 Patreon month, one-time download, CC0 retained user purchase action); (2) logo decal region on garment chest UV flat 2D artwork per D-244, diegetic hook into economy generated brands (wiki/economics/corporations/generated_brands.toml) for brand-logo shirts; (3) manifest + CharacterVisualDescriptor category coverage (slots/tints already generic); (4) batch-fit each authored garment to all 11 body types via the validated Sprint-28 Blender Surface Deform pipeline (tooling/blender_*.py). Authoring routes per garment TBD by feasibility pass: derive from owned Fantasy pack meshes (62 parts, e.g. tunic->tshirt sleeve cut, pants->shorts hem cut) vs hand-author low-poly on the 65-bone rig (Garment Tool). Related: T-1088 locomotion sandbox doubles as the garment-under-animation QA rig (cycle outfits through Walk/Sprint/Crouch_Fwd).
Correction (2026-07-06): Source tier is PARTIALLY owned already docs/assets/downloads/ (gitignored vendor archives, present in both checkouts) holds Universal Base Characters[Source] (630MB: full .blend files for all 6 base bodies incl. FullBody + OnlyHead variants, hairstyle .blends origin-at-0 + rigged-to-head-bone, texture masters, Godot engine project) plus UBC[Standard], UAL 1+2[Standard], Modular Character Outfits Fantasy[Standard]. NOT owned: Fantasy Outfits[Source] the garment recolor masks + named vertex groups. Consequence: authoring NEW garments is fully unblocked (author against source body .blends, create our own tint masks/vertex groups no purchase needed); only pack-DERIVED garments'' clean masks would want one more USD5 Patreon month, decision deferred until the feasibility route table says how much derivation we actually use.
Art direction (user, 2026-07-06): garments read CYBERPUNK/MODERN near-future everyday civilian + corporate wear NOT fantasy/medieval. The owned Fantasy pack is at most a topology donor (low-poly base shapes like pants/shirts are genre-neutral; genre lives in silhouette details, textures, trims, and accessories) no medieval trim/textures may survive into shipped garments. Weight the authoring routes accordingly: texture identity matters as much as mesh. Canon hook: the wiki already ships a garment brand thrds (wiki/star-systems/GJ-475/index.md, Braemar fiber cooperative, corridor fashion markets) so branded clothing connects to Phase-1 lore, not only generated_brands.toml. This emphasis also feeds the Quaternius-pivot evaluation: commercial alternatives skew sci-fi/urban while Quaternius clothing is fantasy-only.
Nuance (user, 2026-07-06): the cyberpunk/modern direction is the DEFAULT, not a prohibition fashion in the Reach is cyclical, so fantasy/medieval-flavored garments may appear here and there as deliberate fashion statements (subculture pieces, luxury/revival trims, statement outerwear). Supersedes the previous note''s hard ''no medieval trim may survive'' line. Practical consequence: a small number of Fantasy-pack pieces may ship near-as-is where they read as intentional fashion; the basic set itself stays modern.
Art direction CONSOLIDATED (user, 2026-07-06 supersedes the two notes above): the Reach has no single clothing style ALL styles coexist, with modern/cyberpunk probably the most prevalent. Style is a prevalence DISTRIBUTION, not a genre rule, and may eventually vary by culture/settlement (fits the simulation DNA; fashion-as-cultural-data is a later phase concern, not this ticket). Consequences now: (1) the engine is style-agnostic by construction (slots/tints/logos carry no genre); (2) the basic set targets the prevalent center modern casual + corporate because it dresses the most NPCs and the shopping experience; (3) garments get a lightweight style tag in the manifest (e.g. style: modern|formal|fantasy|utility) as the cheap seam so per-culture distributions can be authored later without retrofitting; (4) Fantasy-pack pieces are legitimate catalogue members under their own tag, prevalence-weighted low.
Scope pin (user, 2026-07-06, final word on style): for simplicity, THIS ticket ships ONE style only cyberpunk/modern. The pluralistic distribution model above remains the long-term truth (keep the manifest style tag as the seam), but the v1 basic set is single-style; other styles are catalogue growth, not scope.
Route challenge (user, 2026-07-06): not sold on staying Quaternius-primary ''Quaternius is limited and will move the wall at too slow a pace; I''d rather pick something that has what we need now.'' Fresh store verification (2026-07-06): Sidekick packs are one-time USD199.99, perpetual, engine-agnostic license (Godot legal, unsupported); NO raw FBX extraction from unitypackage (community-proven: unitypackage_godot, third-party Sidekick Creator Godot plugin incl. body blend shapes); ''Sold out'' labels are a broken-theme artifact per Shopify JSON (needs one browser cart check); SyntyPass rejected (assets stop being developable after cancel). Relevant packs: Starter (FREE, 57+91 parts), Modern Civilians (165 parts = v1 basic set territory), Sci-Fi Civilians (103 parts, cyberpunk accents), Modern Police (uniforms/officials). DECISION NOW GATED ON THE FREE SPIKE: user downloads free Starter Pack -> Bridge-C spike (extract garment -> Robust Weight Transfer onto our rig -> 11-body batch -> animation QA in T-1088 sandbox). Pass -> buy Modern Civilians, Synty = primary garment supply (+UAL1/UAL2 Source ~USD30 for clips no Synty substitute exists for Godot animations; Fantasy Outfits Source drops to low priority under the single-style pin). Fail -> hand-author route stands. Stay-vs-pivot governance record HELD until spike evidence lands.
Scope guard (user concern, 2026-07-06): Synty intake must NOT displace the create-stuff-yourself pipeline the user''s goal includes in-house garment creation capability, not just swapping vendor catalogs. Resolution: three-route portfolio sharing one engine (a) Synty intake = variety bootstrap only; (b) derivation scripting (cuts/merges/retexture, agent-automatable) = create-yourself lite, turns any donor topology into original designs; (c) TRUE AUTHORING on owned UBC Source body .blends = the pipeline proper. GUARANTEE: the colorable uniform and black suit (no donor exists in any pack) are built via route (c) as the authoring pipeline''s end-to-end proof non-negotiable deliverable of this ticket, so the pipeline exists even if Synty covers everything else. Spike status: extraction done inline by lead (158 FBX, sci-fi civilian theme confirmed); DECISIVE finding Sidekick rig is UE-Mannequin-named, 52/65 bone-name overlap with our rig, sample garment binds to 12 vgroups of which 11 match ours exactly (only head->Head case rename) => transplant likely rename-and-rebind, far cheaper than the weight-transfer the pivot eval assumed; body blend shapes travel in the FBX. Transplant phase parked pending API recovery / token-window reset.
ROUTE DECISION (user, 2026-07-06, final): Synty intake REJECTED despite the spike''s technical PASS (1) USD199.99/pack is too expensive; (2) Sidekick modularity is baked body-segment swap (clothes fused to body chunks), incompatible with our layered clothing-over-body system (11 bodies, skin tones, slot swapping); (3) user dislikes the Synty style. The spike still paid: transplant/batch scripts are source-agnostic and become the G1-family production pipeline; T-1090 (5 bodies broken bare) discovered; the Synty question is closed with evidence, not parked. OPERATIVE ROUTE: (b) derive from owned Fantasy parts + (c) hand-author with a new preferred authoring technique: OFFSET-SHELL FROM OUR OWN BODY MESHES (extract body segment surface, offset outward into garment shell, cut neckline/hem/sleeves; weights inherited by construction, style fully ours, USD0). Suit + uniform remain the full hand-author proof. Purchases still open (separate concerns): UAL1/UAL2 Source ~USD30 for animation clips; Fantasy Outfits Source USD20 low-priority convenience.
Chromakey garment-QA harness DELIVERED (2026-07-06, commits eaca6c8c4/74fa16260/b09a50efb): two-pass depth-proximity clip detection pass A keys covered body segments magenta, pass B re-renders garment-only with a depth-only 3cm epsilon bias toward camera; analyzer splits key pixels into exposed_skin (informational sleeveless arms, collars) vs clip_through (gating skin within 3cm in front of cloth). Run: tooling/garment-qa/run-garment-qa <config>. Verified on the peasant set (avg_m/f x Walk/Sprint/Crouch_Fwd x 3 frames x 4 yaws): 0/18 front-view false positives; genuine findings = sleeveless armhole seam (avg_f, sides), deep-crouch waist gap (worst 150px, back), collar nape. First real fit observations already on record for the peasant set. Sensitivity: clip_epsilon_m + --min-pixels, to calibrate against the first modern garments. This is the acceptance gate for every garment the wardrobe pipeline produces; pair with the T-1088 sandbox for in-world walk QA.
ENGINE DELIVERED (2026-07-06, commits e22ea0fa4 + 278e8b7c2; earlier eaca6c8c4/74fa16260/b09a50efb chromakey, b54b8189d bodies): G1 skinned batch-fit (blender_batch_fit_skinned.py), offset-shell authoring (blender_author_offset_shell.py route c create-yourself, weights by construction, procedural RGBA masks, UV2 chest channel), toon_garment.gdshader (channel-blended 4-region tint + logo after-tint/before-toon), compositor plumb (multi_region coverage flag -> garment shader; descriptor clothing_logos: per-character brand choice), thrds logo stub, manifest style tag + logo_capable. PROOF: tshirt_modern on 6 healthy bodies, 18-assertion suite green, 216-capture chromakey QA. ROUTE GUIDANCE from QA evidence (Q-060): single-reference SD-fit of offset-shells degrades with girth divergence (muscular_m worst 859px clip at 24mm standoff; reference body clean) offset-shell garments should be authored PER BODY (needs per-body masks + runtime <body>_mask.png loading, small change); SD-fit remains right for derived/hand-authored garments sharing one mask (suit, uniform, Fantasy cuts). REMAINING for this ticket: refit tshirt to the 5 repaired forks (T-1090 done add to --only and rerun), per-body shell mode, then the basic set: hoodie, button-down, shorts, jeans, formal pants, jacket + the suit/uniform hand-author proof; creation-screen try-on UI for tints/logos.
Basic-set EXPANSION (user, 2026-07-06 bedtime directive: ''full set of the basics across the board''): wave 2 after the in-flight wave 1 FOOTWEAR: sneakers_modern (trainers, everyday default), shoes_formal (suit pairing), boots_modern (ankle height); SPORT: tank_top (sleeveless, logo-capable), joggers_modern (cuffed, track-stripe tint region), track_jacket (with joggers = the training suit, logo-capable); SWIM: swim_trunks (bright), swimsuit_onepiece (color-block regions). Foot garments follow the peasant_shoes both-feet convention; sole geometry as an offset-shell param extension. Total modern catalogue after wave 2: 17 garments across tops/bottoms/feet/full-body x casual/formal/sport/swim. DEFERRED deliberately: hats/accessories accessory slot path is unexercised and T-721 flags a filter bug; needs daylight, not a 3am debug. Overnight sequence: wave 1 -> wave 2 -> full garment x 11-body fitting matrix (the ''T-1090 fittings'' check, fork bodies especially) -> commits -> gated pushes -> morning report.
Fill-in-the-blanks addendum (user bedtime directive 2 + lead judgment): wave 2 grows to 12 adds sweater_modern (crew pullover), cargo_pants (utility/cyberpunk staple), parka_thrds (cold-weather was absent entirely; ships as the FIRST CANON-BRANDED garment thrds is Braemar''s cold-weather fiber brand per wiki; hip-length to avoid the parked skirt-rigging tier), slides (swim footwear, near-free). PLUS outfit presets: data file of named complete looks (casual/business/sport/beach/worker/sleep as tint-palette presets over garments) authored by the lead at integration makes the catalogue usable for NPC dressing and the shop as OUTFITS, not just items. Post-wave-2 catalogue: 21 modern pieces (tops 8 / bottoms 6 / feet 4 / full-body 2 / swim 3). Hats/accessories remain the deliberate daylight deferral (T-721).
ACCEPTED for first release (user review, 2026-07-07): full catalogue + lookbook approved; toe-box fix reviewed ''I still see some toes, but this will do''. Residual toe hints on closed footwear accepted as first-release quality; further toe-box smoothing is polish backlog, not scope. Closing.', NULL, '2026-07-07 12:04:38', '2026-07-07 12:04:38.700', '2026-07-07 12:04:38.700', NULL, '8fd724c63e771a5cef9dd2120aa022ec', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKCPBF41RWK9PW9M22Q7961C', 'status', 'review', 'done', NULL, '2026-07-07 12:04:49', '2026-07-07 12:04:49.833', '2026-07-07 12:04:49.833', NULL, '69ad0ce426a5cf65efe7df5dc06161ef', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKR283RTWYXD2M005K65S52M', 'status', 'backlog', 'done', NULL, '2026-07-07 12:08:15', '2026-07-07 12:08:15.679', '2026-07-07 12:08:15.679', NULL, 'abf3b8af8fd454d6a75ca095b6d57908', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKCER95EK266H8RWPW8XT1F0', 'description', 'Fable-5-window sidequest (sanctioned cascade exception, 2026-07-05): the presentation layer that turns discrete server tile steps into a believable walking 3D character. Scope: 3D sandbox dev scene (SR_LIVE=1, greybox tiles derived from server snapshots, no hand-authored map), CharacterVisual instantiated in-world, locomotion rig (tile-step + facing-octant interpolation, stance-aware speeds), UAL animation state machine (idle/walk/sprint/careful/crouch), D-148 camera rig (30deg low-angle follow, 45deg rotation). Server-authoritative from day one — no client-local movement model. Builds on D-148/D-149/D-244/D-066 and the T-417 stance system. If the Fable 5 window closes mid-flight, work parks here.
Visual reference (user, 2026-07-06): Xenonauts 2 nails the target tiles/occlusion/character perspective readable tile grid, low-angle tactical camera with angle options (matches D-148 30deg low-angle + 45deg rotation steps and the spikes/3dpipeline angle presets), and crucially its wall/roof occlusion treatment: camera-facing walls cut down/ghosted so interiors stay readable, floors peel per z-level. One deliberate difference: our camera locks to the player character (D-015/T-116) no free map panning. Implication for the locomotion sandbox: greybox walls should prototype camera-facing cutaway occlusion early, since wall readability interacts with the locked low-angle camera.
Scoping call (user + lead agreement, 2026-07-06): wall cutaway is CLIENT-SIDE presentation, decoupled from fog-of-perception. Rationale: two occlusion systems with different subjects perception/fog governs what the CHARACTER knows (server-enforced info boundary; client only receives observer-filtered geometry), cutaway governs what the PLAYER''S CAMERA sees (pure render mode on wall data the client already holds). They compose: cutaway cuts render height; fog overlay still gates what is visible behind. Walls remain first-class client data objects (future destruction/explosion interactions), cutaway is a render mode on them like tint/LOD. Nuance parked for Phase 5 proper: destroyed-while-unobserved walls should render in remembered state (fog-memory applied to geometry). If this holds through implementation, mint a D-record (perception domain) when the sidequest lands.
Animation supply resolved (2026-07-06): user purchased UAL1[Source+Pro] (120 clips, identical lists) + UAL2[Source] (134 clips), unzipped in docs/assets/downloads/. Gains: Turn90_L/R + Turn180_L/R (turn-in-place), Sprint_Enter/Exit + Crouch_Enter/Exit (stance transitions), full 8-direction Walk/Jog/Crouch sets incl. diagonals, Counter_Idle + talking/phone/sitting idles (life-sim verbs), plus _RM root-motion variants (unused in-place is correct for server-authoritative interpolation). Still absent everywhere: walk start/stop + stairs the only remaining custom clips. Integration is NOT a drop-in swap: upstream renamed clips (Walk_Loop -> Walk_Fwd_Loop etc.), so wire via the design''s recorded plan explicit library names + lib/Clip addressing + gait-table update. NOTE for facing design: the 8-dir walk sets unlock Q-084''s parked walk-vs-aim split (direction-matched clips relative to facing) revisit during the S9 tuning session.
Live-session feedback (user, 2026-07-06): (1) Corridor cutaway verdict two minds: ''extremely trippy, could be the game''s own look'' vs ''player cannot see what is in the room unless close''. Resolution: cutaway MODES, toggled live in the sandbox (corridor / bottom-walls-low / both) bottom-wall rule is data-driven (wall drops to stub when a KNOWN floor tile sits on its camera-far side; camera-facing directions derived from WorldRoot basis at runtime, not hand-derived). Default picked by feel during the session; the trippy corridor stays available as a signature-look option. (2) NEW: mouse-over move-here marker on the hovered tile + optimal path line client-side A* over the never-evict KNOWN tile store (info-boundary-honest: the character plans only through known tiles; fog is unpathable), 8-dir with diagonal step cost 1 (time-optimal per the no-sqrt2 wire), terrain-cost provider seam (uniform today, Phase-4 terrain later), click executes by streaming ordinary Move* step commands through the existing throttle (zero protocol change, server validates every step), cancelled by WASD override / path invalidation / arrival. Settles more of Q-084 (click-to-move half).
RMB gesture vocabulary (user, live session 2026-07-06, added to the path-preview feature): single right-click = walk there (current stance); DOUBLE right-click = sprint there (ladder ToggleStanceUp burst to Sprint via the ordinary command queue, pre-sprint stance RESTORED on arrival; a double upgrades the already-started follow in place); LONG-PRESS right-click (~0.4s, commit on release) = ''take cover at location'', prototyped as go-there-then-Crouch-on-arrival (no restore crouching was the point). Real cover mechanics (wall adjacency, directional protection) are future combat design; the gesture is the input-vocabulary prototype. WASD always cancels any follow without stance restore (player override is deliberate).
Follow-facing refinement (user, live session 2026-07-06): during an RMB path-follow the BODY commits to the leg/movement direction (mouse no longer swings body yaw); the mouse instead drives a LAYERED LOOK-AT Head bone (~70deg limit) plus torso twist via a spine bone (~30deg) for looking far lateral/behind; beyond their sum the character physically cannot look further without turning (correct). Pure client presentation on the D-249 model; SetFacing continues to ride the wire during follows, so the SERVER vision cone still follows the mouse (diegetic: the character looks where the player points while walking a committed path). Implementation: Godot LookAtModifier3D pair under the CharacterVisual skeleton, influence-faded in/out on follow start/end; rig gains a commit_body_to_motion flag (yaw from leg velocity while set).', 'Fable-5-window sidequest (sanctioned cascade exception, 2026-07-05): the presentation layer that turns discrete server tile steps into a believable walking 3D character. Scope: 3D sandbox dev scene (SR_LIVE=1, greybox tiles derived from server snapshots, no hand-authored map), CharacterVisual instantiated in-world, locomotion rig (tile-step + facing-octant interpolation, stance-aware speeds), UAL animation state machine (idle/walk/sprint/careful/crouch), D-148 camera rig (30deg low-angle follow, 45deg rotation). Server-authoritative from day one no client-local movement model. Builds on D-148/D-149/D-244/D-066 and the T-417 stance system. If the Fable 5 window closes mid-flight, work parks here.
Visual reference (user, 2026-07-06): Xenonauts 2 nails the target tiles/occlusion/character perspective readable tile grid, low-angle tactical camera with angle options (matches D-148 30deg low-angle + 45deg rotation steps and the spikes/3dpipeline angle presets), and crucially its wall/roof occlusion treatment: camera-facing walls cut down/ghosted so interiors stay readable, floors peel per z-level. One deliberate difference: our camera locks to the player character (D-015/T-116) no free map panning. Implication for the locomotion sandbox: greybox walls should prototype camera-facing cutaway occlusion early, since wall readability interacts with the locked low-angle camera.
Scoping call (user + lead agreement, 2026-07-06): wall cutaway is CLIENT-SIDE presentation, decoupled from fog-of-perception. Rationale: two occlusion systems with different subjects perception/fog governs what the CHARACTER knows (server-enforced info boundary; client only receives observer-filtered geometry), cutaway governs what the PLAYER''S CAMERA sees (pure render mode on wall data the client already holds). They compose: cutaway cuts render height; fog overlay still gates what is visible behind. Walls remain first-class client data objects (future destruction/explosion interactions), cutaway is a render mode on them like tint/LOD. Nuance parked for Phase 5 proper: destroyed-while-unobserved walls should render in remembered state (fog-memory applied to geometry). If this holds through implementation, mint a D-record (perception domain) when the sidequest lands.
Animation supply resolved (2026-07-06): user purchased UAL1[Source+Pro] (120 clips, identical lists) + UAL2[Source] (134 clips), unzipped in docs/assets/downloads/. Gains: Turn90_L/R + Turn180_L/R (turn-in-place), Sprint_Enter/Exit + Crouch_Enter/Exit (stance transitions), full 8-direction Walk/Jog/Crouch sets incl. diagonals, Counter_Idle + talking/phone/sitting idles (life-sim verbs), plus _RM root-motion variants (unused in-place is correct for server-authoritative interpolation). Still absent everywhere: walk start/stop + stairs the only remaining custom clips. Integration is NOT a drop-in swap: upstream renamed clips (Walk_Loop -> Walk_Fwd_Loop etc.), so wire via the design''s recorded plan explicit library names + lib/Clip addressing + gait-table update. NOTE for facing design: the 8-dir walk sets unlock Q-084''s parked walk-vs-aim split (direction-matched clips relative to facing) revisit during the S9 tuning session.
Live-session feedback (user, 2026-07-06): (1) Corridor cutaway verdict two minds: ''extremely trippy, could be the game''s own look'' vs ''player cannot see what is in the room unless close''. Resolution: cutaway MODES, toggled live in the sandbox (corridor / bottom-walls-low / both) bottom-wall rule is data-driven (wall drops to stub when a KNOWN floor tile sits on its camera-far side; camera-facing directions derived from WorldRoot basis at runtime, not hand-derived). Default picked by feel during the session; the trippy corridor stays available as a signature-look option. (2) NEW: mouse-over move-here marker on the hovered tile + optimal path line client-side A* over the never-evict KNOWN tile store (info-boundary-honest: the character plans only through known tiles; fog is unpathable), 8-dir with diagonal step cost 1 (time-optimal per the no-sqrt2 wire), terrain-cost provider seam (uniform today, Phase-4 terrain later), click executes by streaming ordinary Move* step commands through the existing throttle (zero protocol change, server validates every step), cancelled by WASD override / path invalidation / arrival. Settles more of Q-084 (click-to-move half).
RMB gesture vocabulary (user, live session 2026-07-06, added to the path-preview feature): single right-click = walk there (current stance); DOUBLE right-click = sprint there (ladder ToggleStanceUp burst to Sprint via the ordinary command queue, pre-sprint stance RESTORED on arrival; a double upgrades the already-started follow in place); LONG-PRESS right-click (~0.4s, commit on release) = ''take cover at location'', prototyped as go-there-then-Crouch-on-arrival (no restore crouching was the point). Real cover mechanics (wall adjacency, directional protection) are future combat design; the gesture is the input-vocabulary prototype. WASD always cancels any follow without stance restore (player override is deliberate).
Follow-facing refinement (user, live session 2026-07-06): during an RMB path-follow the BODY commits to the leg/movement direction (mouse no longer swings body yaw); the mouse instead drives a LAYERED LOOK-AT Head bone (~70deg limit) plus torso twist via a spine bone (~30deg) for looking far lateral/behind; beyond their sum the character physically cannot look further without turning (correct). Pure client presentation on the D-249 model; SetFacing continues to ride the wire during follows, so the SERVER vision cone still follows the mouse (diegetic: the character looks where the player points while walking a committed path). Implementation: Godot LookAtModifier3D pair under the CharacterVisual skeleton, influence-faded in/out on follow start/end; rig gains a commit_body_to_motion flag (yaw from leg velocity while set).
SIDEQUEST CLOSE-OUT (2026-07-07): all scope delivered and pushed sandbox (scene, greybox store, rig, gait machine, camera, cutaway modes w/ user-verdict mode-2 default recorded as D-250 amendment), RMB navigation vocabulary, layered head/torso look-at, D-252 view/movement protocol split (server+client), purchased UAL tiers wired (turn-in-place, stance enter/exits), design doc committed to docs/design/t1088-locomotion-sandbox-design.md (the ''design §'' references in sandbox code resolve in-repo now). Design-delta close-out notes: no new Gauntlet room was needed (constraint 12 never triggered, server untouched except D-252); ~40 lines of main.gd boot boilerplate duplicated with Pattern: markers for the T-962 extraction; seek-during-blend + remove_immutable_tracks caveats did not manifest in practice; ual_extended wiring superseded by the purchased-tier ual1/ual2 libraries; 2D select_yaw_target moving-branch is dead code post-D-252 (cleanup rides Phase 5). Remaining feel work parked as T-1096 (S9 live tuning needs the user). Closing.', NULL, '2026-07-07 12:10:20', '2026-07-07 12:10:20.956', '2026-07-07 12:10:20.956', NULL, '846e89916f601a4622a03b275f200bc6', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKCER95EK266H8RWPW8XT1F0', 'status', 'backlog', 'done', NULL, '2026-07-07 12:10:20', '2026-07-07 12:10:20.986', '2026-07-07 12:10:20.986', NULL, '3016244358c491462e72331ef063ed35', 2) ON CONFLICT(hash) DO NOTHING;
+1
View File
@@ -9,3 +9,4 @@ INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKHFRCP5R0VBA9BKR97RRBJ0', 'T-1093', '2026-07-06 18:44:32.562', '2026-07-06 18:44:32.562', NULL, 'ce581695739eea8cd11433cb9f6c86d1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKJ25KG3ADM4M5XRXCVR2GQG', 'T-1094', '2026-07-06 20:04:59.393', '2026-07-06 20:04:59.393', NULL, 'e856fa3f89f621007a058db26aff2a05', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKR283RTWYXD2M005K65S52M', 'T-1095', '2026-07-07 10:04:11.591', '2026-07-07 10:04:11.591', NULL, 'e0a6ad2c4dce20a444a5af47d49b36c7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKRZ29KH70NFAY0Z1GCVR4N0', 'T-1096', '2026-07-07 12:10:06.109', '2026-07-07 12:10:06.109', NULL, 'bdf196e11c21f894bf01c08b10371ab7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
+86
View File
@@ -817,3 +817,89 @@ Basic-set EXPANSION (user, 2026-07-06 bedtime directive: ''full set of the basic
Fill-in-the-blanks addendum (user bedtime directive 2 + lead judgment): wave 2 grows to 12 adds sweater_modern (crew pullover), cargo_pants (utility/cyberpunk staple), parka_thrds (cold-weather was absent entirely; ships as the FIRST CANON-BRANDED garment thrds is Braemar''s cold-weather fiber brand per wiki; hip-length to avoid the parked skirt-rigging tier), slides (swim footwear, near-free). PLUS outfit presets: data file of named complete looks (casual/business/sport/beach/worker/sleep as tint-palette presets over garments) authored by the lead at integration makes the catalogue usable for NPC dressing and the shop as OUTFITS, not just items. Post-wave-2 catalogue: 21 modern pieces (tops 8 / bottoms 6 / feet 4 / full-body 2 / swim 3). Hats/accessories remain the deliberate daylight deferral (T-721).', 'review', 'high', NULL, 'client', NULL, '2026-07-06 07:34:18.656', '2026-07-07 09:23:38.257', NULL, '15fb82c297b4afafa017d7247c7ba67e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKR283RTWYXD2M005K65S52M', 'bug', '06FB0TNSRXX29H5X0EB0F0GBW4', 'head_001 template renders grey (untinted) on child/teen/muscular bodies', 'Found by the T-1089 lookbook pass (2026-07-07): overlay head templates (head_001) render grey faces on child/teen/muscular bodies — the template appears not to receive the body''s skin-tone tint, while each body''s own seg_head renders correctly. Workaround used in the lookbook: head_id empty -> body seg_head shows. If head templates are meant to be body-agnostic (D-161 head-as-BoneAttachment3D), the skin-tone shader application needs to cover template heads per body; investigate _apply skin tone path in character_visual.gd for the head template branch.', 'backlog', 'medium', NULL, 'client', NULL, '2026-07-07 10:04:11.590', '2026-07-07 10:04:11.590', NULL, 'fee4a7bdc5705284493e52c31dd588c1', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKCPBF41RWK9PW9M22Q7961C', 'story', '06FB0TNSRXX29H5X0EB0F0GBW4', 'Clothing catalogue engine + basic set — recolorable, logo-capable everyday garments', 'Fable-5-window sidequest workstream 2 (user directive 2026-07-06, extends T-1088 scope family): build the clothing ENGINE and a basic set now, park variety for later. Target bar: RimWorld''s original apparel breadth — varied enough to not feel limiting. Goal experience: being able to pick and SHOP for clothes — colorize + slap logos on casualwear. Basic set: t-shirt (logo-capable), hoodie (logo-capable), button-down shirt, shorts, jeans, formal pants, jacket, colorable uniform, black suit (moved up so officials are includable). Skipped by explicit call: capes, dresses/skirts (parked — difficulty tier). Engine deliverables: (1) multi-region tint masks — requires Quaternius Source tier files (USD5 Patreon month, one-time download, CC0 retained — user purchase action); (2) logo decal region on garment chest UV — flat 2D artwork per D-244, diegetic hook into economy generated brands (wiki/economics/corporations/generated_brands.toml) for brand-logo shirts; (3) manifest + CharacterVisualDescriptor category coverage (slots/tints already generic); (4) batch-fit each authored garment to all 11 body types via the validated Sprint-28 Blender Surface Deform pipeline (tooling/blender_*.py). Authoring routes per garment TBD by feasibility pass: derive from owned Fantasy pack meshes (62 parts, e.g. tunic->tshirt sleeve cut, pants->shorts hem cut) vs hand-author low-poly on the 65-bone rig (Garment Tool). Related: T-1088 locomotion sandbox doubles as the garment-under-animation QA rig (cycle outfits through Walk/Sprint/Crouch_Fwd).
Correction (2026-07-06): Source tier is PARTIALLY owned already docs/assets/downloads/ (gitignored vendor archives, present in both checkouts) holds Universal Base Characters[Source] (630MB: full .blend files for all 6 base bodies incl. FullBody + OnlyHead variants, hairstyle .blends origin-at-0 + rigged-to-head-bone, texture masters, Godot engine project) plus UBC[Standard], UAL 1+2[Standard], Modular Character Outfits Fantasy[Standard]. NOT owned: Fantasy Outfits[Source] the garment recolor masks + named vertex groups. Consequence: authoring NEW garments is fully unblocked (author against source body .blends, create our own tint masks/vertex groups no purchase needed); only pack-DERIVED garments'' clean masks would want one more USD5 Patreon month, decision deferred until the feasibility route table says how much derivation we actually use.
Art direction (user, 2026-07-06): garments read CYBERPUNK/MODERN near-future everyday civilian + corporate wear NOT fantasy/medieval. The owned Fantasy pack is at most a topology donor (low-poly base shapes like pants/shirts are genre-neutral; genre lives in silhouette details, textures, trims, and accessories) no medieval trim/textures may survive into shipped garments. Weight the authoring routes accordingly: texture identity matters as much as mesh. Canon hook: the wiki already ships a garment brand thrds (wiki/star-systems/GJ-475/index.md, Braemar fiber cooperative, corridor fashion markets) so branded clothing connects to Phase-1 lore, not only generated_brands.toml. This emphasis also feeds the Quaternius-pivot evaluation: commercial alternatives skew sci-fi/urban while Quaternius clothing is fantasy-only.
Nuance (user, 2026-07-06): the cyberpunk/modern direction is the DEFAULT, not a prohibition fashion in the Reach is cyclical, so fantasy/medieval-flavored garments may appear here and there as deliberate fashion statements (subculture pieces, luxury/revival trims, statement outerwear). Supersedes the previous note''s hard ''no medieval trim may survive'' line. Practical consequence: a small number of Fantasy-pack pieces may ship near-as-is where they read as intentional fashion; the basic set itself stays modern.
Art direction CONSOLIDATED (user, 2026-07-06 supersedes the two notes above): the Reach has no single clothing style ALL styles coexist, with modern/cyberpunk probably the most prevalent. Style is a prevalence DISTRIBUTION, not a genre rule, and may eventually vary by culture/settlement (fits the simulation DNA; fashion-as-cultural-data is a later phase concern, not this ticket). Consequences now: (1) the engine is style-agnostic by construction (slots/tints/logos carry no genre); (2) the basic set targets the prevalent center modern casual + corporate because it dresses the most NPCs and the shopping experience; (3) garments get a lightweight style tag in the manifest (e.g. style: modern|formal|fantasy|utility) as the cheap seam so per-culture distributions can be authored later without retrofitting; (4) Fantasy-pack pieces are legitimate catalogue members under their own tag, prevalence-weighted low.
Scope pin (user, 2026-07-06, final word on style): for simplicity, THIS ticket ships ONE style only cyberpunk/modern. The pluralistic distribution model above remains the long-term truth (keep the manifest style tag as the seam), but the v1 basic set is single-style; other styles are catalogue growth, not scope.
Route challenge (user, 2026-07-06): not sold on staying Quaternius-primary ''Quaternius is limited and will move the wall at too slow a pace; I''d rather pick something that has what we need now.'' Fresh store verification (2026-07-06): Sidekick packs are one-time USD199.99, perpetual, engine-agnostic license (Godot legal, unsupported); NO raw FBX extraction from unitypackage (community-proven: unitypackage_godot, third-party Sidekick Creator Godot plugin incl. body blend shapes); ''Sold out'' labels are a broken-theme artifact per Shopify JSON (needs one browser cart check); SyntyPass rejected (assets stop being developable after cancel). Relevant packs: Starter (FREE, 57+91 parts), Modern Civilians (165 parts = v1 basic set territory), Sci-Fi Civilians (103 parts, cyberpunk accents), Modern Police (uniforms/officials). DECISION NOW GATED ON THE FREE SPIKE: user downloads free Starter Pack -> Bridge-C spike (extract garment -> Robust Weight Transfer onto our rig -> 11-body batch -> animation QA in T-1088 sandbox). Pass -> buy Modern Civilians, Synty = primary garment supply (+UAL1/UAL2 Source ~USD30 for clips no Synty substitute exists for Godot animations; Fantasy Outfits Source drops to low priority under the single-style pin). Fail -> hand-author route stands. Stay-vs-pivot governance record HELD until spike evidence lands.
Scope guard (user concern, 2026-07-06): Synty intake must NOT displace the create-stuff-yourself pipeline the user''s goal includes in-house garment creation capability, not just swapping vendor catalogs. Resolution: three-route portfolio sharing one engine (a) Synty intake = variety bootstrap only; (b) derivation scripting (cuts/merges/retexture, agent-automatable) = create-yourself lite, turns any donor topology into original designs; (c) TRUE AUTHORING on owned UBC Source body .blends = the pipeline proper. GUARANTEE: the colorable uniform and black suit (no donor exists in any pack) are built via route (c) as the authoring pipeline''s end-to-end proof non-negotiable deliverable of this ticket, so the pipeline exists even if Synty covers everything else. Spike status: extraction done inline by lead (158 FBX, sci-fi civilian theme confirmed); DECISIVE finding Sidekick rig is UE-Mannequin-named, 52/65 bone-name overlap with our rig, sample garment binds to 12 vgroups of which 11 match ours exactly (only head->Head case rename) => transplant likely rename-and-rebind, far cheaper than the weight-transfer the pivot eval assumed; body blend shapes travel in the FBX. Transplant phase parked pending API recovery / token-window reset.
ROUTE DECISION (user, 2026-07-06, final): Synty intake REJECTED despite the spike''s technical PASS (1) USD199.99/pack is too expensive; (2) Sidekick modularity is baked body-segment swap (clothes fused to body chunks), incompatible with our layered clothing-over-body system (11 bodies, skin tones, slot swapping); (3) user dislikes the Synty style. The spike still paid: transplant/batch scripts are source-agnostic and become the G1-family production pipeline; T-1090 (5 bodies broken bare) discovered; the Synty question is closed with evidence, not parked. OPERATIVE ROUTE: (b) derive from owned Fantasy parts + (c) hand-author with a new preferred authoring technique: OFFSET-SHELL FROM OUR OWN BODY MESHES (extract body segment surface, offset outward into garment shell, cut neckline/hem/sleeves; weights inherited by construction, style fully ours, USD0). Suit + uniform remain the full hand-author proof. Purchases still open (separate concerns): UAL1/UAL2 Source ~USD30 for animation clips; Fantasy Outfits Source USD20 low-priority convenience.
Chromakey garment-QA harness DELIVERED (2026-07-06, commits eaca6c8c4/74fa16260/b09a50efb): two-pass depth-proximity clip detection pass A keys covered body segments magenta, pass B re-renders garment-only with a depth-only 3cm epsilon bias toward camera; analyzer splits key pixels into exposed_skin (informational sleeveless arms, collars) vs clip_through (gating skin within 3cm in front of cloth). Run: tooling/garment-qa/run-garment-qa <config>. Verified on the peasant set (avg_m/f x Walk/Sprint/Crouch_Fwd x 3 frames x 4 yaws): 0/18 front-view false positives; genuine findings = sleeveless armhole seam (avg_f, sides), deep-crouch waist gap (worst 150px, back), collar nape. First real fit observations already on record for the peasant set. Sensitivity: clip_epsilon_m + --min-pixels, to calibrate against the first modern garments. This is the acceptance gate for every garment the wardrobe pipeline produces; pair with the T-1088 sandbox for in-world walk QA.
ENGINE DELIVERED (2026-07-06, commits e22ea0fa4 + 278e8b7c2; earlier eaca6c8c4/74fa16260/b09a50efb chromakey, b54b8189d bodies): G1 skinned batch-fit (blender_batch_fit_skinned.py), offset-shell authoring (blender_author_offset_shell.py route c create-yourself, weights by construction, procedural RGBA masks, UV2 chest channel), toon_garment.gdshader (channel-blended 4-region tint + logo after-tint/before-toon), compositor plumb (multi_region coverage flag -> garment shader; descriptor clothing_logos: per-character brand choice), thrds logo stub, manifest style tag + logo_capable. PROOF: tshirt_modern on 6 healthy bodies, 18-assertion suite green, 216-capture chromakey QA. ROUTE GUIDANCE from QA evidence (Q-060): single-reference SD-fit of offset-shells degrades with girth divergence (muscular_m worst 859px clip at 24mm standoff; reference body clean) offset-shell garments should be authored PER BODY (needs per-body masks + runtime <body>_mask.png loading, small change); SD-fit remains right for derived/hand-authored garments sharing one mask (suit, uniform, Fantasy cuts). REMAINING for this ticket: refit tshirt to the 5 repaired forks (T-1090 done add to --only and rerun), per-body shell mode, then the basic set: hoodie, button-down, shorts, jeans, formal pants, jacket + the suit/uniform hand-author proof; creation-screen try-on UI for tints/logos.
Basic-set EXPANSION (user, 2026-07-06 bedtime directive: ''full set of the basics across the board''): wave 2 after the in-flight wave 1 FOOTWEAR: sneakers_modern (trainers, everyday default), shoes_formal (suit pairing), boots_modern (ankle height); SPORT: tank_top (sleeveless, logo-capable), joggers_modern (cuffed, track-stripe tint region), track_jacket (with joggers = the training suit, logo-capable); SWIM: swim_trunks (bright), swimsuit_onepiece (color-block regions). Foot garments follow the peasant_shoes both-feet convention; sole geometry as an offset-shell param extension. Total modern catalogue after wave 2: 17 garments across tops/bottoms/feet/full-body x casual/formal/sport/swim. DEFERRED deliberately: hats/accessories accessory slot path is unexercised and T-721 flags a filter bug; needs daylight, not a 3am debug. Overnight sequence: wave 1 -> wave 2 -> full garment x 11-body fitting matrix (the ''T-1090 fittings'' check, fork bodies especially) -> commits -> gated pushes -> morning report.
Fill-in-the-blanks addendum (user bedtime directive 2 + lead judgment): wave 2 grows to 12 adds sweater_modern (crew pullover), cargo_pants (utility/cyberpunk staple), parka_thrds (cold-weather was absent entirely; ships as the FIRST CANON-BRANDED garment thrds is Braemar''s cold-weather fiber brand per wiki; hip-length to avoid the parked skirt-rigging tier), slides (swim footwear, near-free). PLUS outfit presets: data file of named complete looks (casual/business/sport/beach/worker/sleep as tint-palette presets over garments) authored by the lead at integration makes the catalogue usable for NPC dressing and the shop as OUTFITS, not just items. Post-wave-2 catalogue: 21 modern pieces (tops 8 / bottoms 6 / feet 4 / full-body 2 / swim 3). Hats/accessories remain the deliberate daylight deferral (T-721).
ACCEPTED for first release (user review, 2026-07-07): full catalogue + lookbook approved; toe-box fix reviewed ''I still see some toes, but this will do''. Residual toe hints on closed footwear accepted as first-release quality; further toe-box smoothing is polish backlog, not scope. Closing.', 'review', 'high', NULL, 'client', NULL, '2026-07-06 07:34:18.656', '2026-07-07 12:04:38.699', NULL, 'fe689e46ed80988afbb62827b85c5a9f', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKCPBF41RWK9PW9M22Q7961C', 'story', '06FB0TNSRXX29H5X0EB0F0GBW4', 'Clothing catalogue engine + basic set — recolorable, logo-capable everyday garments', 'Fable-5-window sidequest workstream 2 (user directive 2026-07-06, extends T-1088 scope family): build the clothing ENGINE and a basic set now, park variety for later. Target bar: RimWorld''s original apparel breadth — varied enough to not feel limiting. Goal experience: being able to pick and SHOP for clothes — colorize + slap logos on casualwear. Basic set: t-shirt (logo-capable), hoodie (logo-capable), button-down shirt, shorts, jeans, formal pants, jacket, colorable uniform, black suit (moved up so officials are includable). Skipped by explicit call: capes, dresses/skirts (parked — difficulty tier). Engine deliverables: (1) multi-region tint masks — requires Quaternius Source tier files (USD5 Patreon month, one-time download, CC0 retained — user purchase action); (2) logo decal region on garment chest UV — flat 2D artwork per D-244, diegetic hook into economy generated brands (wiki/economics/corporations/generated_brands.toml) for brand-logo shirts; (3) manifest + CharacterVisualDescriptor category coverage (slots/tints already generic); (4) batch-fit each authored garment to all 11 body types via the validated Sprint-28 Blender Surface Deform pipeline (tooling/blender_*.py). Authoring routes per garment TBD by feasibility pass: derive from owned Fantasy pack meshes (62 parts, e.g. tunic->tshirt sleeve cut, pants->shorts hem cut) vs hand-author low-poly on the 65-bone rig (Garment Tool). Related: T-1088 locomotion sandbox doubles as the garment-under-animation QA rig (cycle outfits through Walk/Sprint/Crouch_Fwd).
Correction (2026-07-06): Source tier is PARTIALLY owned already docs/assets/downloads/ (gitignored vendor archives, present in both checkouts) holds Universal Base Characters[Source] (630MB: full .blend files for all 6 base bodies incl. FullBody + OnlyHead variants, hairstyle .blends origin-at-0 + rigged-to-head-bone, texture masters, Godot engine project) plus UBC[Standard], UAL 1+2[Standard], Modular Character Outfits Fantasy[Standard]. NOT owned: Fantasy Outfits[Source] the garment recolor masks + named vertex groups. Consequence: authoring NEW garments is fully unblocked (author against source body .blends, create our own tint masks/vertex groups no purchase needed); only pack-DERIVED garments'' clean masks would want one more USD5 Patreon month, decision deferred until the feasibility route table says how much derivation we actually use.
Art direction (user, 2026-07-06): garments read CYBERPUNK/MODERN near-future everyday civilian + corporate wear NOT fantasy/medieval. The owned Fantasy pack is at most a topology donor (low-poly base shapes like pants/shirts are genre-neutral; genre lives in silhouette details, textures, trims, and accessories) no medieval trim/textures may survive into shipped garments. Weight the authoring routes accordingly: texture identity matters as much as mesh. Canon hook: the wiki already ships a garment brand thrds (wiki/star-systems/GJ-475/index.md, Braemar fiber cooperative, corridor fashion markets) so branded clothing connects to Phase-1 lore, not only generated_brands.toml. This emphasis also feeds the Quaternius-pivot evaluation: commercial alternatives skew sci-fi/urban while Quaternius clothing is fantasy-only.
Nuance (user, 2026-07-06): the cyberpunk/modern direction is the DEFAULT, not a prohibition fashion in the Reach is cyclical, so fantasy/medieval-flavored garments may appear here and there as deliberate fashion statements (subculture pieces, luxury/revival trims, statement outerwear). Supersedes the previous note''s hard ''no medieval trim may survive'' line. Practical consequence: a small number of Fantasy-pack pieces may ship near-as-is where they read as intentional fashion; the basic set itself stays modern.
Art direction CONSOLIDATED (user, 2026-07-06 supersedes the two notes above): the Reach has no single clothing style ALL styles coexist, with modern/cyberpunk probably the most prevalent. Style is a prevalence DISTRIBUTION, not a genre rule, and may eventually vary by culture/settlement (fits the simulation DNA; fashion-as-cultural-data is a later phase concern, not this ticket). Consequences now: (1) the engine is style-agnostic by construction (slots/tints/logos carry no genre); (2) the basic set targets the prevalent center modern casual + corporate because it dresses the most NPCs and the shopping experience; (3) garments get a lightweight style tag in the manifest (e.g. style: modern|formal|fantasy|utility) as the cheap seam so per-culture distributions can be authored later without retrofitting; (4) Fantasy-pack pieces are legitimate catalogue members under their own tag, prevalence-weighted low.
Scope pin (user, 2026-07-06, final word on style): for simplicity, THIS ticket ships ONE style only cyberpunk/modern. The pluralistic distribution model above remains the long-term truth (keep the manifest style tag as the seam), but the v1 basic set is single-style; other styles are catalogue growth, not scope.
Route challenge (user, 2026-07-06): not sold on staying Quaternius-primary ''Quaternius is limited and will move the wall at too slow a pace; I''d rather pick something that has what we need now.'' Fresh store verification (2026-07-06): Sidekick packs are one-time USD199.99, perpetual, engine-agnostic license (Godot legal, unsupported); NO raw FBX extraction from unitypackage (community-proven: unitypackage_godot, third-party Sidekick Creator Godot plugin incl. body blend shapes); ''Sold out'' labels are a broken-theme artifact per Shopify JSON (needs one browser cart check); SyntyPass rejected (assets stop being developable after cancel). Relevant packs: Starter (FREE, 57+91 parts), Modern Civilians (165 parts = v1 basic set territory), Sci-Fi Civilians (103 parts, cyberpunk accents), Modern Police (uniforms/officials). DECISION NOW GATED ON THE FREE SPIKE: user downloads free Starter Pack -> Bridge-C spike (extract garment -> Robust Weight Transfer onto our rig -> 11-body batch -> animation QA in T-1088 sandbox). Pass -> buy Modern Civilians, Synty = primary garment supply (+UAL1/UAL2 Source ~USD30 for clips no Synty substitute exists for Godot animations; Fantasy Outfits Source drops to low priority under the single-style pin). Fail -> hand-author route stands. Stay-vs-pivot governance record HELD until spike evidence lands.
Scope guard (user concern, 2026-07-06): Synty intake must NOT displace the create-stuff-yourself pipeline the user''s goal includes in-house garment creation capability, not just swapping vendor catalogs. Resolution: three-route portfolio sharing one engine (a) Synty intake = variety bootstrap only; (b) derivation scripting (cuts/merges/retexture, agent-automatable) = create-yourself lite, turns any donor topology into original designs; (c) TRUE AUTHORING on owned UBC Source body .blends = the pipeline proper. GUARANTEE: the colorable uniform and black suit (no donor exists in any pack) are built via route (c) as the authoring pipeline''s end-to-end proof non-negotiable deliverable of this ticket, so the pipeline exists even if Synty covers everything else. Spike status: extraction done inline by lead (158 FBX, sci-fi civilian theme confirmed); DECISIVE finding Sidekick rig is UE-Mannequin-named, 52/65 bone-name overlap with our rig, sample garment binds to 12 vgroups of which 11 match ours exactly (only head->Head case rename) => transplant likely rename-and-rebind, far cheaper than the weight-transfer the pivot eval assumed; body blend shapes travel in the FBX. Transplant phase parked pending API recovery / token-window reset.
ROUTE DECISION (user, 2026-07-06, final): Synty intake REJECTED despite the spike''s technical PASS (1) USD199.99/pack is too expensive; (2) Sidekick modularity is baked body-segment swap (clothes fused to body chunks), incompatible with our layered clothing-over-body system (11 bodies, skin tones, slot swapping); (3) user dislikes the Synty style. The spike still paid: transplant/batch scripts are source-agnostic and become the G1-family production pipeline; T-1090 (5 bodies broken bare) discovered; the Synty question is closed with evidence, not parked. OPERATIVE ROUTE: (b) derive from owned Fantasy parts + (c) hand-author with a new preferred authoring technique: OFFSET-SHELL FROM OUR OWN BODY MESHES (extract body segment surface, offset outward into garment shell, cut neckline/hem/sleeves; weights inherited by construction, style fully ours, USD0). Suit + uniform remain the full hand-author proof. Purchases still open (separate concerns): UAL1/UAL2 Source ~USD30 for animation clips; Fantasy Outfits Source USD20 low-priority convenience.
Chromakey garment-QA harness DELIVERED (2026-07-06, commits eaca6c8c4/74fa16260/b09a50efb): two-pass depth-proximity clip detection pass A keys covered body segments magenta, pass B re-renders garment-only with a depth-only 3cm epsilon bias toward camera; analyzer splits key pixels into exposed_skin (informational sleeveless arms, collars) vs clip_through (gating skin within 3cm in front of cloth). Run: tooling/garment-qa/run-garment-qa <config>. Verified on the peasant set (avg_m/f x Walk/Sprint/Crouch_Fwd x 3 frames x 4 yaws): 0/18 front-view false positives; genuine findings = sleeveless armhole seam (avg_f, sides), deep-crouch waist gap (worst 150px, back), collar nape. First real fit observations already on record for the peasant set. Sensitivity: clip_epsilon_m + --min-pixels, to calibrate against the first modern garments. This is the acceptance gate for every garment the wardrobe pipeline produces; pair with the T-1088 sandbox for in-world walk QA.
ENGINE DELIVERED (2026-07-06, commits e22ea0fa4 + 278e8b7c2; earlier eaca6c8c4/74fa16260/b09a50efb chromakey, b54b8189d bodies): G1 skinned batch-fit (blender_batch_fit_skinned.py), offset-shell authoring (blender_author_offset_shell.py route c create-yourself, weights by construction, procedural RGBA masks, UV2 chest channel), toon_garment.gdshader (channel-blended 4-region tint + logo after-tint/before-toon), compositor plumb (multi_region coverage flag -> garment shader; descriptor clothing_logos: per-character brand choice), thrds logo stub, manifest style tag + logo_capable. PROOF: tshirt_modern on 6 healthy bodies, 18-assertion suite green, 216-capture chromakey QA. ROUTE GUIDANCE from QA evidence (Q-060): single-reference SD-fit of offset-shells degrades with girth divergence (muscular_m worst 859px clip at 24mm standoff; reference body clean) offset-shell garments should be authored PER BODY (needs per-body masks + runtime <body>_mask.png loading, small change); SD-fit remains right for derived/hand-authored garments sharing one mask (suit, uniform, Fantasy cuts). REMAINING for this ticket: refit tshirt to the 5 repaired forks (T-1090 done add to --only and rerun), per-body shell mode, then the basic set: hoodie, button-down, shorts, jeans, formal pants, jacket + the suit/uniform hand-author proof; creation-screen try-on UI for tints/logos.
Basic-set EXPANSION (user, 2026-07-06 bedtime directive: ''full set of the basics across the board''): wave 2 after the in-flight wave 1 FOOTWEAR: sneakers_modern (trainers, everyday default), shoes_formal (suit pairing), boots_modern (ankle height); SPORT: tank_top (sleeveless, logo-capable), joggers_modern (cuffed, track-stripe tint region), track_jacket (with joggers = the training suit, logo-capable); SWIM: swim_trunks (bright), swimsuit_onepiece (color-block regions). Foot garments follow the peasant_shoes both-feet convention; sole geometry as an offset-shell param extension. Total modern catalogue after wave 2: 17 garments across tops/bottoms/feet/full-body x casual/formal/sport/swim. DEFERRED deliberately: hats/accessories accessory slot path is unexercised and T-721 flags a filter bug; needs daylight, not a 3am debug. Overnight sequence: wave 1 -> wave 2 -> full garment x 11-body fitting matrix (the ''T-1090 fittings'' check, fork bodies especially) -> commits -> gated pushes -> morning report.
Fill-in-the-blanks addendum (user bedtime directive 2 + lead judgment): wave 2 grows to 12 adds sweater_modern (crew pullover), cargo_pants (utility/cyberpunk staple), parka_thrds (cold-weather was absent entirely; ships as the FIRST CANON-BRANDED garment thrds is Braemar''s cold-weather fiber brand per wiki; hip-length to avoid the parked skirt-rigging tier), slides (swim footwear, near-free). PLUS outfit presets: data file of named complete looks (casual/business/sport/beach/worker/sleep as tint-palette presets over garments) authored by the lead at integration makes the catalogue usable for NPC dressing and the shop as OUTFITS, not just items. Post-wave-2 catalogue: 21 modern pieces (tops 8 / bottoms 6 / feet 4 / full-body 2 / swim 3). Hats/accessories remain the deliberate daylight deferral (T-721).
ACCEPTED for first release (user review, 2026-07-07): full catalogue + lookbook approved; toe-box fix reviewed ''I still see some toes, but this will do''. Residual toe hints on closed footwear accepted as first-release quality; further toe-box smoothing is polish backlog, not scope. Closing.', 'done', 'high', NULL, 'client', NULL, '2026-07-06 07:34:18.656', '2026-07-07 12:04:49.833', NULL, '348ec6c1104b8c20ae8ee3b2c63797b7', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKR283RTWYXD2M005K65S52M', 'bug', '06FB0TNSRXX29H5X0EB0F0GBW4', 'head_001 template renders grey (untinted) on child/teen/muscular bodies', 'Found by the T-1089 lookbook pass (2026-07-07): overlay head templates (head_001) render grey faces on child/teen/muscular bodies — the template appears not to receive the body''s skin-tone tint, while each body''s own seg_head renders correctly. Workaround used in the lookbook: head_id empty -> body seg_head shows. If head templates are meant to be body-agnostic (D-161 head-as-BoneAttachment3D), the skin-tone shader application needs to cover template heads per body; investigate _apply skin tone path in character_visual.gd for the head template branch.', 'done', 'medium', NULL, 'client', NULL, '2026-07-07 10:04:11.590', '2026-07-07 12:08:15.679', NULL, '8ed376d80da36bf8f1361c3d4dcf9bbf', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKRZ29KH70NFAY0Z1GCVR4N0', 'task', '06FB0TNSRXX29H5X0EB0F0GBW4', 'S9 live feel-tuning session — locomotion constants against the tuned eye', 'The one unfinished piece of T-1088, deliberately parked at sidequest close (2026-07-07) because it needs Jeroen at the keyboard: (1) verify turn-in-place handedness (headless could not — positive shortest-arc delta maps to _L; one-line TURN_CLIP swap if mirrored); (2) judge TURN_SPEED_SCALE=2.0 (UAL turn clips ~1.7-2.0s vs the ~0.3s yaw ease) and the turn-on-stop behavior; (3) general feel constants pass (NATIVE_MPS cadence sync, blends, camera follow/lookahead, cutaway corridor width) — every knob in client/scripts/sandbox/sandbox_constants.gd with the DebugHud showing live numbers. Launch: server --test-mode + SR_LIVE=1 locomotion_sandbox.tscn. Also revisit during the session: Q-084''s walk-vs-aim animation split (the purchased 8-dir walk/jog/crouch sets make direction-matched clips tractable).', 'backlog', 'medium', NULL, 'client', NULL, '2026-07-07 12:10:06.108', '2026-07-07 12:10:06.108', NULL, '85414a301d4c7313046c3be096542165', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKCER95EK266H8RWPW8XT1F0', 'story', '06FB0TNSRXX29H5X0EB0F0GBW4', '3D character locomotion sidequest — composited character walks the Gauntlet on the live server', 'Fable-5-window sidequest (sanctioned cascade exception, 2026-07-05): the presentation layer that turns discrete server tile steps into a believable walking 3D character. Scope: 3D sandbox dev scene (SR_LIVE=1, greybox tiles derived from server snapshots, no hand-authored map), CharacterVisual instantiated in-world, locomotion rig (tile-step + facing-octant interpolation, stance-aware speeds), UAL animation state machine (idle/walk/sprint/careful/crouch), D-148 camera rig (30deg low-angle follow, 45deg rotation). Server-authoritative from day one — no client-local movement model. Builds on D-148/D-149/D-244/D-066 and the T-417 stance system. If the Fable 5 window closes mid-flight, work parks here.
Visual reference (user, 2026-07-06): Xenonauts 2 nails the target tiles/occlusion/character perspective readable tile grid, low-angle tactical camera with angle options (matches D-148 30deg low-angle + 45deg rotation steps and the spikes/3dpipeline angle presets), and crucially its wall/roof occlusion treatment: camera-facing walls cut down/ghosted so interiors stay readable, floors peel per z-level. One deliberate difference: our camera locks to the player character (D-015/T-116) no free map panning. Implication for the locomotion sandbox: greybox walls should prototype camera-facing cutaway occlusion early, since wall readability interacts with the locked low-angle camera.
Scoping call (user + lead agreement, 2026-07-06): wall cutaway is CLIENT-SIDE presentation, decoupled from fog-of-perception. Rationale: two occlusion systems with different subjects perception/fog governs what the CHARACTER knows (server-enforced info boundary; client only receives observer-filtered geometry), cutaway governs what the PLAYER''S CAMERA sees (pure render mode on wall data the client already holds). They compose: cutaway cuts render height; fog overlay still gates what is visible behind. Walls remain first-class client data objects (future destruction/explosion interactions), cutaway is a render mode on them like tint/LOD. Nuance parked for Phase 5 proper: destroyed-while-unobserved walls should render in remembered state (fog-memory applied to geometry). If this holds through implementation, mint a D-record (perception domain) when the sidequest lands.
Animation supply resolved (2026-07-06): user purchased UAL1[Source+Pro] (120 clips, identical lists) + UAL2[Source] (134 clips), unzipped in docs/assets/downloads/. Gains: Turn90_L/R + Turn180_L/R (turn-in-place), Sprint_Enter/Exit + Crouch_Enter/Exit (stance transitions), full 8-direction Walk/Jog/Crouch sets incl. diagonals, Counter_Idle + talking/phone/sitting idles (life-sim verbs), plus _RM root-motion variants (unused in-place is correct for server-authoritative interpolation). Still absent everywhere: walk start/stop + stairs the only remaining custom clips. Integration is NOT a drop-in swap: upstream renamed clips (Walk_Loop -> Walk_Fwd_Loop etc.), so wire via the design''s recorded plan explicit library names + lib/Clip addressing + gait-table update. NOTE for facing design: the 8-dir walk sets unlock Q-084''s parked walk-vs-aim split (direction-matched clips relative to facing) revisit during the S9 tuning session.
Live-session feedback (user, 2026-07-06): (1) Corridor cutaway verdict two minds: ''extremely trippy, could be the game''s own look'' vs ''player cannot see what is in the room unless close''. Resolution: cutaway MODES, toggled live in the sandbox (corridor / bottom-walls-low / both) bottom-wall rule is data-driven (wall drops to stub when a KNOWN floor tile sits on its camera-far side; camera-facing directions derived from WorldRoot basis at runtime, not hand-derived). Default picked by feel during the session; the trippy corridor stays available as a signature-look option. (2) NEW: mouse-over move-here marker on the hovered tile + optimal path line client-side A* over the never-evict KNOWN tile store (info-boundary-honest: the character plans only through known tiles; fog is unpathable), 8-dir with diagonal step cost 1 (time-optimal per the no-sqrt2 wire), terrain-cost provider seam (uniform today, Phase-4 terrain later), click executes by streaming ordinary Move* step commands through the existing throttle (zero protocol change, server validates every step), cancelled by WASD override / path invalidation / arrival. Settles more of Q-084 (click-to-move half).
RMB gesture vocabulary (user, live session 2026-07-06, added to the path-preview feature): single right-click = walk there (current stance); DOUBLE right-click = sprint there (ladder ToggleStanceUp burst to Sprint via the ordinary command queue, pre-sprint stance RESTORED on arrival; a double upgrades the already-started follow in place); LONG-PRESS right-click (~0.4s, commit on release) = ''take cover at location'', prototyped as go-there-then-Crouch-on-arrival (no restore crouching was the point). Real cover mechanics (wall adjacency, directional protection) are future combat design; the gesture is the input-vocabulary prototype. WASD always cancels any follow without stance restore (player override is deliberate).
Follow-facing refinement (user, live session 2026-07-06): during an RMB path-follow the BODY commits to the leg/movement direction (mouse no longer swings body yaw); the mouse instead drives a LAYERED LOOK-AT Head bone (~70deg limit) plus torso twist via a spine bone (~30deg) for looking far lateral/behind; beyond their sum the character physically cannot look further without turning (correct). Pure client presentation on the D-249 model; SetFacing continues to ride the wire during follows, so the SERVER vision cone still follows the mouse (diegetic: the character looks where the player points while walking a committed path). Implementation: Godot LookAtModifier3D pair under the CharacterVisual skeleton, influence-faded in/out on follow start/end; rig gains a commit_body_to_motion flag (yaw from leg velocity while set).
SIDEQUEST CLOSE-OUT (2026-07-07): all scope delivered and pushed sandbox (scene, greybox store, rig, gait machine, camera, cutaway modes w/ user-verdict mode-2 default recorded as D-250 amendment), RMB navigation vocabulary, layered head/torso look-at, D-252 view/movement protocol split (server+client), purchased UAL tiers wired (turn-in-place, stance enter/exits), design doc committed to docs/design/t1088-locomotion-sandbox-design.md (the ''design §'' references in sandbox code resolve in-repo now). Design-delta close-out notes: no new Gauntlet room was needed (constraint 12 never triggered, server untouched except D-252); ~40 lines of main.gd boot boilerplate duplicated with Pattern: markers for the T-962 extraction; seek-during-blend + remove_immutable_tracks caveats did not manifest in practice; ual_extended wiring superseded by the purchased-tier ual1/ual2 libraries; 2D select_yaw_target moving-branch is dead code post-D-252 (cleanup rides Phase 5). Remaining feel work parked as T-1096 (S9 live tuning needs the user). Closing.', 'backlog', 'high', NULL, 'client', NULL, '2026-07-06 07:01:06.475', '2026-07-07 12:10:20.956', NULL, '990a574e76aab1e943fb4997885c7542', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FKCER95EK266H8RWPW8XT1F0', 'story', '06FB0TNSRXX29H5X0EB0F0GBW4', '3D character locomotion sidequest — composited character walks the Gauntlet on the live server', 'Fable-5-window sidequest (sanctioned cascade exception, 2026-07-05): the presentation layer that turns discrete server tile steps into a believable walking 3D character. Scope: 3D sandbox dev scene (SR_LIVE=1, greybox tiles derived from server snapshots, no hand-authored map), CharacterVisual instantiated in-world, locomotion rig (tile-step + facing-octant interpolation, stance-aware speeds), UAL animation state machine (idle/walk/sprint/careful/crouch), D-148 camera rig (30deg low-angle follow, 45deg rotation). Server-authoritative from day one — no client-local movement model. Builds on D-148/D-149/D-244/D-066 and the T-417 stance system. If the Fable 5 window closes mid-flight, work parks here.
Visual reference (user, 2026-07-06): Xenonauts 2 nails the target tiles/occlusion/character perspective readable tile grid, low-angle tactical camera with angle options (matches D-148 30deg low-angle + 45deg rotation steps and the spikes/3dpipeline angle presets), and crucially its wall/roof occlusion treatment: camera-facing walls cut down/ghosted so interiors stay readable, floors peel per z-level. One deliberate difference: our camera locks to the player character (D-015/T-116) no free map panning. Implication for the locomotion sandbox: greybox walls should prototype camera-facing cutaway occlusion early, since wall readability interacts with the locked low-angle camera.
Scoping call (user + lead agreement, 2026-07-06): wall cutaway is CLIENT-SIDE presentation, decoupled from fog-of-perception. Rationale: two occlusion systems with different subjects perception/fog governs what the CHARACTER knows (server-enforced info boundary; client only receives observer-filtered geometry), cutaway governs what the PLAYER''S CAMERA sees (pure render mode on wall data the client already holds). They compose: cutaway cuts render height; fog overlay still gates what is visible behind. Walls remain first-class client data objects (future destruction/explosion interactions), cutaway is a render mode on them like tint/LOD. Nuance parked for Phase 5 proper: destroyed-while-unobserved walls should render in remembered state (fog-memory applied to geometry). If this holds through implementation, mint a D-record (perception domain) when the sidequest lands.
Animation supply resolved (2026-07-06): user purchased UAL1[Source+Pro] (120 clips, identical lists) + UAL2[Source] (134 clips), unzipped in docs/assets/downloads/. Gains: Turn90_L/R + Turn180_L/R (turn-in-place), Sprint_Enter/Exit + Crouch_Enter/Exit (stance transitions), full 8-direction Walk/Jog/Crouch sets incl. diagonals, Counter_Idle + talking/phone/sitting idles (life-sim verbs), plus _RM root-motion variants (unused in-place is correct for server-authoritative interpolation). Still absent everywhere: walk start/stop + stairs the only remaining custom clips. Integration is NOT a drop-in swap: upstream renamed clips (Walk_Loop -> Walk_Fwd_Loop etc.), so wire via the design''s recorded plan explicit library names + lib/Clip addressing + gait-table update. NOTE for facing design: the 8-dir walk sets unlock Q-084''s parked walk-vs-aim split (direction-matched clips relative to facing) revisit during the S9 tuning session.
Live-session feedback (user, 2026-07-06): (1) Corridor cutaway verdict two minds: ''extremely trippy, could be the game''s own look'' vs ''player cannot see what is in the room unless close''. Resolution: cutaway MODES, toggled live in the sandbox (corridor / bottom-walls-low / both) bottom-wall rule is data-driven (wall drops to stub when a KNOWN floor tile sits on its camera-far side; camera-facing directions derived from WorldRoot basis at runtime, not hand-derived). Default picked by feel during the session; the trippy corridor stays available as a signature-look option. (2) NEW: mouse-over move-here marker on the hovered tile + optimal path line client-side A* over the never-evict KNOWN tile store (info-boundary-honest: the character plans only through known tiles; fog is unpathable), 8-dir with diagonal step cost 1 (time-optimal per the no-sqrt2 wire), terrain-cost provider seam (uniform today, Phase-4 terrain later), click executes by streaming ordinary Move* step commands through the existing throttle (zero protocol change, server validates every step), cancelled by WASD override / path invalidation / arrival. Settles more of Q-084 (click-to-move half).
RMB gesture vocabulary (user, live session 2026-07-06, added to the path-preview feature): single right-click = walk there (current stance); DOUBLE right-click = sprint there (ladder ToggleStanceUp burst to Sprint via the ordinary command queue, pre-sprint stance RESTORED on arrival; a double upgrades the already-started follow in place); LONG-PRESS right-click (~0.4s, commit on release) = ''take cover at location'', prototyped as go-there-then-Crouch-on-arrival (no restore crouching was the point). Real cover mechanics (wall adjacency, directional protection) are future combat design; the gesture is the input-vocabulary prototype. WASD always cancels any follow without stance restore (player override is deliberate).
Follow-facing refinement (user, live session 2026-07-06): during an RMB path-follow the BODY commits to the leg/movement direction (mouse no longer swings body yaw); the mouse instead drives a LAYERED LOOK-AT Head bone (~70deg limit) plus torso twist via a spine bone (~30deg) for looking far lateral/behind; beyond their sum the character physically cannot look further without turning (correct). Pure client presentation on the D-249 model; SetFacing continues to ride the wire during follows, so the SERVER vision cone still follows the mouse (diegetic: the character looks where the player points while walking a committed path). Implementation: Godot LookAtModifier3D pair under the CharacterVisual skeleton, influence-faded in/out on follow start/end; rig gains a commit_body_to_motion flag (yaw from leg velocity while set).
SIDEQUEST CLOSE-OUT (2026-07-07): all scope delivered and pushed sandbox (scene, greybox store, rig, gait machine, camera, cutaway modes w/ user-verdict mode-2 default recorded as D-250 amendment), RMB navigation vocabulary, layered head/torso look-at, D-252 view/movement protocol split (server+client), purchased UAL tiers wired (turn-in-place, stance enter/exits), design doc committed to docs/design/t1088-locomotion-sandbox-design.md (the ''design §'' references in sandbox code resolve in-repo now). Design-delta close-out notes: no new Gauntlet room was needed (constraint 12 never triggered, server untouched except D-252); ~40 lines of main.gd boot boilerplate duplicated with Pattern: markers for the T-962 extraction; seek-during-blend + remove_immutable_tracks caveats did not manifest in practice; ual_extended wiring superseded by the purchased-tier ual1/ual2 libraries; 2D select_yaw_target moving-branch is dead code post-D-252 (cleanup rides Phase 5). Remaining feel work parked as T-1096 (S9 live tuning needs the user). Closing.', 'done', 'high', NULL, 'client', NULL, '2026-07-06 07:01:06.475', '2026-07-07 12:10:20.986', NULL, 'ce4896d59ece08a42a88e7e026b7d15f', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
+1
View File
@@ -7,6 +7,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
### Added
- **3D locomotion sandbox + in-house wardrobe engine — Fable-5 character sidequest** (D-248D-252, T-1088, T-1089) — a sanctioned cascade exception delivered a walking 3D character ahead of Phase 5: a locomotion sandbox (SR_LIVE, greybox tiles derived from live server snapshots, no hand-built map) with per-leg constant-velocity interpolation cadence-synced to gait clips, mouse-driven move-here pathing with a walk/sprint/take-cover gesture vocabulary, and a client-side wall-cutaway camera kept decoupled from fog-of-perception. The server protocol's `Facing` is now view-only — movement direction is derived client-side from position deltas, and NPC gaze relocates to path-follow intent (D-252). Alongside it, a full in-house wardrobe engine: offset-shell garment authoring from our own body meshes (per-body mode, weights inherited by construction), a 24-garment catalogue — everyday casual through a hand-authored suit and colorable uniform — fitted across all 11 body types, RGBA multi-region tinting with per-character brand logos, a creation-screen try-on UI, 12 named outfit presets, and a chromakey QA harness gating every garment for clip-through before release. The character asset route (Quaternius rig, in-house wardrobe) was re-confirmed after a hands-on vendor evaluation (D-251 — Synty proved technically viable but was rejected on cost, modularity, and style), and the purchased UAL1/UAL2 animation tiers are wired in. Five of the eleven body types, found broken bare mid-sidequest (T-1090), were repaired — all eleven now render and fit correctly
- **Layer 2 — inter-settlement road & rail graph** (D-211, T-1038) — the generation cascade now draws the transport network *between* a body's settlements: a minimum-spanning backbone connects every city, extra direct links are added where that backbone would force a long detour, and each route is pathed across the real terrain — hugging river valleys (preferred), skirting steep ground, never crossing open water. Every road carries a *maintenance authority* you'll be able to read in its condition (Administrative government highway, Corporate access road, Communal local lane, long-haul Trade route, or Abandoned), and long routes gain waypoint stops at their midpoints. Deterministic and held in the per-body world cache (re-derived from seed, never saved); the Atlas overlay and downstream layers (road-entry directions, territory road-coverage) consume it next. Corp HQs are kept out of the city pool so they don't spawn duplicate roadside towns (D-242)
- **Layer 3 — settlement placement** (D-211, #955) — the generation cascade now places a body's settlements on terrain that fits their economy: capitals and cities are matched to river mouths, coasts, valleys, passes, etc. via the D-195 affinity table (a financial hub gravitates to the coast, a farm to a fertile valley, a fort to a mountain pass). Runs in-game: when a body is first viewed, its settlements are read from the atlas and placed by the live cascade, then held in the per-body world cache
- **Layer 3 — settlement political character** (D-212/213/214/215, #956) — each placed settlement now carries the spatial character that will shape its streets: a *territorial status* (who controls the province — the Concord Assembly, a Syndic corp, the autonomist Compact of Westphalia, contested, or open frontier), a *political archetype* (Commission/Corporate/Pioneer/Military/Academic/Industrial), the *arrangement pattern* its districts will follow (radial core, campus grid, ribbon, fortified perimeter, hub-and-spoke), and a *founding orientation* (street grid facing the coast, the river, the contours, the cardinals, or — for pioneers — a seed-varied bearing). All derived deterministically from the system's governing faction + the settlement's economy; the layouts that consume it come with the quarter generator
+14 -2
View File
@@ -466,7 +466,13 @@ func _load_head(desc: CharacterVisualDescriptor) -> void:
if desc.head_id.is_empty():
return
var path := BASE_PATH + "heads/templates/%s.glb" % desc.head_id
if _attach_to_bone(path, "Head") != null:
# T-1095: tint the template head with the body's skin tone (same treatment
# as seg_head gets via _apply_body_shader) — template heads ship no
# _mask.png sidecar, so pass the full-white mask as the fallback; without
# it the toon_masked recolor_mask defaults black and the tint is ignored
# (the untinted template read grey against mid/dark skin tones).
var tone := SKIN_TONES[clampi(desc.skin_tone, 0, SKIN_TONES.size() - 1)]
if _attach_to_bone(path, "Head", tone["lit"], 0, _white_mask) != null:
_loaded_slots.append("head")
@@ -494,7 +500,11 @@ func _load_eyebrows(_desc: CharacterVisualDescriptor) -> void:
func _attach_to_bone(
path: String, bone_name: String, tint: Color = Color.WHITE, _render_priority: int = 0
path: String,
bone_name: String,
tint: Color = Color.WHITE,
_render_priority: int = 0,
fallback_mask: Texture2D = null,
) -> BoneAttachment3D:
if _skeleton == null:
return null
@@ -520,6 +530,8 @@ func _attach_to_bone(
var mask_tex: Texture2D = null
if ResourceLoader.exists(mask_path):
mask_tex = load(mask_path) as Texture2D
if mask_tex == null:
mask_tex = fallback_mask # T-1095: e.g. full-white for skin-toned heads
var inst := scene.instantiate()
var meshes := _collect_meshes(inst)
+111 -12
View File
@@ -444,9 +444,14 @@ Convention: `<asset_name>_mask.png` as a sidecar file next to the GLB.
- `hat_hardhat.glb` + `hat_hardhat_mask.png`
- For clothing, the mask lives in the item directory: `coveralls_basic/reference_mask.png`
Masks are per-asset, not per-variant. The same mask applies to all 11 body-type variants of a
clothing item because the mask regions (collar is region A, sleeves are region B) correspond
to UV regions, and the UV layout is preserved through Surface Deform fitting.
Masks are per-asset, not per-variant, for hair/facial hair/eyebrows/accessories/heads -- one
mask applies across all 11 body-type variants because the mask is UV-space and the UV layout
is preserved through fitting.
**Superseded for clothing (T-1089, see Section 11):** offset-shell garments now author a mask
**per body type** (`<body_type>_mask.png`, that body's own UV0 layout), falling back to
`reference_mask.png` only for the Surface-Deform-fit route (suit, uniform, Fantasy-derived
cuts), which still shares one mask across variants as described above.
### Head Templates
@@ -567,13 +572,17 @@ For every GLB that supports recoloring, a `_mask.png` sidecar sits next to it:
- **Format:** PNG, same UV layout as the GLB's texture
- **Channels:** Greyscale (R channel only). White = tintable region, black = preserve original
- **For multi-region recoloring:** Use distinct greyscale values per region (e.g. 0.0, 0.33,
0.66, 1.0 for up to 4 independent tint regions). The shader samples the mask and routes
each region to a different `tint_color` uniform. This is not yet implemented in the shader
but the mask format supports it.
- **Default:** `hint_default_black` in the shader means "no mask loaded = no tinting."
Assets without masks render with their original Trellis/authored texture.
**Multi-region resolved differently than proposed here (T-1089, see Section 11).** The
distinct-greyscale-band idea above (0.0/0.33/0.66/1.0 in one channel) was never built --
indexed greyscale bilinear-filters badly at region boundaries (a filtered sample between two
index levels reads as a wrong third region, not a blend). Clothing multi-region instead
channel-routes an **RGBA mask** (R/G/B/A -> 4 independent tints, `toon_garment.gdshader`),
so bilinear filtering cross-fades neighboring regions correctly. This is clothing-only; the
single-channel binary convention above is unchanged for hair/facial hair/eyebrows/accessories.
### Surface Deform Batch Output
The Blender batch pipeline takes one reference clothing mesh and produces 11 fitted variants.
@@ -639,6 +648,7 @@ CharacterVisualDescriptor:
skin_tone: int # index into the 9-tone palette
clothing_slots: Dictionary # slot -> item_id (e.g. "torso" -> "coveralls_basic")
clothing_tints: Dictionary # item_id -> Array[Color] for multi-region recolor
clothing_logos: Dictionary # item_id -> brand id (logo_capable items only; T-1089, Section 11)
accessory_slots: Dictionary # slot -> item_id (e.g. "hat" -> "hat_hardhat")
accessory_tints: Dictionary # item_id -> Color
```
@@ -726,11 +736,11 @@ What does NOT carry forward:
## 10. Open Questions for Review
**Q: Multi-region recolor masks -- implement now or defer?**
The current `toon_masked` shader supports single-region (binary mask). Multi-region requires
a shader update to sample distinct greyscale bands and route them to separate tint uniforms.
Recommendation: defer to when the first multi-region clothing item is authored. The mask PNG
format supports it already (greyscale values), so no re-export is needed later.
**Q: Multi-region recolor masks -- implement now or defer?** **RESOLVED (T-1089, Section 11).**
Deferred until the first multi-region clothing item (the t-shirt) needed it, then implemented
as an RGBA channel-routed mask + `toon_garment.gdshader` -- not the greyscale-band shader
update this note anticipated (see the corrections in Sections 3 and 5 above). `toon_masked`
is retained for single-region assets (hair, accessories).
**Q: Teen clothing variants -- separate catalogue or shared with adult?**
Teen body types use the same clothing system (Surface Deform fitting) but teen proportions
@@ -741,6 +751,94 @@ revisit if visual quality is insufficient.
---
## 11. Wardrobe Pipeline (T-1089)
The clothing engine that shipped the first real catalogue (2026-07-06/07). This section
supersedes the single-region greyscale mask model assumed in Sections 3/5/10 above for
**clothing specifically** -- hair/facial hair/eyebrows/accessories/heads keep the
single-channel convention as originally written.
### Authoring: offset-shell from our own body segments
The preferred authoring route is **offset-shell**
(`tooling/garment-fit/blender_author_offset_shell.py`, one per-garment companion script per
item, e.g. `blender_author_hoodie.py`, `blender_author_boots.py`): extract the body segment(s)
a garment covers, cut neckline/hem/sleeve boundaries via bone-plane thresholds, offset the
surface outward along vertex normals, solidify for real cloth thickness. Because the shell
*is* body topology, bone weights are inherited by construction -- no Surface Deform, no re-rig.
**PER-BODY mode (`--per-body`) is now the standard for offset-shell garments**, not
single-reference-then-fit. Q-060 evidence: single-reference Surface-Deform fitting degraded
with girth divergence (muscular_m hit an 859px worst clip at 24mm standoff on the
reference-authored t-shirt); authoring per body guarantees clearance + exact weights by
construction and lets the offset drop back to the ~12mm ideal. Cut/mask thresholds are
derived from each body's own bone landmarks using the proportional ratios that reproduce the
hand-calibrated reference constants -- recomputed per body, not copied.
Shared geometry practices proven across garments: **boundary weld** (`weld_boundaries` --
merges coincident segment-boundary verts so the offset can't open cracks) and **open-rim
flattening** (`flatten_open_rims` -- pulls the jagged segment-splitter boundary rings onto
clean planes before the offset runs; first required by jeans and the track jacket).
**`convex_toe_box`** (shared helper, used by boots/sneakers/shoes_formal) reshapes the
forefoot into one smooth per-slice ellipse cap so individual-toe geometry doesn't poke the
shoe shell -- residual toe hints are accepted for first release (see Catalogue below).
### Fitting: Surface-Deform batch route
`tooling/garment-fit/blender_batch_fit_skinned.py` remains the route for garments sharing one
reference mesh + mask across all 11 bodies -- Fantasy-pack-derived cuts and hand-authored
pieces on a single UV layout (the suit, the uniform). SD-bind to each target body, bake the
deformed rest shape, transfer + normalize weights, retarget the armature, export with
**`export_skins=True`** (the predecessor script's `export_skins=False` output could not
animate on the shared skeleton -- why it was superseded, not merely extended).
### Runtime contract
- **`coverage.json`** per clothing item: `hides` (segment names to hide), `torso_variant`
(`full`/`upper`), and the new **`multi_region`** flag (routes the item through
`toon_garment.gdshader` instead of the single-tint `toon_masked.gdshader`).
- **Per-body masks:** `<body_type>_mask.png` sits beside each body's fitted GLB; the
compositor prefers it and falls back to `reference_mask.png` for the Surface-Deform-fit
route. See the mask-convention correction in Section 3.
- **`toon_garment.gdshader`** (`client/assets/characters/shaders/`): channel-routes FOUR tints
through an RGBA region mask (R/G/B/A -> `tint_0`..`tint_3`), channel-blended rather than
indexed-greyscale (see the correction in Section 5). A logo decal samples a dedicated UV2
(TEXCOORD_1) chest channel, composited **after** region tinting (brand colors stay
faithful) but **before** the toon shadow (the logo darkens with its fabric).
- **`CharacterVisualDescriptor.clothing_logos`:** `item_id -> brand id`, per-character logo
choice (Section 6), alongside `clothing_tints` (`item_id -> Array[Color]`, one per region).
- **Manifest clothing schema** (`client/assets/characters/manifest.json`, `clothing` key):
`slot`, `style` (`modern`/`formal`/`utility`/... -- the D-251 style-tag seam), `logo_capable`,
and for multi-region items `regions` (named R/G/B/A) + `default_tints`.
- **`outfits.json`:** named complete looks (`presets`) composing manifest items with per-item
tints/logos -- 12 presets (casual/business/sport/beach/worker/sleep) for NPC dressing and
shop display.
### QA: chromakey acceptance gate
`tooling/garment-qa` (`run-garment-qa <config>`) is the acceptance gate every garment in this
pipeline runs before it ships: a two-pass depth-proximity chromakey renders the same frozen
frame twice -- once keying claimed-covered body segments magenta, once with the garment
nudged a few cm toward the camera and re-keyed cyan. A body-key pixel that flips from magenta
to cyan is a genuine clip (`clip_through_pixels`, gates on connected-component size); a pixel
that never flips despite being exposed is informational (`exposed_skin_pixels` -- collars,
sleeveless armholes). Wave 1/2 garments were accepted at <=150px worst clip component.
`client/tools/garment_preview/lookbook_scene` renders the full catalogue for visual review --
the pass that caught the missing-albedo and manifest-nesting findings fixed in `e562418d0`.
### Current catalogue
24 entries: 21 modern (tops, bottoms, feet, full-body, swim -- wave 1 + wave 2 + the
fill-in-the-blanks addendum) + 3 legacy peasant pieces (pre-dating this pipeline, still on
the Surface-Deform-fit route). All 24 fitted across all 11 body types.
**Known artifact classes, accepted for first release:** sleeve/opening-hem epsilon blobs
(offset-shell boundary artifacts, offset-insensitive); residual toe hints on closed footwear
after `convex_toe_box` ("I still see some toes, but this will do" -- user review, 2026-07-07).
Further smoothing is polish backlog, not open scope.
---
## Decisions Referenced
- **D-159:** 5 adult body types x 2 genders + 1 child = 11 base meshes (updated: added Teen)
@@ -749,3 +847,4 @@ revisit if visual quality is insufficient.
- **D-162:** Clothing is pre-baked per body type via Surface Deform
- **D-163:** Trellis generates heads via BoneAttachment3D (offline asset production, curated into template library)
- **D-164:** Fork Quaternius skeleton, replace all body meshes (Source tier .blends as starting point)
- **D-251:** Character asset route reconfirmed 2026-07-06 -- Quaternius rig stays, wardrobe is in-house (offset-shell + Surface-Deform routes, Section 11), UAL1/UAL2 animation tiers purchased
@@ -0,0 +1,389 @@
# T-1088 — 3D Locomotion Sandbox: Final Design (synthesized)
> Provenance: synthesized 2026-07-06 from three independent design candidates +
> a three-lens judge panel (T-1088 sidequest). This is the document the sandbox
> code's "design §" comments reference (client/scripts/sandbox/*). Governance:
> D-248 (interpolation), D-249 (facing authority, amended by D-252), D-250
> (cutaway decoupling), D-252 (view/movement protocol split). Committed at
> sidequest close-out so the section references resolve in-repo.
**Ticket:** T-1088 (story, under Phase-5 epic T-749; sanctioned Fable-5-window cascade exception)
**Base:** Candidate 1 (feel-first) — unanimous #1 across all three judge lenses, zero fatal flaws — with grafts from Candidates 0 and 2 where judges flagged superior ideas, and fixes for every deduction judges recorded against Candidate 1 itself.
**Ground truth:** `design-input.md` (code-verified 2026-07-06) + `spike-findings.md`. The two load-bearing repo facts that sank the other candidates were independently re-verified during this synthesis pass: `character_visual.gd:184-193` (mirrored E/W facing table) and `input_mapper.gd:182-195` (2D-canvas-transform mouse anchor).
## 0. Feel thesis + scope fence
Walking feels right when three channels each have **exactly one smoothing layer**, ordered crisp→soft:
1. **Position** — constant-velocity motion between server-confirmed subtile centers, speed derived per leg from `dist/interval`. No easing on the character itself: easing on a repeating 0.4 s step reads as scooting.
2. **Yaw** — shortest-arc rotation under a per-stance deg/s budget with ease-out. The body is the only thing that eases.
3. **Camera** — exponential low-pass over the character's position. Against a constant-velocity target an exponential follower converges to a *constant* lag offset moving at the *same* velocity — zero jitter while walking, gentle ease on start/stop. This is why the character must NOT also be exponentially eased (the 2D renderer's stacked 12.0-entity + 8.0-camera lerps produce the floaty feel we are replacing).
Animation is slaved to channel 1: clip playback rate is scaled so foot cadence matches actual ground speed (kills foot-slide), and gait selection keys off **render velocity, never input** (so Q-020 silent rejections and bump-to-turn produce a character who turns and stands, not one who walks into a wall).
### Scope fence (every deferral has a named seam — grafted from Candidate 0)
| Deferred | Seam left for it |
|---|---|
| NPC / other-entity rendering | `LocomotionRig.set_wire_target()` single-input contract (§4.0) + injectable `idle_facing_provider`; attach one rig per NPC later |
| Footstep audio (Q-063) | `locomotion_anim.gd` emits `gait_changed(state)`; clip phase exposed via `get_animation_player()` |
| Fog-of-perception 3D overlay (Q-081) | the four-state tint (§3.2) *is* the memory layer; a fog shader composes on top later |
| Day/night lighting (Q-086) | scene-local lights, no baked assumptions |
| `ual_extended.glb` wiring (risk #11) | not needed (no locomotion clip lives there); named-library + `"lib/Clip"` wiring plan recorded in the D-record |
| Client movement prediction | none, by decision (D-054/D-010) — this is what makes Q-020 free (§4.3) |
| Stepped player camera yaw | **OUT** — not canon (ticket-premise correction, design-input §3); requires a new D/Q record before anyone builds it |
| New Gauntlet room | **not needed** — Hub + four 2-wide exit corridors + Sprint Gauntlet 28-tile straight cover idle/walk/sprint/corner/cutaway; constraint 12 untriggered, StableId-77 append held in reserve, server untouched |
| Implant/HudGroups occlusion | one-line `HudGroups.gameplay_occluded` connect to a pause flag (D-170 row 13: the mechanism is CanvasItem-only, the signal is not) |
| Persisting the greybox store | in-memory; store is `RefCounted` with a `to_dict()` seam |
| Foot-lock IK, ghost silhouettes, dithered cut edges | later polish on the same rig/shader |
## 1. Scene + file layout
### 1.1 New files (with promotion status — grafted from Candidate 2, kept under `sandbox/` to avoid pre-committing Phase-5 shapes)
| File | Promotion status |
|---|---|
| `client/scenes/locomotion_sandbox.tscn` | sandbox-only |
| `client/scripts/sandbox/locomotion_sandbox.gd` — boot, connect, poll, pump, descriptor, first-snapshot latch, autopilot | sandbox-only |
| `client/scripts/sandbox/sandbox_constants.gd` — every tuning constant (§12) | proto-production (values migrate) |
| `client/scripts/sandbox/sandbox_space.gd``class_name SandboxSpace` static: THE conversion authority (§2) | **proto-production** |
| `client/scripts/sandbox/greybox_world.gd` — accumulating tile store + MultiMesh painter | store contract proto-production; meshes disposable |
| `client/scripts/sandbox/locomotion_rig.gd` — pure-core mover + facing (static funcs) + thin Node3D wrapper | **proto-production** |
| `client/scripts/sandbox/locomotion_anim.gd` — gait state machine | **proto-production** |
| `client/scripts/sandbox/follow_camera_3d.gd` — D-148 ortho rig | **proto-production** |
| `client/scripts/sandbox/sandbox_debug_hud.gd` — numeric instrumentation | sandbox-only |
| `client/shaders/sandbox/greybox_tile.gdshader` — floor tint + 0.5 m minor / 1.0 m major grid | sandbox-only |
| `client/shaders/sandbox/greybox_wall_cutaway.gdshader` — wall tint + camera-facing height cut | technique proto-production |
| `client/tests/unit/test_greybox_store.gd`, `test_locomotion_math.gd`, `test_gait_table.gd`, `test_sandbox_space.gd` | keep |
Directory moves are cheap at the T-962 gate; the promotion column documents the seam without a `rendering3d/` structure the slice doesn't need.
**Modified files (all additive):** `input_mapper.gd` (§9 provider seam), `character_visual.gd` (§6.3 blend param + player accessor), `client/tests/visual_capture.gd` + `tests/visual.json` (§10.2 per-scenario scene field). **Untouched:** `main.gd`, `main.tscn`, `entity_renderer.gd`, `world_renderer.gd`, `tile_renderer.gd`, fog code, `SnapshotEventRouter`, everything under `server/` — no new Gauntlet room.
### 1.2 Scene tree
```
LocomotionSandbox (Node3D) — locomotion_sandbox.gd
├─ WorldRoot (Node3D) # rotation.y = 45° — THE D-148 map rotation, set once in .tscn
│ ├─ Greybox (Node3D) — greybox_world.gd
│ │ ├─ FloorMM (MultiMeshInstance3D) # PlaneMesh 0.5×0.5 (faces +Y natively), greybox_tile.gdshader
│ │ └─ WallMM (MultiMeshInstance3D) # BoxMesh 0.5×2.5×0.5 offset so base sits at y=0, cutaway shader
│ └─ PlayerRig (Node3D) — locomotion_rig.gd # local position = interpolated sim position (metres)
│ └─ ModelRoot (Node3D) # local rotation.y = smoothed facing yaw (D-151)
│ └─ CharacterVisual # runtime-instantiated; its own rotation.y stays 0
├─ CameraRig (Node3D) — follow_camera_3d.gd
│ └─ Camera3D # PROJECTION_ORTHOGONAL, yaw 0 always
├─ KeyLight (DirectionalLight3D) # energy 1.2, shadows on (character_creation.tscn:65-92 recipe)
├─ FillLight (DirectionalLight3D) # energy 0.4, no shadows
├─ WorldEnvironment # ambient env, same source
└─ DebugHud (CanvasLayer) — sandbox_debug_hud.gd
```
Sim coords stay axis-aligned inside `WorldRoot`; the diamond look is that single static `Transform3D` (D-148 letter: rotation on the tile-grid root, never the camera). Facing yaw is applied on `ModelRoot` in WorldRoot-**local** space, so octant→yaw composes with the map rotation **exactly once** (the compositor-api-spec §2 concern). The camera lives outside WorldRoot at yaw 0 and follows `PlayerRig.global_position` (already rotated).
### 1.3 Boot sequence
`_ready()`: (1) `SimBridge.server_port` from `SR_PORT` if set (pattern `visual_capture.gd:79-88`); `connect_to_sim()` guarded on DISCONNECTED. Handshake + StartupMessage are automatic. (2) Build a local `CharacterVisualDescriptor` from the first valid `manifest.json` ids per category (`GameState.character_visual_descriptor` is never populated — §2.1 of design-input); no hardcoded id strings. (3) `CharacterVisual.new()``ModelRoot.add_child()``load_descriptor()`**in-tree before load_descriptor** (shader-load-in-`_ready` constraint). Hidden until first snapshot. (4) Install the InputMapper facing provider (§9). (5) One-line `HudGroups.gameplay_occluded` connect to a pause flag. (6) Parse `SR_AUTOPILOT` if set (§10.2).
`_process(delta)`: (1) `var snap = SimBridge.poll_snapshot(); if snap: GameState.apply_snapshot(snap)` then **direct calls** `greybox.on_snapshot()` and `player_rig.on_snapshot_wire(...)` — no `SnapshotEventRouter` (its registration lives in `main.gd`; two consumers don't justify touching it — grafted from Candidates 0/2, resolving a judge-flagged ambiguity in Candidate 1). (2) Input pump: `for entry in InputMapper.flush_queue(): SimBridge.send_input(entry)`**the only pump in the codebase lives in main.gd; without this the sandbox connects but never moves**. (3) First-snapshot latch: snap rig + camera + seed greybox + show character (first snapshot arrives in `_process`, never `_ready`). (4) Push the cutaway shader uniform (§8). (5) Autopilot tick if active.
### 1.4 Copied vs extracted vs reimplemented (D-166)
- **Reimplemented from pattern (~40 lines):** poll loop, pump, connect guard, SR_PORT read, first-snapshot latch, `TELEPORT_DISTANCE_THRESHOLD = 5`. Extracting a shared session driver means editing `main.gd`, which D-166 freezes until Phase 5 proper. Each copy carries `# Pattern: main.gd:NNN — extract to shared driver in Phase 5 (T-962)`.
- **Reused as-is:** `SimBridge`, `GameState`, `SnapshotHandler`, `Protocol` (autoloads load from `project.godot` regardless of scene — precedent: standalone `character_creation.tscn`).
- **Reused with one additive seam each:** `InputMapper` (§9), `CharacterVisual` (§6.3).
- **Reimplemented greenfield:** everything 3D. Spike camera math is ported, not the file (spikes live outside `client/`; the spike's 45°-iso preset is struck by D-148).
- Sandbox scripts are not autoloads → full `class_name` typing is fine; the one InputMapper field added follows the autoload untyped-var rule.
## 2. Coordinate & scale convention (§5.5 — stated once, converted in one file)
All conversion lives in **`sandbox_space.gd`** (grafted from Candidate 2 — the strongest structural answer to risk #8's "three cheap ways to be half-a-tile off"):
> **THE CONVENTION: one sim `TilePosition` step = one 0.5 m subtile (D-066/D-222). All sandbox positions/speeds are metres. Sim +x (East) → local +X; sim +y (South, Y-down) → local +Z; sim z-level → Y (Gauntlet z=0 → Y=0). Tile index → world: `Vector3((t.x + 0.5) * 0.5, 0, (t.y + 0.5) * 0.5)`. Sim facing angle θ (0=East, +π/2=South, Y-down radians, `vision_cone.rs`) → ModelRoot yaw: `yaw = PI/2 θ`.**
Verification (re-checked this pass against Godot's actual +yaw semantics, where +90° yaw turns a +Z-facing model to +X): θ=π/2 (South) → yaw 0 → +Z=South ✓; θ=0 (East) → yaw +90° → +X ✓; θ=π (West) → 90° → X ✓; θ=−π/2 (North) → 180° → Z ✓. Full octant table (derived, golden-tested): South 0°, Southeast 45°, **East +90°**, Northeast 135°, North 180°, Northwest 135°, **West 90°**, Southwest 45°.
Three traps explicitly closed:
- **`CharacterVisual.set_facing()` is never called.** Its internal table (`west=+90°, east=90°`, `character_visual.gd:184-193`) assumes a mirrored axis mapping (East→−X) incompatible with ours; using it makes the model face west while walking east — the fatal flaw both other candidates shipped. The rig owns `ModelRoot.rotation.y` exclusively; a comment at the instantiation site names the trap.
- **Live wire sends tile-center floats (N+0.5); TestHarness sends integers.** Always `floori()` the wire float to recover the tile index, then recompute the center — byte-identical behavior in both modes. **Never `round()`.**
- **D-243 "voxel = 1 m" is generation-cascade vocabulary** and appears nowhere in this code. The 1 m visual tile (2×2 subtiles) exists only as major grid lines in the floor shader.
Scale sanity check (grafted from Candidate 2): corridor walkable width is 2 subtiles = 1 m — the ~1.7 m-tall character should just fit; if it doesn't, a conversion is wrong.
## 3. Greybox tile store (risk #1 — the one genuinely new subsystem)
### 3.1 Data structure (`greybox_world.gd`; store core is a `RefCounted`, headless-testable)
```gdscript
enum Kind { FLOOR, WALL } # wire tile_kind is walkability-derived (query.rs:78-82);
# doors are entities on the wire — Door/Object collapse to FLOOR
enum Vis { FORWARD, PERIPHERAL, BOUNDARY_WALL, REMEMBERED }
var _store: Dictionary = {} # Vector3i(x,y,z) sim coords -> Kind (last observation wins)
var _instance_index: Dictionary = {} # Vector3i -> int (slot in its MultiMesh)
var _visible_now: Dictionary = {} # Vector3i -> Vis, rebuilt each consumed snapshot
```
Per consumed snapshot (gated on `GameState.current_tick` change, the `world_renderer.gd:33-36` pattern; called directly by the root): rebuild `_visible_now` from `GameState.visible_tiles` (already merged from both wire spellings by `snapshot_handler.gd:52-64`); new tiles append to `_store` and get a MultiMesh slot with its `Transform3D` written once; tiles whose visibility state flipped get one `set_instance_color()` each. **Diff-only — no full-buffer rebuilds** (contrast the 2D `TileRenderer` clear-and-reset we deliberately do not copy).
**Eviction: never.** Gauntlet worst case 117×125 = 14,625 tiles — trivial. Never-evicting is also the correct semantic: **last observation wins** is the embryo of the Phase-5 "destroyed-while-unobserved renders remembered" fog-memory D-record (grafted framing from Candidate 2). Persists across the Home-key teleport — a feature. MultiMeshes preallocate `instance_count = 16384`, unallocated instances scaled to zero.
### 3.2 Render states — four-state tint (grafted from Candidate 2; surfaces the wire's visibility enum)
| State | Instance color |
|---|---|
| Visible, Forward | white (full albedo) |
| Visible, Peripheral | 85% value |
| BoundaryWall fringe | 70% value (beyond LOS edge by design, #584) |
| Remembered (in store, not visible now) | `Color(0.42, 0.45, 0.52)` — dim, cool, clearly "memory" |
| Never seen | nothing rendered — the void; perception is upstream, server-enforced |
Mechanics specified (fixing a Candidate-1 hand-wave): both MultiMeshes set `use_colors = true`; instance color arrives in the spatial shaders as the `COLOR` built-in and multiplies albedo. Floors use `PlaneMesh` 0.5×0.5 (faces +Y natively — no orientation fix needed); walls use `BoxMesh` 0.5×2.5×0.5 with the mesh center offset so the base sits at y=0 (explicit, fixing Candidate 2's unstated origin detail).
### 3.3 Mesh strategy (engages Q-079, both sides recorded)
**Choice: two `MultiMeshInstance3D`s.** The two things this greybox uniquely needs are per-instance state (four-state tint) and per-material shader uniforms (cutaway) — GridMap has neither per-cell color nor per-cell shader state, so it would need duplicate MeshLibrary items per tint state (item churn on every LOS change). Plain per-tile `MeshInstance3D` (≈15k nodes) rejected outright. **Q-079 annotation carries both positions** (grafted from Candidate 0's counter-case per judge instruction): GridMap with a code-built MeshLibrary + diff-only `set_cell_item` is a legitimately simpler greybox *if per-instance color is not required*, and remains the natural Phase-5 candidate for static real-asset geometry; MultiMesh won here because tint + cutaway demand per-instance parameters. Data point, not resolution. D-244 satisfied: everything is real 3D geometry.
Floor read at 1 m: `greybox_tile.gdshader` draws world-space grid lines — minor every 0.5 m (alpha 0.15), major every 1.0 m (alpha 0.45) — so the floor *reads* as D-066 1 m tiles while data stays per-subtile.
## 4. Locomotion interpolation (§5.2 — the core feel decision)
### 4.0 Rig input contract (grafted from Candidate 2, reconciled with the facing split)
```gdscript
# locomotion_rig.gd — thin Node3D over a pure static core
func set_wire_target(pos_m: Vector3, facing_octant: String, stance: String, tick: int) -> void
var step_window_ms_provider: Callable # player adapter: func(): return InputMapper.MOVE_INTERVAL_MS[stance]
var idle_facing_provider: Callable # player adapter: InputMapper octant (§5); NPC adapter: leave invalid → wire facing always
```
The rig never reads `GameState` or `InputMapper` itself — the sandbox root adapts snapshot fields into `set_wire_target`. An NPC adapter later feeds the same call from `VisibleEntity` rows with no rig changes; leaving `idle_facing_provider` unset collapses the facing split (§5) to pure-wire — single code path by construction, answering the NPC-uniformity objection to the split. `MOVE_INTERVAL_MS` is **read from the InputMapper autoload, never copied** (grafted from Candidate 2, fixing Candidate 1's drift risk).
### 4.1 Model: per-leg constant velocity, `dist / interval`
**Rejected — exponential lerp** (2D `LERP_SPEED 12.0`): velocity is a per-step sawtooth (~90% of a 0.5 m step in ~190 ms, then near-stop until the next target at 400800 ms cadence) — rhythmic lurch with the walk clip cycling over near-zero ground speed; foot cadence can never sync to a sawtooth. Correct for 2D sprites, wrong for a walking skeleton (this was Candidate 0's fatal feel flaw).
**Rejected — D-054's fixed 100150 ms tween:** dash-then-stand stutter at Walk/Careful/Crouch (the §5.2 conflict; resolving it is governance delta #1).
**Chosen — constant-velocity legs sized to the step:** on a new target, cover the distance in exactly one stance interval, so held-key motion is seamless — the character arrives precisely when the next confirmed step lands.
```gdscript
# On snapshot with changed player tile:
# dist = _render_pos.distance_to(new_target)
# if dist > SNAP_DIST_M (2.5 m = 5 subtiles, matches 2D TELEPORT_DISTANCE_THRESHOLD): snap all channels
# else:
# interval = step_window_ms_provider.call() / 1000.0
# _leg_speed = clamp(dist / interval, base, CATCHUP_MAX_FACTOR(3.0) * base)
# Per frame:
# _render_pos = _render_pos.move_toward(target, _leg_speed * delta)
# _velocity = dir * _leg_speed (zero on arrival) # consumed by gait selection + cadence sync
```
One rule handles everything: **cardinal** 0.5/0.4 = 1.25 m/s (Walk); **diagonal** 0.707/0.4 = 1.77 m/s — arrives exactly on time (no √2 on the wire, D-053; a fixed speed would accumulate lag on held diagonals); **multi-tile deltas** (latest-wins drops, risk #2) close within ~one interval under the 3× clamp with feet speeding up to match (§6.2); **teleports** (Home key, cross-map) exceed 2.5 m → hard snap of position, yaw, camera, and a 0.0-blend anim reset. First-ever snapshot snaps (2D precedent `entity_renderer.gd:133-140`). Stance-lag on toggle (≤1 RTT) self-corrects because every leg recomputes `dist/interval`.
### 4.2 Idle hysteresis (walk↔idle flicker killer)
Snapshot arrival jitters ±12 ticks against the client throttle, so the rig frequently arrives a few frames early. `is_moving` stays true until at-target for `IDLE_ENTER_DELAY_S = 0.18` (covers 23 ticks of jitter at every stance; a real stop reaches Idle inside the human ~0.3 s settle expectation, overlapped by the 0.20 s gait→idle blend). This names and kills the most common discrete-step artifact.
### 4.3 Paused ticks, blocked moves
- **Paused** (`game_time.tick_rate == "Paused"`): identical-position snapshots keep arriving; target-chasing is idempotent; the rig settles to Idle through normal hysteresis; the Idle clip keeps breathing (living pause).
- **Blocked move (Q-020):** the rig chases only server-confirmed positions — nothing to roll back, ever. Visible behavior *is* bump-to-turn: facing updates (server sets Facing even on blocked moves), position doesn't, and gait keys off render velocity so the character turns to face the wall and stands. **Annotate Q-020: a strictly interpolate-only client needs zero blocked-move handling by construction** (Candidate 0's crisp framing, grafted).
## 5. Facing authority (§5.3 — "server feet, client eyes", new governance record)
| Rig state | Yaw target source | Why |
|---|---|---|
| Moving (incl. hysteresis window) | snapshot `player_facing` octant | Server sets Facing from the move delta on every accepted move (`movement.rs:479-482`) — it *is* the motion direction, arrives in the same snapshot as the position change. |
| Idle | `idle_facing_provider``InputMapper.facing_octant` — the **same snapped octant that rides the SetFacing wire** | D-054: only the octant crosses the wire, so any mouse-responsive idle facing is client-local by construction. Using InputMapper's own octant (not the raw angle) guarantees the model never shows an octant the server wasn't told. 2D precedent: `entity_renderer.gd:186-193` already draws the indicator from client-local facing. |
| Idle + `dialogue_active`/`free_camera_mode` | frozen (last target held) | InputMapper computes but does not send octants while suppressed (`input_mapper.gd:71`) — freeze on the same condition. |
**Quantified artifact:** idle, the model turns up to one RTT (~50100 ms localhost) before the server's vision cone — imperceptible, and disagreement is bounded by the in-flight SetFacing. Moving, mouse aim is visually ignored (feet follow feet). Single-source alternatives are strictly worse: pure-snapshot idle facing = ~250300 ms laggy 45° pops (the 2D client already cheats past this); pure-client facing breaks server-authoritative display while moving. The NPC-uniformity objection is dissolved by the provider seam (§4.0): NPCs simply don't install a provider. D-151 ("ModelRoot rotates to the true octant angle") satisfied in both states.
**Turn smoothing:** `yaw = lerp_angle(yaw, target, 1 exp(TURN_SHARPNESS·δ))`, `TURN_SHARPNESS = 14.0/s`, per-frame change clamped to per-stance deg/s budgets: Sprint 1080, Walk 720, Careful 540, Crouch 420, Idle 600. A 180° reversal at Walk completes in ~0.25 s ≈ half a step; a 45° corner resolves in ~60 ms.
**Governance:** new policy → claim a D record (architecture — id assigned at claim time via `pql decisions claim`, never presumed) and annotate Q-084 (stays open; it parks the camera-rotation variant).
## 6. Animation state machine
### 6.1 States and clips (verified imported bare names — library `""`, `_Loop` stripped, case-sensitive)
`gait(stance, moving) -> StringName` is a pure function; the table is the ONLY place clip strings live:
| stance | idle | moving | NATIVE_MPS (initial guess, HUD-tuned) |
|---|---|---|---|
| Sprint | `Idle` (2.50 s) | `Sprint` (0.67 s) | 3.2 |
| Walk | `Idle` | `Walk` (1.33 s) | 1.4 |
| Careful | `Idle` | **`Walk_Formal`** (1.33 s) | 1.2 |
| Crouch | `Crouch_Idle` (2.93 s) | `Crouch_Fwd` (2.00 s) | 0.9 |
**Careful pick (§5.4): `Walk_Formal`** — unanimous across candidates. Stance readability is a D-053 goal; a distinct upright-deliberate silhouette reads at ortho distance where slowed `Walk` reads as lag. Fallback if it reads "parade march": `Walk` at 0.6× — one table cell. `Jog_Fwd` is the reserve if `Sprint` reads too aggressive at 2.5 m/s. A gdUnit test asserts every table cell exists in a real headless CharacterVisual's `get_animation_list()` (grafted from Candidate 2 — guards the case-sensitive silent-miss end-to-end, not against a constant copy).
### 6.2 Cadence sync (kills foot-slide)
While moving: `speed_scale = clamp(rig.current_speed / NATIVE_MPS[clip], 0.6, 1.8)`. Because rig speed is **constant per leg**, speed_scale is constant per leg — no within-step wobble (the whole point of rejecting exponential lerp). Catch-up bursts push it up so feet chase instead of skating. Idle states run at 1.0. The DebugHud shows live speed + speed_scale, making tuning `NATIVE_MPS` a five-minute on-screen job.
### 6.3 Transitions: `AnimationPlayer.play(name, custom_blend)` — no AnimationTree
Additive `CharacterVisual` API (CharacterVisual is the D-149 3D compositor, explicitly not the D-166-frozen 2D renderer; this ticket is its designated first external driver):
```gdscript
func play_animation(anim_name: String, blend_time: float = -1.0) -> void # custom_blend passthrough; -1 = current hard cut
func get_animation_player() -> AnimationPlayer # speed_scale + phase access
```
Blend table: Idle→gait **0.12 s**, gait→Idle **0.20 s**, gait↔gait **0.15 s**, ↔Crouch_* **0.25 s**, teleport **0.0** (hard cut — a cross-map jump must not smear). Transitions fire only on state change (edge-triggered); loops never restart mid-cycle.
- **Rejected — AnimationTree:** `load_descriptor()` re-runs destroy/recreate `AnimPlayer` (`character_visual.gd:761-767`), invalidating external tree paths; blend spaces buy nothing for discrete tile locomotion; UAL has no start/stop/turn clips for a tree to use anyway. The anim driver's `play(clip, blend, speed)` output surface is where a tree slots in later.
- **Rejected — manual two-player crossfade:** reimplements the engine.
- **Known caveat (grafted from Candidate 0):** cross-clip blending with `remove_immutable_tracks=true` can pop on bones animated in only one clip; the six clips share the full 65-bone skeleton so exposure is low — if a pop shows, the fallback is a rig-local AnimationTree, finding recorded on the ticket.
**Phase preservation on gait↔gait:** capture `phase = current_position/length` before switching, `play(new, blend)`, `seek(phase * new_length, false)` so legs keep their beat. Pre-flagged caveat: verify seek-during-blend doesn't cancel the crossfade in Godot 4.6; if it does, drop the seek — the 0.15 s blend masks resync acceptably (tune-by-eye, recorded on the ticket).
**`ual_extended.glb`: deferred.** No locomotion clip needs it; naive wiring collides on the duplicate `""` library name. Wiring plan (register under an explicit library name, switch addressing to `"lib/Clip"`, per the spike loader) recorded in the D-record. Q-063 seam: `gait_changed(state)` signal + clip-phase access.
## 7. Camera (D-148 rig, locked follow)
`follow_camera_3d.gd`, spike pivot-orbit math (`camera.position = pivot + basis * Vector3(0, 0, CAM_DIST=30)`), orthographic `size = 9.0`, near 0.1 / far 100.
- **Presets (pitch from horizontal — trust D-148's preset list over its contradictory opening prose; editorial note filed, §11):** `[-30 (gameplay default), -5 frontal, -80 overhead]`. **T-key cycles** — sanctioned dev affordance (D-148/D-158), tilt lerp 3.0/s. The spike's 45 iso preset is struck by D-148 and absent.
- **Yaw 0, always. Stepped player yaw is OUT** (not canon; a D/Q record comes first). The diamond view is entirely `WorldRoot.rotation.y = 45°`.
- **Locked follow (D-015, no panning):** `pivot = pivot.lerp(PlayerRig.global_position, 1 exp(6.0·δ))` — deliberately the *only* soft layer (§0). Against constant velocity this converges to a constant trailing offset `v/k`: Walk 0.21 m, Sprint 0.42 m (~4.7% of view height) — reads as speed, zero jitter. `LOOKAHEAD_S = 0.0` knob exposed for the tuning pass.
- **Snap:** rig emits `teleported` → hard pivot set; same on first-snapshot latch.
- **Zoom:** scroll lerps ortho size 614 — sandbox dev affordance, ungoverned and kept that way.
## 8. Wall cutaway — first pass (ticket scoping call: client-side render mode)
Entirely in `greybox_wall_cutaway.gdshader`; per-frame CPU cost = one uniform write (`u_char_pos_xz` = the rig's **interpolated** world XZ, so the cut zone glides with the character and spatial smoothstep becomes temporal smoothness). Camera ground-forward is a constant `vec2(0, -1)` world-space (yaw-0 camera looks Z; correct under the 45° WorldRoot because the test runs in world space).
```glsl
vec2 to_wall = v_world_xz - u_char_pos_xz;
float side_f = smoothstep(-0.25, 0.75, to_wall.y); // wall between camera and character
float rad_f = 1.0 - smoothstep(CUT_RADIUS - CUT_BAND, CUT_RADIUS, length(to_wall)); // 5.0 m, 1.5 m band
float allowed = mix(WALL_H, CUT_HEIGHT, side_f * rad_f); // 2.5 m -> 0.75 m stub
if (v_world_y > allowed) discard;
```
A half-disc of camera-side wall drops to a 0.75 m stub with smooth falloff — the Xenonauts read minus per-building logic the greybox doesn't have. Open box tops are acceptable greybox fidelity; ghost band/dither/capping are named polish on the same shader. **Rejected:** per-cell mesh swap / per-instance `scale.y` rewrite (Candidate 2 — pops per tile step, full-buffer churn); hard-edged discard window (Candidate 0 — works, but the smoothstep is strictly smoother at equal cost); transparency ghosting (ortho sorting headaches).
**Composition with fog (the ticket's scoping call, verified consistent):** cutaway cuts *render height*; the four-state tint (§3.2) gates *knowledge* — independent factors in the same shader. Remembered walls can be cut too (the player's camera doesn't care what the character currently sees). Perception stays server-enforced upstream. If this holds through implementation, mint the perception-domain D record on landing, per the ticket.
## 9. Input integration — the `facing_angle_provider` seam (mandatory; repo-verified this pass)
`InputMapper._update_facing_from_mouse()` (`input_mapper.gd:182-195`) anchors on `GameState.player_position * Constants.TILE_SIZE` through the 2D canvas transform — in a 3D scene that anchor is a far-off-screen point, so `facing_angle` is garbage and `_wasd_to_world_dir()` (:202-213) rotates ALL WASD movement by it: the character could be moved but not steered, with wrong SetFacing octants riding the wire. An anchor-only override (Candidate 0) is insufficient — the screen-space mouse angle still isn't a sim-space angle under the 45° WorldRoot rotation + 30° ortho foreshortening (constant ~45° skew plus up to ~19° warp). The only correct fix, one additive seam, 2D path byte-identical when unset:
```gdscript
# input_mapper.gd (autoload — untyped var per parse-order rule)
var facing_angle_provider = Callable() # () -> float sim-space radians (0=East, +PI/2=South), or NAN = no update
func _update_facing_from_mouse() -> void:
if facing_angle_provider.is_valid():
var a: float = facing_angle_provider.call()
if is_finite(a):
facing_angle = a
facing_octant = _angle_to_octant(a)
return
# ... existing 2D canvas-transform path, unchanged ...
```
The sandbox provider: unproject the mouse (`project_ray_origin/normal`) onto the y=0 plane, convert through `WorldRoot.to_local()` (undoing the 45°), take the delta from the rig's local position, return `atan2(delta.z, delta.x)` — which *is* the sim convention because local +Z = sim +y (§2). Deadzone 0.1 m (mirrors the 2D jitter guard). Everything downstream — octant snap, SetFacing-on-change, mouse-relative WASD, dialogue suppression — runs unchanged; D-054 semantics preserved exactly. D-166: InputMapper is input infrastructure, not the frozen 2D render path; the change is additive and inert for 2D.
## 10. Verification
### 10.1 Primary loop — live eyeball with numeric instrumentation
All feel judgment happens under `SR_LIVE=1` (CLAUDE.md preference; TestHarness diverges in coords/pacing — risk #9):
```bash
# T1: cd server && cargo run --bin settled-reach-server -- --test-mode
# T2: SR_LIVE=1 ~/bin/godot4 --path client res://scenes/locomotion_sandbox.tscn
```
Server exits on client disconnect — fresh spawn per run. DebugHud: tick, tick_rate, stance, player tile, render speed (m/s), leg distance, clip + speed_scale, snapshot age, yaw target/actual, store size — cadence sync is verifiable numerically on screen, not just by eye.
### 10.2 Harness + deterministic input
`visual_capture.gd` gains an additive per-scenario `"scene"` field (default `res://scenes/main.tscn` — zero change to existing scenarios/goldens); the fog-overlay wait is `get_node_or_null`-guarded when absent. **Deterministic capture input is `SR_AUTOPILOT`** (grafted from Candidate 0 — highest-fidelity of the three): `SR_AUTOPILOT="east:2.0,south:1.5,stance_up,east:1.0"` parsed by the sandbox root into timed `Input.action_press/release` on the **real actions**, so scripted input exercises the genuine InputMapper throttle/octant/facing path — no test-only code in the rig, no SimBridge bypass masking input bugs. ~25 lines.
`tests/visual.json` additions: `locomotion_idle_live` (`"live": true`, new scene, static golden — character idling at Hub spawn, 30° frame, greybox floor); `locomotion_gaits` (autopilot walk east → turn south → stance ladder → `make visual-movie` contact sheet — the gait/cutaway/memory-dim review artifact; animation frames deliberately **not** golden-compared); `locomotion_cutaway` (autopilot walk south near a wall → golden of the stub). If the harness lacks env passthrough per scenario, that is a second small additive key in the same edit.
### 10.3 Milestones — how each is seen (grafted spine from Candidate 2)
| Milestone | Seen via |
|---|---|
| M1 Boot: character idles at Hub spawn (50,58), lit, outlined, greybox under feet | live recipe + `make screenshot SCENARIO=locomotion_idle_live` |
| M2 Walk: continuous cadence, no inter-step stall, correct E/W facing | movie frames — equal displacement per frame; HUD speed constant |
| M3 Stance ladder: 4 speeds, distinct silhouettes, foot-slide tuning | `locomotion_gaits` movie + live Sprint Gauntlet 28-tile straight |
| M4 Steer/aim: mouse-relative WASD correct in 3D, idle aim turns the model | live manual session (the §9 seam's proof) |
| M5 Store: walk a loop — remembered tiles dim behind you, void beyond | live + before/after screenshots |
| M6 Camera: 30° default, T-cycle, constant-offset follow, 45° diamond | live; screenshot per preset |
| M7 Cutaway: south walls drop to stub, character readable behind | `locomotion_cutaway` screenshot |
| M8 Teleport: Home mid-walk → snap, no glide, anim hard-cut | live manual |
### 10.4 gdUnit4 (headless — logic only, by design of the pure-core split)
CAN cover: `SandboxSpace` golden conversions (tile↔world, wire-float `floori` recovery, all 8 octant yaws incl. the E/W-critical cells); store accumulation/never-evict/four-state flips (synthetic dicts + a fixture-replay case pushing `tests/fixtures/gauntlet/*.msgpack` through `Protocol.decode_snapshot()``GameState` → store); mover math (leg-speed derivation incl. diagonal 1.77 m/s, 3× clamp, 2.5 m snap, idle hysteresis, paused idempotence, blocked-move zero-motion); yaw shortest-arc + budget clamp; `gait()` every cell exists in a real headless CharacterVisual's `get_animation_list()`; provider octant math; `WallCutaway`-equivalent predicate if extracted. CANNOT cover: blend quality, foot slide, cutaway pixels, ortho composition — §10.1/10.2's job. `tests/run-godot --filter locomotion|sandbox`; the pre-push gate runs everything — no pre-emptive local duplication (team rule).
## 11. Governance deltas (consolidated)
1. **Claim D (architecture)***3D locomotion interpolation*: per-leg constant-velocity windows keyed to the client throttle (`dist/interval`, read from `InputMapper.MOVE_INTERVAL_MS`; 3× catch-up clamp; 2.5 m/5-tile teleport snap; diagonal 1.41× accepted as sim truth), cadence-synced `speed_scale`, Careful=`Walk_Formal`, `ual_extended` named-library wiring plan, stepped camera yaw reaffirmed OUT / T-cycle as the sanctioned dev affordance. Resolves design-input §5.2; **annotate D-054** ("100150 ms Tween" scoped to the 2D renderer henceforth). Id assigned at claim time via `pql decisions claim D architecture "…"`.
2. **Claim D (architecture)***3D facing presentation authority*: moving = server snapshot octant; idle = client-local aim octant (the same octant sent as SetFacing), injected via `idle_facing_provider` (NPCs: unset → pure wire); frozen under input suppression; includes the verified octant→yaw table (South 0°, East +90°, West 90°, North 180° — `yaw = π/2 θ`) as the confirmed compositor-api-spec §2 composition (rotation composes once via WorldRoot), and the explicit "never call `CharacterVisual.set_facing()` from the rig" trap note. Resolves design-input §5.3.
3. **Annotate Q-084** — facing-source settled by delta #2 for presentation; walk-vs-aim animation split and camera-rotation option remain parked.
4. **Annotate Q-079** — MultiMesh chosen for the sandbox (per-instance four-state tint + cutaway shader state); counter-case recorded: GridMap with code-built MeshLibrary + diff painting is simpler where per-instance color isn't needed and remains the Phase-5 static-geometry candidate. Data point, not resolution.
5. **Annotate Q-020** — strict no-prediction rig confirmed: silent move rejection needs zero client handling by construction; visible behavior is bump-to-turn.
6. **Annotate Q-063** — footstep seam exists (`gait_changed` signal + clip-phase access); animation-event vs raycast still open.
7. **Deferred D (perception), minted at sidequest close-out per the ticket's own instruction** — wall cutaway is a client-side render mode on wall data inside the information boundary, decoupled from and composing with fog-of-perception; fog-memory-applied-to-geometry nuance recorded for Phase 5.
8. **Editorial note on D-148** — its opening "30° tilt (60° from horizontal)" contradicts its own preset list; add a clarifying sentence (presets authoritative: 30° from horizontal). Flagged per CLAUDE.md rather than silently trusted.
9. **T-1088 close-out notes** — no new Gauntlet room needed (constraint 12 untriggered, server untouched); Phase-5 extraction note for the ~40 lines of duplicated boot boilerplate; seek-during-blend and `remove_immutable_tracks` caveats with outcomes.
## 12. Constants appendix (`sandbox_constants.gd` — every feel knob in one file)
```gdscript
const SUBTILE_M := 0.5 # D-066/D-222 — THE convention (§2)
const WALL_H := 2.5
const CUT_HEIGHT := 0.75
const CUT_RADIUS := 5.0
const CUT_BAND := 1.5
# stance intervals: NOT duplicated — read InputMapper.MOVE_INTERVAL_MS at runtime
const CATCHUP_MAX_FACTOR := 3.0
const SNAP_DIST_M := 2.5 # 5 subtiles — matches 2D TELEPORT_DISTANCE_THRESHOLD
const IDLE_ENTER_DELAY_S := 0.18
const TURN_SHARPNESS := 14.0
const TURN_BUDGET_DEG := {"Sprint": 1080.0, "Walk": 720.0, "Careful": 540.0, "Crouch": 420.0, "Idle": 600.0}
const GAIT_CLIP := {...} # §6.1 — the ONLY place clip strings live
const NATIVE_MPS := {"Walk": 1.4, "Walk_Formal": 1.2, "Sprint": 3.2, "Crouch_Fwd": 0.9}
const SPEED_SCALE_CLAMP := Vector2(0.6, 1.8)
const BLEND := {"idle_to_gait": 0.12, "gait_to_idle": 0.20, "gait_to_gait": 0.15, "crouch": 0.25, "teleport": 0.0}
const CAM_PITCH_PRESETS := [-30.0, -5.0, -80.0] # D-148; -45 iso struck
const CAM_DIST := 30.0
const CAM_ORTHO_SIZE := 9.0
const CAM_FOLLOW_RATE := 6.0
const CAM_TILT_RATE := 3.0
const LOOKAHEAD_S := 0.0
const MAP_ROTATION_DEG := 45.0 # WorldRoot — the single D-148 Transform3D
const TINTS := {"forward": Color.WHITE, "peripheral": 0.85, "boundary": 0.70, "remembered": Color(0.42, 0.45, 0.52)}
const MOUSE_AIM_DEADZONE_M := 0.1
```
## 13. Decisions made and why (where candidates disagreed)
1. **Interpolation model** — exp lerp k=12 (C0) vs per-leg `dist/interval` constant velocity (C1) vs constant-speed seek + clamp (C2): **C1.** Exp lerp produces a per-step velocity sawtooth foot cadence can never sync to (two judges called it fatal for feel). C1 and C2 are near-equivalent; C1's per-leg recompute plus cadence-synced `speed_scale` is the only model under which foot-slide elimination is possible, and C2's elegant single-clamp formula is effectively subsumed.
2. **Octant→yaw table** — C0 and C2 copied `character_visual.gd`'s `east=90°/west=+90°` table under a sim-x→+X mapping: **verified wrong against the repo this pass** (the model would face west walking east, and both would have baked the mirror into governance/goldens). C1's `yaw = π/2 θ` (East=+90°) is arithmetically verified and adopted; `set_facing()` is never called.
3. **Mouse input in 3D** — C2 missed the breakage entirely (fatal: unsteerable character); C0 fixed only the anchor (screen angle ≠ sim angle under 45°+pitch); **C1's `facing_angle_provider`** (ray-to-ground-plane + `WorldRoot.to_local` + `atan2(z,x)`) is the only correct D-054 path and is adopted verbatim.
4. **Facing authority** — single wire source (C0/C2) vs "server feet, client eyes" split (C1): **C1's split**, because pure-snapshot idle aim is ~250300 ms of laggy 45° pops the 2D client already cheats past, and the shown octant is always one the server was told. C2's NPC-uniformity objection is answered structurally: the idle source is an injected provider — NPCs leave it unset and collapse to single-source, one code path.
5. **Mesh strategy** — GridMap (C0) vs MultiMesh (C1/C2): **MultiMesh**, because the four-state tint and cutaway need per-instance/per-material state GridMap lacks; C0's GridMap counter-case is preserved inside the Q-079 annotation per judge instruction.
6. **Cutaway** — hard discard window (C0) vs smoothstep height cut in shader (C1) vs CPU classifier + instance scale.y (C2): **C1** — fragment-based like C0 but with spatial falloff keyed to the interpolated position (temporal smoothness for free); C2's per-step buffer rewrite pops.
7. **Snapshot fan-out** — SnapshotEventRouter (C1, ambiguous) vs direct consumer calls (C0/C2): **direct calls** — the router is wired by main.gd for the 2D stack; two consumers don't justify touching it.
8. **Stance intervals** — copied table (C0/C1) vs read from the autoload (C2): **read `InputMapper.MOVE_INTERVAL_MS`** — kills the drift risk.
9. **Deterministic capture input** — flows (C1) vs `SR_SCRIPT` SimBridge bypass (C2) vs `SR_AUTOPILOT` real Input actions (C0): **C0's autopilot** — it exercises the genuine InputMapper path, so captures would have caught the very input bug C2 shipped.
10. **File organization**`sandbox/` (C0/C1) vs `rendering3d/` promotion split (C2): **`sandbox/` + a promotion-status column** — documents the seam without pre-committing Phase-5 directory shapes the sandbox exists to discover (judges split; the column captures C2's value at zero structural cost). C2's `SandboxSpace` single-conversion-authority file is adopted regardless.
11. **Wall height** — 1.5 m (C0) vs 2.5 m (C1/C2): **2.5 m** — walls shorter than the 1.7 m character read wrong.
12. **Peripheral/BoundaryWall tint** — ignored (C0/C1) vs four-state (C2): **four-state** — one instance-color value each, and it surfaces the wire's visibility enum in the greybox for free.
13. **New Gauntlet room** — all three: not needed. Hub + corridors + Sprint Gauntlet cover every milestone; constraint 12 untriggered.
14. **D-record ids** — C1 presumed "next free is D-248": **never presume**; ids are assigned at `pql decisions claim` time.
## 14. Implementation plan (each step one-agent-sized; groups run in parallel)
- **Group A (serial foundation):** S1 scene skeleton + boot + space/constants.
- **Group B (parallel on A):** S2 input seam · S3 greybox store/renderer · S4 locomotion rig · S5 camera.
- **Group C (parallel on B):** S6 animation machine + CharacterVisual API · S7 cutaway shader.
- **Group D (serial close-out):** S8 harness + autopilot + scenarios → S9 live feel-tuning + governance records.
Per team rules: no pre-emptive fmt/clippy/test runs — the push gate owns verification; no server changes, so full `cargo test` is untouched. Feel tuning (S9) is deliberately last and human-in-the-loop against the live server.
+1
View File
@@ -491,6 +491,7 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
- **Phase-5 nuance recorded, not implemented:** when destruction exists, a wall destroyed *while unobserved* should keep rendering in its remembered state — the fog-memory model applied to geometry, not just entities. The T-1088 never-evict tile store (last-observation-wins, remembered-state tint) is the embryo of exactly this.
- **Cross-reference:** [D-011](#d-011), [D-015](#d-015), [D-148](architecture.md#d-148-30-low-angle-camera-with-45-map-rotation--supersedes-d-019), [D-244](architecture.md#d-244-asset-rendering--3d-objects-in-world-2d-limited-to-textures--flat-artwork), Q-081 (a 3D fog overlay composes on top of the cutaway, unaffected). Tickets: T-1088 (first implementation: smoothstep half-disc height-cut, `client/shaders/sandbox/greybox_wall_cutaway.gdshader`).
- **Dissent:** None.
- **Amendment (2026-07-06/07, T-1088 live sessions):** the half-disc evolved through play evidence into three cutaway MODES — sightline corridor (pitch-derived reach), bottom-walls-low (a wall drops when a KNOWN non-wall tile sits behind it, scanning through 2-thick wall runs and the corner diagonal), and both. **User verdict: mode 2 (both) is the shipped default** (`SandboxConstants.CUTAWAY_MODE`); C cycles modes as a dev affordance. Cut decisions are per-instance whole-box quantized (per-fragment falloff carved organic notches). The decoupling principle of this record is unchanged — all modes remain pure render-height treatments on boundary-filtered wall data.
---