Hard-linked View Components 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: Every runtime UI view references its child components and sprites through hard [SerializeField] inspector links assigned in the prefab — no F()/FindDeep/GetComponentInChildren/GetComponent/UIButton.Attach at runtime, no sprite loading from the project.
Architecture: Most views already declare the [SerializeField] fields but keep a by-name Bind() fallback; the work is (a) bake the links into prefabs, (b) delete the fallback + FindDeep. Two deeper strands: author Button+UIButton components into the prefabs so nothing calls UIButton.Attach at runtime, and serialize NavBarView's captured sprites/sizes.
Tech Stack: Unity UGUI, TMPro, Zenject (controllers only), C#. No unit-test framework — "tests" are editor verification scripts (null-ref assert) + a grep gate + compile + a play spot-check.
Global Constraints¶
- All prefab writes go through
PrefabUtility.LoadPrefabContents→ mutate →SaveAsPrefabAsset→UnloadPrefabContents. NeverOpenPrefab(it auto-saves). - Do not touch the root Canvas / RectTransform driven state (anchors
(0,0)/(0,0), pivot(0,0), scale(0,0,0)) — it was just repaired to matchWinScreenView. - Child object names stay (they're just no longer used for lookup).
- No behaviour/layout changes. Pure wiring refactor.
- Real Python:
C:\Users\rksy\AppData\Local\Programs\Python\Python312\python.exe; setPYTHONUTF8=1for scripts that print. - Commit after each view converts cleanly (per-view revertability).
- Field names already present in a script are kept as-is (they're the wiring keys).
Task 0: Shared editor tooling (wiring + verification)¶
Files:
- Create: unity/raiders/Assets/Scripts/Editor/UIWiring/UIWireOneOff.cs (throwaway; deleted in the final task)
- Create: unity/raiders/Assets/Scripts/Editor/UIWiring/UIWireVerify.cs (throwaway; deleted in the final task)
Interfaces:
- Produces: UIWire.Set(GameObject prefabRoot, Component target, string field, Object value) helper that sets a SerializedProperty object-ref by field name; UIWireVerify.AssertAllRefs(prefabPath, componentType) that logs any null serialized object-ref.
- [ ] Step 1: Write the wiring helper
UIWireOneOff.cs — a RunCommand-invocable static utility. Core helper:
// Set a serialized object-reference field on `target` (a component in a LoadPrefabContents tree).
static void Set(Component target, string field, Object value)
{
var so = new SerializedObject(target);
var p = so.FindProperty(field);
if (p == null) { Debug.LogError($"no field '{field}' on {target.GetType().Name}"); return; }
p.objectReferenceValue = value;
so.ApplyModifiedPropertiesWithoutUndo();
}
// Array variant: sizes the array then assigns element refs.
static void SetArray(Component target, string field, Object[] values) { /* FindProperty; arraySize; GetArrayElementAtIndex(i).objectReferenceValue */ }
// FindDeep by name (reuse the existing depth-first search) to resolve children during the one-off pass.
Each per-view wiring pass: var root = PrefabUtility.LoadPrefabContents(path); var v = root.GetComponentInChildren<T>(true); Set(v, "field", FindDeep(root.transform,"child")?.GetComponent<TChild>()); ... PrefabUtility.SaveAsPrefabAsset(root, path); PrefabUtility.UnloadPrefabContents(root);
- [ ] Step 2: Write the verifier
UIWireVerify.cs: AssertAllRefs(string prefabPath, System.Type compType) — LoadPrefabContents, get the component(s), iterate SerializedObject visible properties; for any SerializedPropertyType.ObjectReference (and array elements) whose objectReferenceValue == null, log "[UNWIRED] {prefab} {comp}.{path}". Returns count of nulls.
-
[ ] Step 3: Verify it compiles — run any
Unity_RunCommand(fails if the editor assembly doesn't compile). Expected: compiles OK. -
[ ] Step 4: Commit
git add unity/raiders/Assets/Scripts/Editor/UIWiring/
git commit -m "chore(ui): add one-off UI wiring + verify editor tooling"
Group A — verify-only (already hard-linked)¶
Task 1: Verify InGameView / TopPanelView / CurrencyView wiring¶
These three have no Bind/FindDeep/F() — they are already the target state. Only confirm every prefab link is assigned.
Files (prefabs): InGameView/InGameView.prefab, Shared/TopPanelView.prefab, Shared/CurrencyView.prefab (+ any prefab embedding TopPanel/Currency: LobbyView, InventoryView, InventoryEquipmentView).
- [ ] Step 1: Run
UIWireVerify.AssertAllRefsforInGameView,TopPanelView,CurrencyViewon their prefabs. Expected: 0 unwired (a[UNWIRED]line means an authored link is missing — wire it viaUIWireOneOff.Setusing the field's// commentname, e.g.timerValue ← txt_value). - [ ] Step 2: Grep gate —
InGameView.cs,TopPanelView.cs,CurrencyView.cscontain noFindDeep/GetComponentInChildren/F(. Expected: none. - [ ] Step 3: Commit (only if a wiring fix was needed):
git commit -am "fix(ui): wire missing InGame/TopPanel/Currency links".
Group B — simple fallback removal (fields exist; delete Bind fallback + FindDeep)¶
For each: (1) run the one-off wiring pass to bake links from the field // comment names; (2) verify 0 unwired; (3) delete the if (x == null) x = FindDeep(...) fallback lines, the Bind() method (inline any non-lookup setup into Awake), the _bound flag, and the FindDeep/Img/Txt/Sub helpers; (4) grep gate; (5) compile; (6) commit.
Task 2: InventoryPassiveSkillView¶
Fields → children: description ← txt_description, icon ← img_icon. Sprites: none. Prefab: InventoryView/InventoryPassiveSkill.prefab.
- [ ] Wire → verify 0 unwired → delete Bind/FindDeep, keep _normal = description.color capture in Awake (guard null) → grep gate → compile → commit.
Task 3: InventoryStatView¶
Fields → children: value ← txt_value, arrow ← img_arrow. Prefab: InventoryView/InventoryStat.prefab.
- [ ] Wire → verify → delete Bind/FindDeep; move _normal = value.color into Awake → grep gate → compile → commit.
Task 4: StageIndicator¶
Fields → children: numberLabel ← txt_value. Prefabs: the three pip templates inside LobbyView.prefab (panel_done/panel_active/panel_notdone). Wire on each template instance.
- [ ] Wire (three templates) → verify → delete the LobbyView.FindDeep fallback in Awake/SetNumber → grep gate → compile → commit.
Task 5: InventoryItemView¶
Fields → children: icon ← img_icon, frame ← img_frame, levelLabel ← txt_lvl, arrowUp ← img_arrow_up (GameObject), betterOutline ← img_framebetteritem (GameObject), back ← img_back. Sprites (already serialized, verify assigned): commonFrame/rareFrame/epicFrame/legendaryFrame. Prefab: InventoryView/InventoryItem.prefab.
- [ ] Wire → verify (incl. the 4 frame sprites) → delete Bind/Img/FindDeep fallback → MakeClickable: see Task 9 (UIButton authoring) — until then keep UIButton.Attach but note it. Grep gate (allow UIButton for now) → compile → commit.
Group C — composite views (fallback removal + dynamic templates)¶
Task 6: InventoryItemCardView¶
Heaviest FindDeep user (14 sites). Fields to ADD (currently all via F()/Sub()): _outline ← img_cardoutline; badge: _badgeRoot ← panel_label, _badgeText ← txt_lable, _badgeArrow ← img_arrow; header: _headerItem ← (InventoryItemView under layout_header), _nameText ← txt_itemname, _powerText ← panel_power/txt_value, _powerArrow ← panel_power/img_arrow; stats array _stats[4] ← InventoryStat_Attack/Health/Crit/Speed; passives: _passivesRoot ← layout_passives, _passiveTemplate ← (typed InventoryPassiveSkillView, first child of layout_passives); actions: _actionsRoot ← layout_actions, _scrapCost/_levelCost ← (InventoryStatView[0..1] under layout_actions), buttons _scrap/_equip/_unequip/_lvlup ← btn_scrap/btn_equip/btn_unequip/btn_lvlup.
- [ ] Convert each private field to [SerializeField]; type the passive template as InventoryPassiveSkillView and spawn via Instantiate(_passiveTemplate) (returns typed — drop GetComponent). Buttons: reference authored UIButton (Task 9). Wire prefab InventoryView/InventoryItemCard.prefab → verify → delete Bind/F/Sub/Stat/Btn → grep gate → compile → commit.
Task 7: InventoryView¶
Fields → children: topPanel ← (TopPanelView), navBar ← (NavBarView), cellPrefab (already serialized — verify), arrays _slots[8] ← layout_items_l[0..3] + layout_items_r[0..3] (as [SerializeField] InventoryItemView[] slots), _stats[4] ← InventoryStat_Attack/Health/Crit/Speed, _grid ← grid_items (as [SerializeField] RectTransform grid). Stash: Instantiate(cellPrefab) already returns typed.
- [ ] Add serialized slots/stats/grid; wire prefab InventoryView/InventoryView.prefab (slots in Loadout.SlotOrder order) → verify → delete Bind/F/Stat/SlotCell lookup + GetComponentInChildren → grep gate → compile → commit.
Task 8: LobbyView & InventoryEquipmentView¶
LobbyView fields → children (mostly exist): topPanel/navBar (by-type → serialized), descendButton ← btn_raid, prevButton ← btn_left, nextButton ← btn_right, locationName ← txt_locationname, stageValue ← layout_stagenumber/txt_value, haulValue ← panel_haul/panel_value/txt_value, raidsValue ← panel_raids/txt_value, doneTemplate/activeTemplate/notdoneTemplate ← panel_done/active/notdone, add [SerializeField] Transform stageRow ← panel_stagenumbers. Stage pips: Instantiate(template) + typed StageIndicator ref via GetComponent is dropped by typing the template as StageIndicator OR keep go.GetComponent<StageIndicator>() on the clone (acceptable — clone of an authored prefab; but prefer typing templates as StageIndicator). InventoryEquipmentView fields: equippedCard ← InventoryItemCard_Equipped, selectedCard ← InventoryItemCard_Selected, topPanel (by-type→serialized), add closeButton ← btn_close (authored UIButton, Task 9).
- [ ] Wire both prefabs → verify → delete Bind/FindDeep/Btn/Txt/F/GetComponentInChildren → grep gate → compile → commit.
Group D — eliminate runtime UIButton.Attach (author components in prefabs)¶
Task 9: Author Button+UIButton into clickable prefab nodes¶
UIButton.Attach adds Button+UIButton at runtime on: nav slots (btn_raid/btn_inventory), item cells (img_back), lobby buttons (btn_raid/btn_left/btn_right), lobby stage pips, card buttons (btn_scrap/btn_equip/btn_unequip/btn_lvlup), equipment btn_close, inventory slot/stash cells.
- [ ] For each authored clickable node, in its prefab add a Button (transition None, targetGraphic = the node's Image) + a UIButton component; then reference the UIButton from the owning view's serialized field. Do this via LoadPrefabContents (add components with AddComponent, set targetGraphic, save).
- [ ] Replace every UIButton.Attach(go, graphic) call-site with the serialized UIButton ref (fixed clickables) — the wiring binds Clicked. For dynamically-instantiated clickables (stash cells, stage pips): author the Button+UIButton on the template prefab so each clone already has it; get the typed UIButton from the clone via the template being typed (or a serialized field on the cell view exposing its UIButton). Keep UIButton.Attach only if a clone genuinely can't pre-author it (document why).
- [ ] Verify → grep gate (UIButton.Attach gone from converted views) → compile → play spot-check each screen → commit.
Task 10: NavBarView (capture → serialized)¶
Serialize the two slots' pieces and the reference styles: add [SerializeField] for each Slot's rect/bg/label/icon (or a serialized Slot[2]), and activeSprite/inactiveSprite/activeSize/inactiveSize.
- [ ] Add serialized fields for the captured sprites/sizes + the two slots' rect/bg/label/icon; wire prefab NavBarView.prefab → verify → delete Bind/Capture/FindDeep → grep gate → compile → play spot-check nav → commit.
Task 11: Final gate + remove tooling¶
- [ ] Repo-wide grep: no
FindDeep,GetComponentInChildren,UIButton.Attach, orF(/Sub(lookup helpers remain inAssets/Scripts/UI/. Fix any stragglers. - [ ] Run
UIWireVerify.AssertAllRefsacross all touched prefabs → 0 unwired. - [ ] Play-test lobby → inventory → equipment popup → in-raid HUD → nav switching. Expected: identical behaviour.
- [ ] Delete
Assets/Scripts/Editor/UIWiring/(throwaway tooling) + its.meta. - [ ] Commit:
git commit -am "refactor(ui): hard-link all view components; remove runtime lookups + UIButton.Attach".
Self-review notes¶
InGameView/TopPanelView/CurrencyViewalready compliant → Task 1 is verify-only.- Dynamic repeats (stash cells, stage pips, passive rows) keep a typed template ref +
Instantiate(template)(returns typed) — noGetComponent; count stays runtime (unavoidable). - The heaviest strands are Task 9 (authoring
Button+UIButtoninto prefabs) and Task 10 (NavBarView) — schedule extra care/verification there. - Every prefab edit uses
LoadPrefabContents/SaveAsPrefabAsset; the root driven Canvas state is never touched.