Files
AIC-Project/docs/superpowers/plans/2026-07-30-virtual-hud-input.md

335 lines
13 KiB
Markdown

# Virtual HUD Input Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build reusable Unity HUD input components for a virtual joystick and a MOBA-style action button pad that emit pure input events for lobby and minigame consumers.
**Architecture:** Add focused runtime components under `Client/Assets/Script/xmain/Input/`: `VirtualJoystick`, `VirtualActionButton`, and `VirtualActionPad`. Add JSON prefab descriptions through the existing `Doc/UIPrefabCreater` pipeline, then migrate `LobbyWorldController` to consume `VirtualJoystick` instead of owning joystick sampling.
**Tech Stack:** Unity UGUI, EventSystem pointer interfaces, TextMeshPro, NUnit EditMode tests, existing JSON-to-prefab builder.
## Global Constraints
- Components emit pure input events and must not send network messages.
- First version uses current UGUI/EventSystem/StandaloneInputModule, not Unity's new Input System.
- First version reuses existing Common UI resources and does not require new MOBA circle button art.
- Existing minigame framework contracts must not change.
- Do not revert or overwrite unrelated working tree changes.
---
## File Structure
- Create `Client/Assets/Script/xmain/Input/VirtualJoystick.cs`: joystick sampling, pointer capture, dead zone, knob motion, and public direction events.
- Create `Client/Assets/Script/xmain/Input/VirtualActionButton.cs`: action button IDs, event payload, pointer state machine, cooldown/charge/enabled visual state.
- Create `Client/Assets/Script/xmain/Input/VirtualActionPad.cs`: five-button registry, event forwarding, and pad-level state API.
- Create `Client/Assets/Script/Tests/EditMode/VirtualJoystickTests.cs`: component math and state tests for joystick behavior.
- Create `Client/Assets/Script/Tests/EditMode/VirtualActionButtonTests.cs`: button state machine tests and visual state tests.
- Create `Client/Assets/Script/Tests/EditMode/VirtualActionPadTests.cs`: button discovery and event forwarding tests.
- Create `Doc/UIPrefabCreater/UI_VirtualJoystick.json`: reusable joystick prefab definition.
- Create `Doc/UIPrefabCreater/UI_VirtualActionPad.json`: reusable MOBA-style action pad prefab definition.
- Modify `Client/Assets/Script/xmain/Client/LobbyWorldController.cs`: load `UI_VirtualJoystick`, subscribe to events, remove duplicated joystick sampling methods, and use the component for camera exclusion.
---
### Task 1: VirtualJoystick Runtime Component
**Files:**
- Create: `Client/Assets/Script/xmain/Input/VirtualJoystick.cs`
- Test: `Client/Assets/Script/Tests/EditMode/VirtualJoystickTests.cs`
**Interfaces:**
- Produces: `XGame.Input.VirtualJoystick.Value`, `IsActive`, `Started`, `Changed`, `Ended`, `IsScreenPositionInAcquireArea(Vector2)`, `SetPointerForTest(Vector2?)`
- Consumes: Unity `RectTransform`, optional child `Knob`, UGUI canvas camera conversion.
- [ ] **Step 1: Write the failing tests**
Add tests that create a `VirtualJoystick`, assign a root and knob `RectTransform`, call `SetPointerForTest`, and assert dead-zone, clamping, release, and acquire-area behavior.
```csharp
[Test]
public void Joystick_DeadZoneReportsInactiveAndZero()
{
var fixture = CreateJoystick();
fixture.Joystick.SetPointerForTest(new Vector2(106f, 100f));
Assert.That(fixture.Joystick.IsActive, Is.False);
Assert.That(fixture.Joystick.Value, Is.EqualTo(Vector2.zero));
}
[Test]
public void Joystick_ClampsDirectionAndMovesKnobWithinTravel()
{
var fixture = CreateJoystick();
fixture.Joystick.SetPointerForTest(new Vector2(300f, 100f));
Assert.That(fixture.Joystick.IsActive, Is.True);
Assert.That(fixture.Joystick.Value.x, Is.EqualTo(1f).Within(0.001f));
Assert.That(fixture.Knob.anchoredPosition.x, Is.EqualTo(24f).Within(0.001f));
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: Unity EditMode tests for `VirtualJoystickTests`.
Expected: compile fails because `XGame.Input.VirtualJoystick` does not exist.
- [ ] **Step 3: Implement minimal component**
Implement `VirtualJoystick` with:
```csharp
public Vector2 Value { get; private set; }
public bool IsActive { get; private set; }
public event Action Started;
public event Action<Vector2, bool> Changed;
public event Action Ended;
public bool IsScreenPositionInAcquireArea(Vector2 screenPosition);
public void SetPointerForTest(Vector2? screenPosition);
```
It must bind `Knob` in `Awake`, compute screen center/radius from root or `Joystick` child, clamp values, apply dead zone, and reset on release.
- [ ] **Step 4: Run test to verify it passes**
Run: Unity EditMode tests for `VirtualJoystickTests`.
Expected: all `VirtualJoystickTests` pass.
---
### Task 2: VirtualActionButton Runtime Component
**Files:**
- Create: `Client/Assets/Script/xmain/Input/VirtualActionButton.cs`
- Test: `Client/Assets/Script/Tests/EditMode/VirtualActionButtonTests.cs`
**Interfaces:**
- Produces: `ActionButtonId`, `ActionButtonPhase`, `ActionButtonEvent`, `VirtualActionButton.ButtonEvent`, `SetCooldown`, `SetCharges`, `SetButtonEnabled`, `SetCancelArea`, `SetIcon`
- Consumes: Unity `IPointerDownHandler`, `IDragHandler`, `IPointerUpHandler`, optional `Icon`, `CooldownMask`, `CooldownText`, `ChargeText`, `AimIndicator` or `DragArrow`.
- [ ] **Step 1: Write the failing tests**
Add tests for tap, drag release, cancel, blocked cooldown, and charges.
```csharp
[Test]
public void Button_TapEmitsDownThenTap()
{
var button = CreateButton();
var events = Capture(button);
button.HandlePointerDownForTest(new Vector2(100f, 100f), 1, 0f);
button.HandlePointerUpForTest(new Vector2(102f, 100f), 1, 0.1f);
Assert.That(events.Select(e => e.Phase), Is.EqualTo(new[] { ActionButtonPhase.Down, ActionButtonPhase.Tap }));
}
[Test]
public void Button_CooldownEmitsBlockedOnly()
{
var button = CreateButton();
var events = Capture(button);
button.SetCooldown(3f, 5f);
button.HandlePointerDownForTest(new Vector2(100f, 100f), 1, 0f);
Assert.That(events.Single().Phase, Is.EqualTo(ActionButtonPhase.Blocked));
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: Unity EditMode tests for `VirtualActionButtonTests`.
Expected: compile fails because action button types do not exist.
- [ ] **Step 3: Implement minimal component**
Implement pointer interfaces and test helpers:
```csharp
public void HandlePointerDownForTest(Vector2 screenPosition, int pointerId, float now);
public void HandleDragForTest(Vector2 screenPosition, int pointerId, float now);
public void HandlePointerUpForTest(Vector2 screenPosition, int pointerId, float now);
```
Use a single active pointer, emit `Down`, `Drag`, `Tap`, `Release`, `Cancel`, `HoldStart`, `HoldTick`, `HoldEnd`, and `Blocked` according to the spec. Refresh cooldown mask fill, cooldown text, charge text, icon, and enabled/blocked visuals when matching nodes exist.
- [ ] **Step 4: Run test to verify it passes**
Run: Unity EditMode tests for `VirtualActionButtonTests`.
Expected: all `VirtualActionButtonTests` pass.
---
### Task 3: VirtualActionPad Runtime Component
**Files:**
- Create: `Client/Assets/Script/xmain/Input/VirtualActionPad.cs`
- Test: `Client/Assets/Script/Tests/EditMode/VirtualActionPadTests.cs`
**Interfaces:**
- Consumes: `VirtualActionButton`
- Produces: `GetButton(ActionButtonId)`, pad-level `ButtonEvent`, `SetCooldown`, `SetCharges`, `SetButtonEnabled`, `SetCancelAreaVisible`
- [ ] **Step 1: Write the failing tests**
Add tests that build five children named `Btn_Primary`, `Btn_Skill1`, `Btn_Skill2`, `Btn_Skill3`, and `Btn_Skill4`, attach buttons, call `Awake` through Unity lifecycle, and assert lookup/event forwarding.
```csharp
[Test]
public void Pad_FindsFiveButtonsAndForwardsEvents()
{
var pad = CreatePadWithButtons();
var events = new List<ActionButtonEvent>();
pad.ButtonEvent += events.Add;
pad.GetButton(ActionButtonId.Primary).HandlePointerDownForTest(new Vector2(100f, 100f), 1, 0f);
Assert.That(events.Single().ButtonId, Is.EqualTo(ActionButtonId.Primary));
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: Unity EditMode tests for `VirtualActionPadTests`.
Expected: compile fails because `VirtualActionPad` does not exist.
- [ ] **Step 3: Implement minimal component**
Bind child buttons by existing component or child name, assign stable IDs, set each button's cancel area, subscribe to child events, and forward state API calls.
- [ ] **Step 4: Run test to verify it passes**
Run: Unity EditMode tests for `VirtualActionPadTests`.
Expected: all `VirtualActionPadTests` pass.
---
### Task 4: JSON Prefab Descriptions
**Files:**
- Create: `Doc/UIPrefabCreater/UI_VirtualJoystick.json`
- Create: `Doc/UIPrefabCreater/UI_VirtualActionPad.json`
- Test: `Client/Assets/Script/Tests/EditMode/VirtualInputPrefabLayoutTests.cs`
**Interfaces:**
- Consumes: existing `JsonUIPrefabBuilder` component support for `Image`, `TextMeshProUGUI`, `CanvasGroup`, `Button`, and `MonoBehaviour`.
- Produces: prefab definitions that attach `XGame.Input.VirtualJoystick` and `XGame.Input.VirtualActionPad`.
- [ ] **Step 1: Write the failing prefab layout tests**
Test the JSON files exist, define expected root names, include required child node names, and use expected prefab paths.
```csharp
[Test]
public void VirtualActionPadJson_DefinesFiveButtonsAndCancelArea()
{
string json = File.ReadAllText("Doc/UIPrefabCreater/UI_VirtualActionPad.json");
Assert.That(json, Does.Contain("\"prefabName\": \"UI_VirtualActionPad\""));
Assert.That(json, Does.Contain("\"Btn_Primary\""));
Assert.That(json, Does.Contain("\"Btn_Skill4\""));
Assert.That(json, Does.Contain("\"CancelArea\""));
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: Unity EditMode tests for `VirtualInputPrefabLayoutTests`.
Expected: fail because JSON files do not exist.
- [ ] **Step 3: Add JSON files**
Add `UI_VirtualJoystick.json` from current `UI_LobbyJoystick.json` with generic names and `VirtualJoystick` component. Add `UI_VirtualActionPad.json` with the root, five buttons, cancel area, icon/cooldown/charge/aim child nodes, and `VirtualActionPad` component.
- [ ] **Step 4: Run test to verify it passes**
Run: Unity EditMode tests for `VirtualInputPrefabLayoutTests`.
Expected: all layout tests pass.
---
### Task 5: LobbyWorldController Migration
**Files:**
- Modify: `Client/Assets/Script/xmain/Client/LobbyWorldController.cs`
- Test: `Client/Assets/Script/Tests/EditMode/LobbyWorldControllerVirtualJoystickTests.cs`
**Interfaces:**
- Consumes: `XGame.Input.VirtualJoystick`
- Produces: lobby movement that uses joystick events instead of private joystick sampling.
- [ ] **Step 1: Write the failing integration test**
Use reflection or exposed test helpers to verify `LobbyWorldController` can accept a `VirtualJoystick` value and `GetMoveDirection` includes it. If direct runtime reflection is too brittle, add a focused test for a new internal helper that maps joystick vector plus camera vectors to world direction.
```csharp
[Test]
public void LobbyMovement_UsesVirtualJoystickVector()
{
Vector3 dir = LobbyWorldController.MapMoveInputForTest(
new Vector2(1f, 0f),
Vector3.forward,
Vector3.right,
false, false, false, false);
Assert.That(dir, Is.EqualTo(Vector3.right));
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: Unity EditMode tests for `LobbyWorldControllerVirtualJoystickTests`.
Expected: fail because helper or migration is not present.
- [ ] **Step 3: Migrate controller**
Change `JoystickPrefabPath` to `UI_VirtualJoystick.prefab`, replace joystick root/knob sampling fields with one `VirtualJoystick` field, subscribe to `Changed` and `Ended`, keep the current `joystick` and `joystickActive` cached values as business input, and remove duplicated pointer sampling helpers. Camera input uses `virtualJoystick.IsScreenPositionInAcquireArea(pos)` when available.
- [ ] **Step 4: Run test to verify it passes**
Run: Unity EditMode tests for `LobbyWorldControllerVirtualJoystickTests`.
Expected: integration test passes.
---
### Task 6: Verification
**Files:**
- All files changed in tasks 1-5.
**Interfaces:**
- Consumes: all implementation tasks.
- Produces: verified working tree ready for review.
- [ ] **Step 1: Run targeted EditMode tests**
Run all new test classes:
```text
VirtualJoystickTests
VirtualActionButtonTests
VirtualActionPadTests
VirtualInputPrefabLayoutTests
LobbyWorldControllerVirtualJoystickTests
```
Expected: all pass.
- [ ] **Step 2: Run broader existing affected tests**
Run existing tests that touch UI and stage isolation:
```text
MiniGameStageIsolationTests
UIManagerAttachTests
MatchWaitingPrefabLayoutTests
```
Expected: all pass.
- [ ] **Step 3: Inspect working tree**
Run: `git status --short`
Expected: new/modified files are limited to input components, tests, prefab JSON, plan/spec if updated, and `LobbyWorldController.cs`; unrelated existing Unity asset changes remain unstaged.
- [ ] **Step 4: Commit only related files if tests pass**
Stage only the files created or modified by this implementation and commit with:
```bash
git commit -m "feat: add virtual hud input components"
```
Expected: commit includes no unrelated actor/UI asset modifications.