Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5858838
Fix merge conflict markers committed into two prefab .meta files
RomeXO10 Sep 28, 2026
bf19207
Module 2: in-headset HUD, localized strings and interaction fixes
RomeXO10 Sep 28, 2026
8859733
Add one-command build and deploy scripts for the AEDES modules
RomeXO10 Sep 28, 2026
af2ba1f
Module 2: tip containers out by tilt, not a ground-layer raycast
RomeXO10 Sep 29, 2026
b085c35
Module 2: stop the swarm ending a round in seconds; freeze scoring at…
RomeXO10 Sep 29, 2026
866bce7
Module 2: bite grace period 2.5 s -> 5 s
RomeXO10 Sep 29, 2026
094872e
Module 2: implement repellent cream - squeeze onto a hand for 30 s wi…
RomeXO10 Sep 29, 2026
d5f3897
Module 2 HUD: stop toasts covering the objective label; un-overlap en…
RomeXO10 Sep 29, 2026
92708f3
Module 2 HUD: give the end card's fact block room (panel 560 -> 640)
RomeXO10 Sep 29, 2026
e224cc6
Module 2 HUD: auto-size the end card's fact text to its box
RomeXO10 Sep 29, 2026
4f2f8c4
Module 2: make the lawn litter grabbable so it can be binned
RomeXO10 Sep 29, 2026
ea898c1
Module 2: stop newly-physical litter launching into the air at round …
RomeXO10 Sep 29, 2026
4554c90
Module 2: a released jar lid falls instead of hanging in mid-air
RomeXO10 Sep 29, 2026
6bb2f78
Module 2: drop UIHand's no-op DontDestroyOnLoad
RomeXO10 Sep 29, 2026
f48f7e0
Module 2: put the tip-out splash on the nearest surface below
RomeXO10 Sep 29, 2026
0c129f9
Module 2: every vase splashes when tipped out
RomeXO10 Sep 29, 2026
0e30868
Module 2: actually load the string table; keep the input readout out …
RomeXO10 Sep 30, 2026
fc25691
Module 2: a put-down swatter falls and can be replaced
RomeXO10 Sep 30, 2026
fd66d66
Module 2: clearing breeding sites now thins the swarm and the bites
RomeXO10 Sep 30, 2026
bc6e4a0
Module 2: sites a round does not use stay in the house, already dealt…
RomeXO10 Sep 30, 2026
b2b0a40
Module 2: open breeding sites show wrigglers and a rising column of m…
RomeXO10 Sep 30, 2026
6fc6407
Module 2: stop the right stick flipping the player 180 degrees
RomeXO10 Oct 2, 2026
198a34c
Module 2: the wrist menu stays open while you reach for a button
RomeXO10 Oct 2, 2026
16f46e6
Module 2: wrist-menu tools appear in front of the player, not the rig…
RomeXO10 Oct 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using NUnit.Framework.Interfaces;
using UnityEngine;

public class StereoSoundListener : MonoBehaviour
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ MonoBehaviour:
m_TransformationPriority: 0
m_TurnSpeed: 60
m_EnableTurnLeftRight: 1
m_EnableTurnAround: 1
m_EnableTurnAround: 0
m_LeftHandTurnInput:
m_InputSourceMode: 0
m_InputAction:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27965,10 +27965,8 @@ MonoBehaviour:
gameOver: {fileID: 401769903}
aButton: {fileID: 5561125842531183599, guid: c348712bda248c246b8c49b3db54643f, type: 3}
score: 0
timer: 120
timer: 180
isOver: 0
saveWaterContainer: []
notSaveWaterContainer: []
sun: {fileID: 185372124}
skybox: {fileID: 2100000, guid: ec46d2188d4b8774e847e75f1e12a8db, type: 2}
lightColor:
Expand Down
648 changes: 598 additions & 50 deletions Assets/1.TeamWorkspace/Module2/Scripts/GameManager/M2Manager.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.EventSystems;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
using UnityEngine.XR.Interaction.Toolkit.Interactors;
using UnityEngine.XR.Interaction.Toolkit.UI;

