Repository navigation
Module 3 scene update - #63
Closed
sophamixay2018-eng wants to merge 63 commits into
Closed
sophamixay2018-eng wants to merge 63 commits into
sophamixay2018-eng wants to merge 63 commits into
Conversation
Ports the localization work from the Boy branch and fixes three things that would
have blocked Module 3, which is far more text-heavy than Modules 1 and 2.
- LocalizationManager now parses RFC-4180 CSV over the whole file rather than
splitting on commas. Module 3 dialogue is full sentences ('she has been hot for
three days, since Monday'), every one of which contains a comma. Quoted values
may also contain line breaks.
- Adds a Lao column alongside English, Vietnamese and French; adds LocalizedKey
(accepts world-space TextMeshPro, not only TextMeshProUGUI) and ColorTexts.
- Switches the Lao font asset to a dynamic atlas backed by Lao_SomVang.ttf. It was
static with only 64 glyphs baked and 12 ASCII characters, enough for four menu
strings and nothing else; the TTF carries all 95 printable ASCII and 87 Lao
codepoints, so any string now renders.
- Language choice persists in PlayerPrefs; GetLocalizedValue takes format args.
LocalizedKey also had a discarded GetComponent call and the manager defaulted to a
language the CSV did not contain.
The spine of Module 3. A headless, seeded, reproducible transmission model with no UnityEngine dependency, plus the tests that hold it to the design's teaching goals. Why headless: section 5 of the design requires that, before any score is shown, the game replays what would have happened had the first patient been covered on day one. A counterfactual is only honest if the outbreak can be re-run from a seed with one input changed, which is impossible if the epidemic lives in Update(), coroutines and UnityEngine.Random. Random draws are keyed on (seed, day, stream, entity) rather than taken from a running sequence, so adding one net does not shift everybody else's draws and the two runs stay comparable. Mosquitoes are individuals, not per-household counts, because the trace-back replay has to name the source house, the container the mosquito came from and the night the patient slept unprotected. Only an individual carries that provenance. The tests encode section 5's 'designing it so the lesson cannot be avoided' as assertions. Over 200 seeds: do nothing 14.0 secondary cases cleared every container, no nets 10.5 still an outbreak net the well 13.6 no better than nothing net the first two patients, day 1 5.4 61% reduction net every visible patient 4.7 69% of the residue from invisible sources The assertions are relative rather than absolute so they survive re-calibration while still catching a parameter set that has stopped teaching the lesson. One deviation from CLAUDE.md, which says no assembly definitions for game code: the Sim folder gets one. It is safe because the assembly is a leaf that references nothing, Assembly-CSharp auto-references asmdefs so every MonoBehaviour can still use these types, and it is what makes NUnit tests possible - the first in this repo. Also adds ReclaimNet: a net can be carried on to the next patient, so the shortage that section 5 requires is about where the squad's attention goes rather than about a warehouse.
The simulation parameters are GAMA's to set, not ours to hand-pick in C# literals. This is the Unity side of that handover, built to take delivery while a session is already running. - ParameterCsv reads a key,value CSV with any extra columns GAMA wants to add, and WriteTemplate emits the current values with units, sections, apply scopes and notes. That template (Resources/Module3/Module3Parameters.csv) is the document to hand the GAMA modellers so both sides agree on key names before either writes code. - Validation refuses a file whole rather than half-applying it: out-of-range values, reversed min/max pairs, and two design invariants - a net for every household, or a net that helps a well person as much as a patient - either of which would quietly remove the module's lesson. - Partial files are layered onto what is in force, so GAMA can send only what changed. - ParameterTimeline keeps parameters as a sequence keyed by the day they took effect, not a single object. A mid-session change applies from the NEXT day and never retroactively: people already infected keep the course of illness they were given, or the trace-back replay becomes a lie about what the squad saw. The counterfactual replays against the same timeline, so the difference between the two outbreaks is still the net and not a parameter change only one run saw. - Structural keys (plot count, what Module 2 left in the yards) cannot rebuild the neighbourhood under a player who is standing in it, so they are held for the next session. - Unknown keys are logged, not fatal: a newer GAMA model may know more than this build. Module3GamaLink subclasses SimulationManager rather than editing it, per CLAUDE.md section 9. Only one SimulationManager subclass belongs on a GameObject - Modules 1 and 2 both have two fighting over the static Instance in Awake, and Module 3 should not inherit that.
Documentation/Module3-Roadmap.md - the build plan: phases, what is salvageable from the Boy branch's Start_M3 greybox, the repo traps worth knowing before hitting them, and why none of the design's six open questions block code. Documentation/Module3-Asset-Brief.md - a self-contained brief for whoever produces the models, whether that is a generative 3D model, a contractor or someone in Blender. Format, scale, pivots, socket conventions, triangle budgets, file paths and naming, split into placeholder and final tiers so the placeholders unblock code immediately. The asset gap is much smaller than the design document implies: the Lao houses, fences, gates, PF_Bed, PF_MosquitoNet and the whole Module 2 container set already exist in Team Assets. What is genuinely missing is the fan, the two window screen states, a few handheld props and the villager animation clips. Villager health states are deliberately animation and material swaps rather than separate meshes - cheaper, reads better at conversational distance, and it keeps sick characters from being visually flagged, which section 5 is emphatic about.
Section 8 makes the handover the clock. A turn is two to four minutes, far too short for anything in dengue to develop, so the calendar jumps between turns rather than anything being sped up - 'The compression is in the calendar, not in the biology.' - Sim/Session.cs runs the turn and round structure and advances the model in whole days at a boundary, never continuously. No UnityEngine, so the whole session shape is testable headlessly; M3Session only supplies the clock and the events. - Sim/HandoverBrief.cs is what the Coach briefs the incoming Pilot with. Section 8: 'Every handover should show consequence.' So it carries new cases, patients who are unwell and uncovered, anyone needing a doctor now, households nobody has visited, and people whose fever has broken - 'Danger often arrives as the fever comes down.' - Session.RecordedActions() and PatientZeroId() are what the counterfactual replays. Patient zero is deliberately the first SYMPTOMATIC index case: replaying 'you could have covered them' only works if the squad could have found them. - M3DebugDriver plays a whole session from the keyboard, no headset and no art. Editor and development builds only. Two bugs the session tests caught: 1. UnreferredWarningGraceDays was 2 against a 3-day handover jump, so a warning sign could appear and send itself to hospital entirely between two turns. The squad never saw it and could not have acted, which quietly made section 4's referral rule unreachable rather than merely hard. Raised to 4, and ParameterCsv now refuses any file where the grace period does not outlast the handover. With this fixed, a squad that refers now ends a session with zero unaided hospitalisations, against 2.25 for one that does not. 2. The febrile and defervescing windows overlapped on the last day of fever, so on that day an uncovered patient was reported as uncovered and never as 'looking better' - masking exactly the moment the module wants the Coach to warn about. Calibration is unchanged: 14.1 secondary cases for a do-nothing squad against 4.7 for one that covers every visible patient.
Raising UnreferredWarningGraceDays left the committed Resources CSV carrying the old value, which the new validation rule then rejected - so every session would have logged an error at startup and fallen back to the built-in defaults, with nothing visible in play mode but a warning nobody reads. - Regenerated the file. - Added a 'Module 3 > Regenerate parameter template' menu item, which refuses to write a file that would not pass validation rather than moving the failure to startup. - Added tests that the shipped file exists, is accepted, matches the built-in defaults and carries every key in the schema. Regenerating by hand is exactly the step that gets forgotten, so it is now a test rather than a note in a document.
The validation rule only caught net_over_resting_person being worse than net_over_active_person, but 0.9 against 0.92 is not 'worse' - it is 'no better', and that is just as fatal: it makes netting the sick and netting the well the same action and the module has no lesson left. Tightened to at most half, which is the threshold the calibration suite already asserts, so a parameter file cannot pass validation and then fail calibration. Caught by running the suite in Unity rather than headlessly. Full EditMode run: 40 tests, 40 passing, across calibration, session, parameter CSV and baked-file drift.
All nine placeholder assets are built and independently verified against the brief: within triangle budget, one material each, unit scale, real metres, Fan_Blade separated, both screens matching the measured Lao house window opening, and the rolled-up net sharing the deployed net's origin so the two swap 1:1. Three divergences from what the brief asked for, and the delivered asset wins each: - Socket names are Socket_Mount / Socket_Grip / Socket_Hook / Socket_Light, and the fan's moving part is Fan_Blade. No code had bound to a socket name yet, so the artefact is the source of truth and the brief was corrected. - One SM_DrinkingVessel rather than a separate glass and jug: the simulation has a single BringWater action, so two meshes served no mechanic. - The deployed net reuses the existing SM_MosquitoNet rather than being remodelled; only the rolled-up variant was new. The brief also asked for an airflow-direction socket on the fan that turns out not to be needed - SetFan applies to a household, not to a person. Recorded as D12 in the decisions log.
The end of the session - section 5 calls these the moments the module is built to deliver. All of it is a read off the transmission log, which is what Phase 1's individual mosquitoes bought. - TraceBack turns a case into the replay section 5 asks for: the container the mosquito came out of, the person it fed on, and where it ended up. Every field was recorded at the moment of infection, so this is a read rather than a reconstruction. - OutbreakMap: every chain that ran and every chain that stopped, plus the households where the squad's cover held. - Scoring implements section 10, including its two structural rules - a broken chain outscores everything else combined (measured: 1030 points against 127 in played sessions, dominating in 155 of the 155 sessions that broke one), and nothing the player gets wrong ever subtracts. A missed warning sign and an unnecessary referral are notes worth zero points, because the module corrects by showing consequences. - FieldJournal exports the facilitator's copy, ending on section 5's one question: 'Why did the outbreak stop, or why did it not?' Two things worth recording: Chain-broken cannot be judged from the end state. A squad that plays well takes the net back once a patient is past it, so by the end almost nobody whose chain they broke still has a net - the first implementation scored zero in every session. Person now records WasCoveredWhileInfectious and EverFedOnWhileInfectious as they happen. The Module 2 bridge needed widening. Section 5 hangs the debrief on a chain that began where nobody was ever visibly ill, but that chain only appears in 67% of sessions for a squad that plays well - they have fewer chains of every kind, so the squads who most deserve the moment are least likely to see it. The map now also carries the people who caught it and never felt a thing, which exists in every session and makes the same point; the chain is the sharper version when it happens.
Implements the "Unblocking pattern" from the Module 3 roadmap §4: a greybox
placeholder at the final path and name for every genuinely-missing asset, so
code can bind to prefab paths and Socket_* transforms now and art can swap the
mesh inside the prefab later with no scene churn.
Nine meshes, generated procedurally and committed with their FBXs:
electric fan (animatable blade), intact and torn window screens, rolled-up
mosquito net, repellent bottle, drinking vessel, cloth, torch, and a
provisional health centre massing. Each maps to a Phase 1 player action.
Dimensions are measured off existing repo art rather than invented: the window
leaves give a 1.50 x 1.00 m opening with a 0.1202 m reveal, and the rolled-up
net shares an origin and canopy height with SM_MosquitoNet so the two net
states swap at one transform. Only the rolled-up state was modelled -- the
deployed one reuses the shipped mesh, and PF_MosquitoNet is untouched.
Tooling:
- Tools/Module3/make_placeholders.py parametric meshes, enforces tri budgets
- Tools/Module3/measure_reference.py measures existing art
- Tools/Module3/make_unity_assets.py stable GUIDs + placeholder material
- Assets/Editor/Module3PlaceholderPrefabs.cs
AEDES > Module 3 > Build / Verify Placeholder Prefabs. Idempotent, so
prefab GUIDs and scene references survive a re-run. Verify checks tri
budget, measured size and socket survival.
Verified by importing into Unity 6000.3.8f1 and inspecting the built prefabs:
all sizes, identity root transforms, socket orientations and the fan's blade
pivot. Documentation/Module3-Placeholder-Assets.md records the numbers and the
Blender->Unity axis/unit traps involved, which a Blender round-trip cannot
catch.
Phase 3's view layer, now that the greybox props are in. - Module3Props is the single place the prefab paths and socket names live, so replacing a greybox mesh with real art stays a non-event. It sits in the Sim assembly because it is pure string constants and because an asmdef cannot reference Assembly-CSharp - the dependency only runs the other way - so the tests could not otherwise see it. - HouseholdView holds no state of its own: a house is whatever the model says it is when you walk in, which is what makes a return visit worth making (section 8). Screens swap torn to intact, the net swaps rolled-up to deployed at one transform, the fan appears and its Fan_Blade turns. - VillagerView shows the three visible states through pose, a skin material swap and props rather than three meshes. Section 5 constrains this class specifically: no icon, no outline, no colour coding, nothing overhead. A sick villager looks tired, the way a person does. If anything here reads as 'warning: infected', it is a bug. PlaceholderAssetTests is the part worth keeping: if a prefab path or socket name moves, nothing fails at compile time and nothing fails in play mode either - a prop just quietly stops appearing. These assert the paths resolve, the fan blade is a separate object with its own mesh, both net states and both screen states swap at one transform, every handheld prop carries Socket_Grip, and nothing is over budget. 77 tests passing.
The remaining art dependency, specified the way the prop brief was: self-contained, names real paths, and states the contract the code already binds to. Two things worth knowing that only turned up on inspection: - SK_Character is a Unity Humanoid rig (animationType 3, avatar configured), so clips can be retargeted from any humanoid source rather than hand-keyed. That changes the shape of the job considerably and the brief leads with it. - There is no Supercyan character pack in this branch, contrary to what CLAUDE.md and the original asset brief imply. SK_Character is the only human mesh, and the one existing human clip is Armature_walk. Corrected in the asset brief. The animator parameter names (Unwell, Resting, Talking) are fixed because VillagerView is already written and tested against them, so the brief states them as a contract rather than a suggestion. It also says not to touch NpcAnimationController, which Module 1's street NPCs use. The framing rule from design section 5 leads the document rather than being a footnote: a sick villager must never look frightening or contagious. The module teaches students to go to the people who are unwell, and animation is where that could most easily be undone. Age variety is raised as a question with a recommendation rather than a task, since there is only one human mesh and adding more is a costed decision.
Everything up to now was verified by tests and had never run as a game - there was no Module 3 scene at all. There is now, and it builds a playable neighbourhood. The scene is a generated artefact, not something anyone hand-dresses. CLAUDE.md 9 and the roadmap both name Unity YAML merges as the likeliest thing to hurt this project: Module 1's main scene is ~38,000 lines and two people editing it conflict irreconcilably. Module_3_MainScene is 682 lines and 28 objects - lighting, ground, Managers, Neighbourhood, Player - and the neighbourhood itself is laid out at runtime by M3NeighbourhoodBuilder. Regenerating via AEDES > Module 3 > Generate Main Scene is how you change it. The layout derives from the same parameters GAMA sets: lanes and households_per_lane decide both the model's adjacency and where the houses physically stand, so the plot you can see from the next plot stays the plot a mosquito can reach. They cannot drift. Module3GamaLink is deliberately NOT in the scene. Only one SimulationManager subclass may sit on a GameObject - Unity calls Awake on disabled components too and it assigns the static Instance, so two of them race (CLAUDE.md 10.4). Module 3 runs standalone; turning the GAMA link on is a deliberate one-component addition. The smoke test fails if a second one ever appears. Module3SceneSmokeTest opens the scene and builds a real neighbourhood, because the builder and views are MonoBehaviours in Assembly-CSharp and an asmdef cannot reference the predefined assemblies - so the Sim suite cannot reach them. It catches what matters: unassigned prefab references, missing managers, overlapping plots, a builder that silently produces nothing. Result: seed 39783304, 10 households, 38 residents, 252 mosquitoes, 2 visibly ill on day one, 4 nets for 10 households.
…xists Three factual corrections, all verified against the assets: - The brief said there is one human mesh and no character pack to draw on. Neither is true. SK_Character carries three body meshes on its one skeleton (fodo, nasa, Roger), and the Supercyan Character Pack Free Sample is in the project with 12 Humanoid animation FBXs that retarget onto the rig directly. That is a retarget source the brief was telling the animator they did not have, and it changes §7's premise: the gap is a child and an elder specifically, not bodies in general. - The rig FBX also carries idle/walk/t-pose takes with no clip ranges configured, so there is an existing idle to build the well-idle from. - Gave the lying pose the measured net and bed dimensions (1.56 x 1.77 m dome, 1.39 m tall, over a 1.60 x 1.94 m bed) instead of only a silhouette width. Also noted that age needs no animation of its own, since one clip set retargets to every body.
…arry
The brief listed four clips but never said how they divide the work with the
dialogue and the props, which leaves the two performances that matter most to
guesswork. Both are already decided by Observation.cs:
- A warning sign is signalled by an explicit line ("there is blood", "she
cannot keep water down") plus a prop in the room (cloth, bucket). So the body
must NOT escalate for it. If it does, students learn to scan for the most
dramatic person instead of learning the two signs, and a quiet person with a
bucket by the bed gets walked past.
- Defervescence is the opposite: the household says they are feeling better and
AddRoomSigns contributes nothing about the illness, so the pose is the only
honest signal the player gets. It has to read as better than the fever pose
but not well, which is the hardest ask in the brief and was not stated.
Added as a subsection of section 4 rather than renumbering.
…urnal Section 9 puts two of the three students outside the headset with jobs to do, and section 8 says the countdown 'should be visible to the Coach, not only to the Pilot'. So this is built for the cast, not the headset: a screen-space overlay with large type that a Coach and an Analyst can work from on a phone or a laptop. CoachBrief lives in the engine-free assembly because the ORDER is a teaching decision, not a layout one. Whatever is top of a list a fourteen-year-old reads aloud against a clock is what the squad does next, so it is tested: - Anyone needing a doctor is always first, and the line cap can never truncate them away. Section 4 calls the referral rule the heart of the module; third on a list it may as well not be there. - Someone whose fever has broken is flagged AHEAD of someone visibly unwell. The obviously ill patient will get attention anyway; the one who seems fine will not unless the Coach says so, and section 8 makes that the module's signature mistake. - The brief is capped at six lines so it stays sayable in the time it takes to pass a headset over. The cap yields to referrals and to nothing else. - Every urgent line names a house, because the Coach is holding the map. The debrief follows section 5's fixed order - the map, then the replay of what would have happened had the first patient been covered on day one, and only then the score. M3JournalWriter writes the Field Journal to persistentDataPath at session end, ending on section 5's question for the facilitator to read aloud. A failed write is logged and swallowed: the class is still in the room and the screen still has to show them their outbreak. 84 tests passing; the scene still regenerates and smoke-tests clean at 744 lines.
…rred one away Two things the view layer and the brief disagreed about, both found by reading Observation.cs against the brief. A fifth clip, A_Villager_Sit_Improving. The brief's section 4 says someone whose fever has broken must read as visibly better than someone with a fever, and then section 6 mapped both to A_Villager_Sit_Unwell. The animator parameters already separate the two states - Unwell=false, Resting=true is reachable only from defervescence - so this costs no code, only a clip the list was missing. It is the clip worth most care: the household line says they are feeling better and AddRoomSigns contributes nothing about the illness, so the pose is the only honest signal the player gets. VillagerView now hides the body of anyone at the health centre. Nothing was handling AtTheHealthCentre, so a referred villager went on standing in their house looking perfectly well - undoing the feedback for the one action the module most wants to reward, and leaving the household talking to someone who is not there. Hidden rather than deactivated, because deactivating the GameObject would stop OnTurnStarted arriving and strand them there for the rest of the session. Only skinned renderers are touched, so the props keep following the observation. Also recorded in the brief why Unwell/Resting deliberately cannot tell a warning sign apart from a fever: those two must look the same.
The atlas was switched to dynamic population but its texture kept m_IsReadable: 0, so TextMeshPro could not add any glyph beyond the 60 baked ones: every scene load logged 'Unable to add the requested character ... make the texture readable' (22 times in Module 3), and any Lao text needing another glyph rendered with gaps.
…ting At 9 m the Lao houses (8.55 m wide, 9.6 m roof) stood 0.45 m apart with their roofs overlapping by 0.57 m - no way through between them and z-fighting where the roofs cross (measured in play). Spacing is visual only; the model's adjacency comes from lanes / households_per_lane.
The desktop camera never moved from the generated spawn point, 1 m from house 0's front wall and facing it, so a session played from the keyboard showed a blank wall throughout and never the house being acted on. The driver now stands the camera in the lane in front of the selected house (Tab cycles), including at session start.
The builder placed each plot's productive containers at local z = -1.8, inside the 6.4 m-deep Lao house (checked: every jar sat within the house footprint). They are the visible reason mosquitoes are around (section 5), so they now stand in front of the house on the lane side (yardDepth 4.6, the footprint ends at 3.4).
M3Hud and M3JournalWriter subscribed in OnEnable only if M3Session.Instance already existed - but the session sets Instance in its own Awake, and Awake/OnEnable order across GameObjects is undefined. In the generated scene they lost the race: OnHandover, OnSessionFinished and OnActionResolved had no listeners (checked in play), so the handover screen, the debrief and action feedback never appeared and the Field Journal was never written. Subscribe again in Start, which runs after every Awake, guarding against doubles.
…u have missed CoachBrief only falls back to m3.coach.nothingUrgent when it has no lines, and an unvisited house always produces a line - so 'Go and see the houses you have missed' was shown exactly when no house had been missed (seen after a turn that visited all ten). Point at return visits instead, which section 8 is about. English only; the Lao column is still to be translated.
The next turn's clock started the moment the previous one ended, under the 12 s handover screen - measured: 179.6 -> 173.6 s while the panel was still showing. Every incoming Pilot lost the handover, about 3.6 minutes over an 18-turn session. M3Session.HoldTurnClock(seconds), called by M3Hud when it shows the handover.
…/18' After the final handover Session.TurnIndex is one past the end, so under the debrief the bar read 'Turn 19/18 Round 7 Player 1' with a frozen 2:58 clock (harness, full 18-turn session). When the session is not running, show 'Session over' with the day and nets instead.
… warning sign
Referring someone without a warning sign is 'gently corrected, never
punished' (section 10) - they stay home. But the code still set
Person.Referred, and that flag then:
- greyed out 'get them to the health centre' ('already going'),
- kept their warning sign out of the handover brief,
- stopped the unreferred-warning consequence from ever firing, and
- scored them as a correct referral once the sign appeared.
Found in a playtest by referring a well villager. Referred is now set only
when the person actually goes; the unneeded case sets ReferredWithoutNeed,
which Scoring counts as UnnecessaryReferrals. A well person is told
'they are well' rather than being described as having a fever.
New test AnEarlyReferralDoesNotSilenceALaterWarningSign fails on the old
code and passes now; the whole suite (85) passes.
screenOffset (1.6, 1.4, 0.1) is the middle of the Lao house, so every house's window screen (1.5 x 1.0 m, pivot at its bottom edge) hung at head height in the room as a black panel with no window behind it - seen walking into a house in a first-person capture. It now sits in the back-wall window (SM_WindowLeft (1), centred at 1.10, 1.04, -2.87 in house space). Scene's serialized value updated to match.
Seen in a first-person capture: every villager ran on the spot inside the house, in a single-file row facing a wall, all the same size. - The villager prefab is Module 1's street NPC, whose Human component walks waypoints and sets the animator's isWalk on Start. The builder disables it and clears isWalk, so they play the character's own idle clip. - They stand in a loose fan about 2.6 m from the side door the volunteer comes in by, turned towards it, instead of a row across the room. - Children are 65% size and elders 94%, with a little per-person variation, so a household reads as a family until villager art exists (the model has one outfit; its extra meshes do not render on their own).
From the lanes the horizon was an empty white-blue band over a grey plane. M3Surroundings (on the Neighbourhood object, called by the builder once the plots stand) adds, at runtime and from a seed: - the FS002_Day skybox Module 1 uses, and light linear haze, - grass to the horizon (the scene's grey ground recoloured to match) and a dirt track down the lane, - rice paddies with dykes either side of the village, some flooded, - palms and banana plants round the houses and a broken tree line further out, from the Team Assets nature prefabs (colliders stripped), - a single low-poly ring of hills, facing inwards, with seamless noise. Scenery only - nothing interactive, nothing the model reads. The camera's far clip is raised to 520 m so the hills draw. The scene generator wires the same references, so a regenerated scene keeps it. Checked from eye height in the lane and behind the houses.
…r at most - PF_Bed pointed at a mesh GUID that has never matched any asset (since it was first committed), so beds never rendered. It now nests SM_Bed.fbx; the root GameObject id is unchanged so the scene's bedPrefab reference still resolves. - NpcAnimationController lacked the Resting/Unwell/Talking parameters that VillagerView sets, so those calls did nothing and a resting villager was teleported to the bed still standing. Add the parameters. With no lying clip for this rig yet, VillagerView tips the villager onto their back, centred on the mattress (rig origin is at the feet) at mattress height. - The fan is a correct 0.3 m table fan but read as a toy on the floor next to a full-size door; scale it 2.5x uniformly when placed (a non-uniform scale would skew the spinning blade). - The model gives every house 2-5 productive containers and the builder drew one jar each, lining every yard. Draw at most one, in about half the houses, by a stable per-house roll. Presentation only; sim container counts unchanged.
Until now Module 3 was a plain camera and a keyboard driver. It now plays the
way it ships: an XR rig, controller locomotion, pointing at people, panels in
the room, and a headset-safe HUD. Driven and checked in the editor purely
through simulated Quest controllers - no scripted camera.
The rig (M3XRPlayer)
- Spawns the XRI starter rig at the head of the lane. Left stick walks (1.4
m/s - the sample's 2.5 is a jog), right stick snap-turns or teleports.
The plain camera + M3DebugDriver stay as the desktop fallback
(AEDES > Module 3 > Use Desktop Camera).
- Each turn's clock is held until the incoming Pilot presses A ("ready"), so
handing over the headset does not eat their time. Left menu button asks
"End your turn now? A = yes, B = no".
- Body capsule capped at 1.6 m and step offset 0.3 (M3CappedBodyManipulator).
Talking to people
- Every villager is a target (M3VillagerTarget): point + trigger, or grip.
The press is detected as an edge of "held" - XRI's own
ReadWasPerformedThisFrame missed it depending on update order.
- M3PersonPanel: a panel at arm's reach, angled off so the person stays
visible - who they are, what you can see, what they tell you, then the
closed verb list (never 'diagnose' or 'treat') for them and for the house,
with the reason under any disabled action. All from Observe, so it can
show nothing the model says you could not see. Pressable with the ray.
- Walking into a house counts as looking in (M3VisitTracker).
HUD
- M3Hud was a screen-space overlay, which a headset does not draw. In VR it
becomes a world-space canvas that follows the head at 1.7 m (casting mirrors
the headset, so the Coach still reads the countdown). Handover, start card
and end-turn question are in it. Status text is localized.
Found by actually walking through the village
- The Lao houses were modelled with the doors CLOSED (solid slabs with
colliders in real openings): nobody could go in. Doors are swung open.
- The doorway was 1.53 m clear - lower than the 1.7 m villagers who live in
it and than a standing adult's head. A first fix capped the body at 1.3 m,
which only let the head push through the wall above the door. Houses are
now scaled 1.4x (doorway 2.1 m); everything placed in house space, plot
spacing, lane width and the yard scale with it.
- CharacterController sweeps up by its step offset before stepping: at the
sample's 0.5 m it hit the lintel and refused to enter at all.
- Villagers and beds stood on the ground under the raised floor.
Checked in play with head heights 1.25 / 1.6 / 1.85 m: all walk in, a visit
is recorded, the ray hovers the patient, trigger opens the panel, a ray press
puts up a net (nets 4 -> 3), menu/B/A end the turn, the handover holds the
clock, and the next Pilot's A starts it. Sim suite: 86/86.
Three things that only showed up by driving the rig with the controller inputs a Pilot really uses: - Pressing A (ready / yes) made the rig JUMP 0.5 m: the XRI starter rig binds A to a 1.25 m JumpProvider. Jump, grab-move (grip - which is how you reach for a villager, and it would drag the Pilot) and climbing are switched off. - Right-stick teleport did nothing. The teleport ray only sees areas on XRI's 'Teleport' interaction layer (31); the ground's TeleportationArea was on the default layer. Fixed, and each house gets a thin floor-only teleport surface (its own floor mesh includes the walls, which would put a Pilot in a wall). Checked: lands on the lane and on a house floor (y 0.25). - Requests Floor tracking. The rig leaves it unspecified, which falls back to a fixed 1.36 m camera offset instead of the wearer's real standing height - and door clearances, panel height and hand positions all assume true height.
The debrief's closing question ('Why did the outbreak stop, or why did it
not?') landed below the dark panel, over the scene, whenever the text ran
long (seen in the headset view after a full session). Handover and debrief
lengths vary - and Lao runs longer than English - so both now auto-size
(down to 55%) instead of spilling.
…lack Two faults made the rolled-up net read as a black column standing on the chest of whoever was resting in the bed. Black: M_Module3Placeholder sat on SHD_SimpleLit_Static, a toon graph that takes its colour from a gradient texture (_Texture_01) and ignores _BaseColor, so a flat-colour material on it draws solid black. All nine placeholder props used it (net, torn/intact window screens, fan, cloth, torch, bottle, vessel, health centre). It is now URP/Lit like the rest of Team Assets; make_unity_assets.py is changed to match so regenerating does not undo it. The GUID is unchanged, so no prefab reference moves. Placement: the bundle is modelled a metre above its origin and was placed at the bed's centre, right over the sleeper. PF_Bed now carries a Socket_NetHook on the head post farthest from the door (bed-local -0.66, 1.3, -0.8) and HouseholdView slides a rolled net so its own Socket_Hook meets it. Going by sockets rather than a measured offset means replacement art can reshape the bundle freely. The deployed net is unchanged, still centred on the bed. Tests: the rolled net stays clear of the sleeper's width, and no Module 3 prop may sit on the toon graph without a gradient texture. Both were checked to fail with their fix removed.
T_CoconuTree.mat pointed _BaseMap/_MainTex at texture guid ba9f6207..., which no asset in the project owns, so the leaves drew with Unity's missing-texture white and read as silver under the sky. Point it at CoconutLeaft.png (e3a47a5b...). The bad reference dates from the first palm commit, so Module 1's palms had it too.
Replaces the greybox meshes from the unblocking pass with detailed models, generated headlessly in Blender (Tools/Module3/make_final_models.py): a lathed fan with a crossed-ring guard and separate spinning blade, a two-bay window screen and a torn variant with peeled flaps, a gathered and tied rolled net, a trigger-spray bottle, an enamel cup with a handle, a creased cloth, a ribbed-grip torch and a health centre with a hipped roof and porch. The contract is unchanged, so nothing that binds to these moves: same paths, node names (Fan_Base / Fan_Head / Fan_Blade), Socket_* empties at the same points, the same overall envelope (Verify checks size to 2 cm), the same triangle budgets, and the existing .fbx.meta files, so GUIDs and every reference survive. Output is FBX, not glTF: the asset brief accepts glTF for placeholders only. Each prop gets its own flat-colour material (make_final_materials.py) on URP/Lit, the same fix M_Module3Placeholder received: the project's SHD_SimpleLit_Static graph draws its colour from a gradient texture and ignores _BaseColor, so a flat colour on it renders black in the game. Module3PlaceholderPrefabs assigns Entry.Material per asset instead of forcing the one shared grey material. Verified in Unity: Verify Placeholder Contract reports no problems.
…dult Answers Animation Brief section 7 with option 2: new meshes on their own Humanoid skeletons instead of scaling an adult. Three new characters, built headlessly in Blender (Tools/Module3/make_villagers.py): an elder (1.58 m) and two children (1.10 m) that share one child skeleton, 1.4-1.8k triangles each against the existing bodies' 0.6-0.7k. They follow SK_Character's super-deformed proportions, measured from its bones, and face the same way (Blender -Y, so Unity +Z), because villagers are aimed with LookRotation. Lao village context is carried by clothing, hair and colour only: a sinh-style skirt with hem band, shoulder sash and cuffs for the elder, tee and shorts or tunic and skirt for the children, bare feet (every scene is indoors), one plain skin tone, and no facial features beyond the same two eye bars the existing characters have. Module3VillagerPrefabs configures each FBX as Humanoid through ModelImporter.humanDescription, builds the prefabs, and Verify measures the baked mesh (height, which way the feet point) and plays the shared walk clip to prove it retargets. The prefabs reuse NpcAnimationController, so they stand in the same idle as the existing villagers, come with wellSkin/unwellSkin wired (those fields are empty on the old prefab), and carry a body-sized collider. M3NeighbourhoodBuilder picks a body by the person's age band: children from childPrefabs, elders from elderPrefabs, indexed by person id so it is stable across rebuilds. Purpose-built bodies are not age-scaled any more; a scene without the new arrays falls back to the old scaled adult. Module3SceneBuilder assigns the arrays, and the scene gains just those entries. PF_CharacterV1 carries three complete bodies (fodo, nasa, Roger) as active siblings, so every adult drew all three stacked in one spot. ShowOneBody keeps one per person. VillagerView gains bodyHeight so the lying-down offsets suit a child or an elder rather than assuming the 1.7 m adult. Materials are URP/Lit, matching the placeholder fix: the project's toon graph ignores _BaseColor and draws a flat colour black, which is how the first version of these rendered until the scene was played and shot. Documented in Documentation/Module3-Villager-Bodies.md, with the decision recorded in the Animation Brief. Checked by playing the scene: colours, scale, facing, the idle pose, and a child and an elder laid on beds. Not seen on a headset.
The plain warm brown (0.78, 0.58, 0.43) rendered khaki-olive in the game: the scene is lit by a cyan sky ambient plus sun, and URP/Lit multiplies the albedo by it, which pulls a brown towards green-grey (the existing adults read the same way). The skin albedo is now warmer and more saturated (0.90, 0.52, 0.30), which renders as a warm tan-brown; the unwell skin is the paler, greyer counterpart of the same tone. Found and checked by playing the scene; a matte variant (no specular, no environment reflection) made no visible difference, so it is the lighting, not the sheen. The preview colour in make_villagers.py is unchanged and now says it is only a preview; the shipped tone lives in make_villager_materials.py.
The ground was three flat-colour quads: a grey-green void with a brown stripe. It is now painted at build time from the scene seed, with no texture asset and no change to the scene file. - M3GroundPainter (Burst job): one non-repeating image over the village and its lane, with mottled grass (lush and dry patches, tufts, pale blades) and the track painted in - ragged edge, wheel ruts that come and go, pebbles, a tussocky grass strip down the middle, petering out into grass at the ends. Plus a small seamless tile for the fields out to the horizon. - M3Surroundings.BuildGround uses them; the scene's grey Ground plane keeps its collider (walking, teleport) but its renderer is off so it does not z-fight the painted quad. groundTexel / groundContrast are tunable. - Smoke test fails if the village ground ends up untextured.
The painted ground now wears bare where people actually walk. For every plot, in plot space so both rows of houses share one layout: - a path from the lane down the gap between houses to the side door in the +x wall (the one the Pilot comes in by), and a bare apron in front of it; - a swept yard against the front wall that breaks into patches further out, and stops short of the track so a band of grass keeps the road distinct. Grass round the worn ground is trodden thin and dry, as it already was beside the track. M3Surroundings.Build takes the plot list (one-line change at the M3NeighbourhoodBuilder call site); house measurements are in model units scaled by the builder's houseScale.
The house was flat colour: every face samples one swatch of a palette texture, so it read as smooth plastic. Its prefab and materials are shared with Modules 1 and 2, and its UVs only pick a colour, so an ordinary detail map has nothing to grip. Instead: - Shaders/M3HouseLit.shader is URP 17.3's Lit with one change: a world-space grain multiplied into the albedo (fine grain and blotches, plus streaks that run down walls and down a roof slope), projected along the axis the surface faces. Palette colours, lighting, shadows, fog and lightmaps are unchanged. - Resources/Module3 holds a seamless 256px detail texture and two materials that are the house's M_Gradient_Light / M_Gradient on that shader. Module3HouseSurfaceAssets regenerates them (AEDES > Module 3 menu). - M3HouseSurface swaps them onto the houses M3NeighbourhoodBuilder builds, so Modules 1 and 2 are untouched. - The smoke test fails if the assets go missing.
… walkway The fan of villagers stood across the middle of the room, right in the path from the door to the bed, so a Pilot walking in bumped into them (found driving the rig with the thumbstick). They now stand as a loose group in the south-west corner, still turned to face the door, leaving the diagonal from the door to the bed free.
The villager prefab is Module 1's street NPC, whose CapsuleCollider is 0.5 m in radius. Villagers stand about a metre apart, so the colliders overlapped and pointing at one person could open a neighbour's conversation: in a controller-only run the second conversation of a visit kept reopening the first person's panel (aiming at an elder opened the villager next to them). Capsule radii are capped at 0.28 m.
… are visible on the bed PF_CharacterV1 carries three adult bodies (fodo, nasa, Roger) but only Roger has bones assigned; fodo and nasa have every bone reference empty, and Unity draws a skinned mesh with no bones as nothing at all. ShowOneBody picks one body per adult by id, so two thirds of the adults were invisible - found when a patient in bed showed an empty mattress although the game said they were lying there (mesh 'visible', bounds correct, no error). The three meshes share one 31-bone skeleton (bind poses identical), so the unrigged bodies now take the rigged body's bones at build time. Done in the builder, not the prefab, because Module 1's street NPCs stack all three bodies from the same prefab and rigging them there would show three overlapping people.
… moves the person
The panel closed itself when the villager's body was more than 5.5 m from the Pilot. Sending
someone to the health centre puts their body back at its spot in the far corner of the room -
over 5.5 m from where the Pilot stands beside the bed - so the panel shut the moment the
referral went through, and the reply ('Right call. They are on their way to the health centre
now.') was only ever seen as a small line in the HUD. Found filming the referral: the panel was
gone within a second of the press. The distance is now measured from where the conversation began.
Already done prefab
This reverts commit 2f0a4ab.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.