From 3d6dcc9d056480a67e7a3d57dc7543486d151ea7 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Tue, 7 Jul 2026 14:15:56 +0200 Subject: [PATCH] =?UTF-8?q?docs+fix:=20sidequest=20close-out=20=E2=80=94?= =?UTF-8?q?=20T-1095=20head=20tint,=20design=20doc,=20wardrobe=20docs,=20c?= =?UTF-8?q?hangelog=20(T-1088/T-1089)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .pql/changelog/ticket_history/2026-07.sql | 81 ++++ .pql/changelog/ticket_idmap/2026-07.sql | 1 + .pql/changelog/tickets/2026-07.sql | 86 ++++ CHANGELOG.md | 1 + client/scripts/rendering/character_visual.gd | 16 +- .../character-asset-organization.md | 123 +++++- .../design/t1088-locomotion-sandbox-design.md | 389 ++++++++++++++++++ governance/decisions/perception.md | 1 + 8 files changed, 684 insertions(+), 14 deletions(-) create mode 100644 docs/design/t1088-locomotion-sandbox-design.md diff --git a/.pql/changelog/ticket_history/2026-07.sql b/.pql/changelog/ticket_history/2026-07.sql index b4407040e..ae7278093 100644 --- a/.pql/changelog/ticket_history/2026-07.sql +++ b/.pql/changelog/ticket_history/2026-07.sql @@ -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 . 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 _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 . 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 _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; diff --git a/.pql/changelog/ticket_idmap/2026-07.sql b/.pql/changelog/ticket_idmap/2026-07.sql index c5f0b31ea..2f4b567fd 100644 --- a/.pql/changelog/ticket_idmap/2026-07.sql +++ b/.pql/changelog/ticket_idmap/2026-07.sql @@ -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); diff --git a/.pql/changelog/tickets/2026-07.sql b/.pql/changelog/tickets/2026-07.sql index 613e43244..faa6d76e8 100644 --- a/.pql/changelog/tickets/2026-07.sql +++ b/.pql/changelog/tickets/2026-07.sql @@ -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 . 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 _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 . 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 _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); diff --git a/CHANGELOG.md b/CHANGELOG.md index ec01baff5..ff1c2cf75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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-248–D-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 diff --git a/client/scripts/rendering/character_visual.gd b/client/scripts/rendering/character_visual.gd index 5b120b109..de6a54b99 100644 --- a/client/scripts/rendering/character_visual.gd +++ b/client/scripts/rendering/character_visual.gd @@ -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) diff --git a/docs/architecture/character-asset-organization.md b/docs/architecture/character-asset-organization.md index e31819a0e..97fb14aca 100644 --- a/docs/architecture/character-asset-organization.md +++ b/docs/architecture/character-asset-organization.md @@ -444,9 +444,14 @@ Convention: `_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** (`_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:** `_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 `) 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 diff --git a/docs/design/t1088-locomotion-sandbox-design.md b/docs/design/t1088-locomotion-sandbox-design.md new file mode 100644 index 000000000..2e46a5023 --- /dev/null +++ b/docs/design/t1088-locomotion-sandbox-design.md @@ -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 400–800 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 100–150 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 ±1–2 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 2–3 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 (~50–100 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 = ~250–300 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 6–14 — 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** ("100–150 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 ~250–300 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. \ No newline at end of file diff --git a/governance/decisions/perception.md b/governance/decisions/perception.md index 845902ad8..622b7b6e2 100644 --- a/governance/decisions/perception.md +++ b/governance/decisions/perception.md @@ -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. ---