/// <summary>
/// Scene-wide interaction repairs for Module 2, applied at runtime so none of it needs a prefab or
/// scene edit. M2Manager adds this component to itself, the same way it adds Module2HUD.
///
/// Each fix here is for a defect found during on-headset testing:
/// - the EventSystem carried a plain Input System module, so XR pokes highlighted the wrist-menu
/// buttons but never clicked them;
/// - the right hand's interactor sphere was not a trigger, making it a solid moving collider that
/// physically shoved objects away as you reached for them;
/// - no grabbable had an attachTransform, so objects snapped to your palm by their origin;
/// - grabbables used Discrete collision detection and no interpolation, so thin objects sank into
/// the floor and jittered once released;
/// - colliders were much smaller than the objects they belong to (the vase's flowers had none at
/// all), so only a small part of each object responded to touch;
/// - the litter on the lawn (tag Trash) had no Rigidbody or XRGrabInteractable, so it could not be
/// picked up and TrashBin could never fire (found by playtesting through the harness).
/// </summary>
public class Module2Interaction : MonoBehaviour
{
[Tooltip("Grow each grabbable's box collider to cover its renderers. Turn off to keep authored colliders exactly as they are.")]
public bool fitCollidersToRenderers = true;

[Tooltip("Make every direct interactor's collider a trigger so hands stop physically shoving objects.")]
public bool forceInteractorTriggers = true;

[Tooltip("Give grabbables an attach point at the centre of their collider instead of their origin.")]
public bool addAttachTransforms = true;

[Tooltip("Radius of each hand's grab sphere. The authored 0.1 m demands precision that children with no VR experience do not have; 0 leaves it alone. A proper far/ray interactor is the real fix and needs a prefab change.")]
public float interactorRadius = 0.14f;

[Tooltip("Make litter (tag Trash) grabbable so it can be carried to the bin.")]
public bool makeTrashGrabbable = true;

readonly List<XRGrabInteractable> _hooked = new List<XRGrabInteractable>();

void OnDestroy()
{
foreach (var grab in _hooked)
if (grab != null) grab.selectEntered.RemoveListener(OnAnyGrab);
_hooked.Clear();
}

void OnAnyGrab(SelectEnterEventArgs args)
{
if (M2Manager.Instance != null) M2Manager.Instance.NoteFirstGrab();
}

void Start()
{
FixEventSystem();
if (forceInteractorTriggers) FixDirectInteractors();
// Before TuneGrabbables, so the litter gets the same collider/attach/first-grab treatment.
if (makeTrashGrabbable) MakeTrashGrabbable();
TuneGrabbables();
}

// XRI turns a poke or ray into a UI click through XRUIInputModule. With any other input module
// the TrackedDeviceGraphicRaycaster still drives the button's highlight, which is why the wrist
// menu lit up green but never fired its onClick.
void FixEventSystem()
{
var eventSystem = EventSystem.current != null ? EventSystem.current : FindFirstObjectByType<EventSystem>();
if (eventSystem == null)
{
Debug.LogWarning("[Module2Interaction] No EventSystem in the scene - the wrist menu cannot be clicked.");
return;
}

if (eventSystem.GetComponent<XRUIInputModule>() != null) return;

foreach (var module in eventSystem.GetComponents<BaseInputModule>())
module.enabled = false;

eventSystem.gameObject.AddComponent<XRUIInputModule>();
Debug.Log("[Module2Interaction] Added XRUIInputModule to the EventSystem so XR pokes can click UI.");
}

void FixDirectInteractors()
{
int fixedCount = 0;
foreach (var interactor in FindObjectsByType<XRDirectInteractor>(FindObjectsSortMode.None))
{
foreach (var collider in interactor.GetComponents<Collider>())
{
if (!collider.isTrigger)
{
// Detection itself uses OverlapSphere, so this does not change what the hand can
// reach - it stops the hand being a solid object that bats things out of the way.
collider.isTrigger = true;
fixedCount++;
}

// XRDirectInteractor sizes its overlap query from this sphere's radius, so widening it
// widens the grab window.
if (interactorRadius > 0f && collider is SphereCollider sphere)
sphere.radius = interactorRadius;
}
}
if (fixedCount > 0) Debug.Log($"[Module2Interaction] Made {fixedCount} interactor collider(s) triggers.");
}

void MakeTrashGrabbable()
{
int count = 0;
foreach (var go in GameObject.FindGameObjectsWithTag("Trash"))
{
if (go.GetComponent<XRGrabInteractable>() != null) continue;
var body = go.GetComponent<Rigidbody>();
if (body == null) body = go.AddComponent<Rigidbody>();
body.mass = 0.2f;
// Litter was placed sunk into the ground (it never had a body, so it did not matter).
// Uncapped, the solver fired a banana leaf out at 8 m/s and it flew 3 m up at round
// start; capped, embedded pieces ease up onto the surface.
body.maxDepenetrationVelocity = 0.3f;
go.AddComponent<XRGrabInteractable>();
count++;
}
if (count > 0) Debug.Log($"[Module2Interaction] Made {count} piece(s) of litter grabbable.");
}

void TuneGrabbables()
{
foreach (var grab in FindObjectsByType<XRGrabInteractable>(FindObjectsSortMode.None))
{
var body = grab.GetComponent<Rigidbody>();
if (body != null)
{
// ContinuousSpeculative is valid for both kinematic and dynamic bodies, unlike
// ContinuousDynamic, and stops thin props tunnelling into the floor.
body.collisionDetectionMode = CollisionDetectionMode.ContinuousSpeculative;
body.interpolation = RigidbodyInterpolation.Interpolate;
}

if (fitCollidersToRenderers) FitColliderToRenderers(grab);
if (addAttachTransforms && grab.attachTransform == null) AddAttachTransform(grab);

// The HUD drops its beginner guidance as soon as the child picks anything up.
grab.selectEntered.AddListener(OnAnyGrab);
_hooked.Add(grab);
}
}

// The vase is the clearest case: one 10 x 29 x 10 cm box around the stem, and no collider at all
// on the flowers, so most of what you can see could not be touched.
void FitColliderToRenderers(XRGrabInteractable grab)
{
var box = grab.GetComponent<BoxCollider>();
if (box == null) return;

var renderers = grab.GetComponentsInChildren<Renderer>();
if (renderers.Length == 0) return;

Bounds worldBounds = renderers[0].bounds;
for (int i = 1; i < renderers.Length; i++)
worldBounds.Encapsulate(renderers[i].bounds);

Transform t = box.transform;
Vector3 scale = t.lossyScale;
if (Mathf.Approximately(scale.x, 0f) || Mathf.Approximately(scale.y, 0f) || Mathf.Approximately(scale.z, 0f)) return;

// Renderer bounds are world axis-aligned, so this is an approximation for a rotated prop.
// Good enough for the upright props in this scene, and it only ever grows the collider.
Vector3 localSize = new Vector3(
worldBounds.size.x / Mathf.Abs(scale.x),
worldBounds.size.y / Mathf.Abs(scale.y),
worldBounds.size.z / Mathf.Abs(scale.z));

if (localSize.sqrMagnitude <= box.size.sqrMagnitude) return;

box.center = t.InverseTransformPoint(worldBounds.center);
box.size = localSize;
}

void AddAttachTransform(XRGrabInteractable grab)
{
var collider = grab.GetComponentInChildren<Collider>();
if (collider == null) return;

var attach = new GameObject("[runtime] Attach");
attach.transform.SetParent(grab.transform, false);
attach.transform.position = collider.bounds.center;
attach.transform.rotation = grab.transform.rotation;
grab.attachTransform = attach.transform;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

100 changes: 89 additions & 11 deletions Assets/1.TeamWorkspace/Module2/Scripts/GameManager/MosquitoSpawn.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using NUnit.Framework;
using System.Collections.Generic;
using UnityEngine;

public class MosquitoSpawn : MonoBehaviour
Expand All @@ -9,28 +9,106 @@ public class MosquitoSpawn : MonoBehaviour
[Header("Game Setting")]
public int maxMosquitoInMap;
public int currentMosquitoInMap;
[Tooltip("Mosquitoes sustained per breeding site that is still open.")]
public int mosquitoesPerSite = 5;
[Tooltip("Seconds between spawn checks. The old code ran a tag search every frame.")]
public float spawnInterval = 0.5f;
[Tooltip("Mosquitoes added per check, so the house fills in a few seconds rather than a few frames.")]
public int spawnsPerCheck = 3;
[Tooltip("Mosquitoes sent away per check when the swarm is bigger than the open sites support, so clearing a container thins it over a couple of seconds instead of all at once.")]
public int retiresPerCheck = 2;
[Tooltip("Height above a container that its mosquitoes appear at.")]
public float emergeHeight = 0.3f;
public GameObject[] spawnPoint;

private float _nextSpawnTime;
private readonly List<Mosquito> _live = new List<Mosquito>();

private void Update()
{
if (Time.time < _nextSpawnTime) return;
_nextSpawnTime = Time.time + Mathf.Max(spawnInterval, 0.05f);
SpawnMosqitoInMap();
}

// Mosquitoes now emerge from the containers that still hold water, so covering a jar visibly
// thins the swarm around it. They used to appear at nine fixed points unrelated to the water,
// which meant the player's actions had no legible effect on where mosquitoes came from.
private bool TryGetSpawnPosition(M2Manager manager, out Vector3 position)
{
position = Vector3.zero;

if (manager.OpenSiteCount > 0)
{
int index = Random.Range(0, manager.OpenSiteCount);
int i = 0;
foreach (var site in manager.OpenSites)
{
if (i++ != index) continue;
if (site == null) break;
position = site.transform.position + Vector3.up * emergeHeight;
return true;
}
}

// Nothing left holding water: fall back to the authored points so the scene is never empty.
if (spawnPoint != null && spawnPoint.Length > 0)
{
var point = spawnPoint[Random.Range(0, spawnPoint.Length)];
if (point != null) { position = point.transform.position; return true; }
}

return false;
}

public void SpawnMosqitoInMap()
{
//print("isWorking");
if (M2Manager.Instance.isOver == 1)
if (mosquitoPrefab == null) return;

M2Manager manager = M2Manager.Instance;
if (manager == null || !manager.IsPlaying) return;

// Ones already flying off do not count towards the swarm.
_live.Clear();
foreach (var mosquito in FindObjectsByType<Mosquito>(FindObjectsSortMode.None))
if (!mosquito.Retired) _live.Add(mosquito);
currentMosquitoInMap = _live.Count;

// The swarm is sustained by the breeding sites that are still open, so dealing with a
// container visibly thins it out. This is what the old unused
// "Random.Range(5, notSaveWaterContainer.Length)" line was reaching for; that array was
// never populated, so it computed Random.Range(5, 0) and threw the result away.
int target = manager.SiteCount > 0
? Mathf.Min(maxMosquitoInMap, manager.OpenSiteCount * Mathf.Max(mosquitoesPerSite, 1))
: maxMosquitoInMap;

int room = target - currentMosquitoInMap;
if (room < 0) { RetireExcess(-room); return; }
if (room == 0) return;

int toSpawn = Mathf.Min(room, Mathf.Max(spawnsPerCheck, 1));
for (int i = 0; i < toSpawn; i++)
{
if (GameObject.FindGameObjectsWithTag("Mosquito").Length < maxMosquitoInMap)
{
int randomIndex = Random.Range(0, spawnPoint.Length);
int randomMosquitoCount = Random.Range(5, M2Manager.Instance.notSaveWaterContainer.Length);
Instantiate(mosquitoPrefab, spawnPoint[randomIndex].transform.position, Quaternion.identity);
currentMosquitoInMap = GameObject.FindGameObjectsWithTag("Mosquito").Length;
//print("isWork");
}
if (!TryGetSpawnPosition(manager, out Vector3 position)) continue;

Instantiate(mosquitoPrefab, position, Quaternion.identity);
currentMosquitoInMap++;
}
}

// The ones nearest the player go first: they are the ones biting, so dealing with a container eases
// the buzzing round your head, and they fly off rather than popping out of existence.
private void RetireExcess(int excess)
{
var camera = Camera.main;
if (camera != null)
{
Vector3 head = camera.transform.position;
_live.Sort((a, b) =>
(a.transform.position - head).sqrMagnitude.CompareTo((b.transform.position - head).sqrMagnitude));
}

int count = Mathf.Min(excess, Mathf.Max(retiresPerCheck, 1));
for (int i = 0; i < count && i < _live.Count; i++) _live[i].Retire();
}
}
Loading
Loading