From d9beaebfdefa4e1d54cde83098693a6e2d170eb8 Mon Sep 17 00:00:00 2001 From: ud18010 Date: Tue, 4 Aug 2026 14:41:27 +0800 Subject: [PATCH] docs: plan player avatar selection --- .../2026-08-04-player-avatar-selection.md | 460 ++++++++++++++++++ 1 file changed, 460 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-04-player-avatar-selection.md diff --git a/docs/superpowers/plans/2026-08-04-player-avatar-selection.md b/docs/superpowers/plans/2026-08-04-player-avatar-selection.md new file mode 100644 index 00000000..205b4536 --- /dev/null +++ b/docs/superpowers/plans/2026-08-04-player-avatar-selection.md @@ -0,0 +1,460 @@ +# Player Avatar Selection 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:** 在现有角色属性系统中加入 54 张头像的预览式选择、确认保存和全局刷新,并提供可滚动的头像选择界面。 + +**Architecture:** 使用 PlayerAvatarCatalog 统一头像 ID 与资源路径;PlayerProfileModule 管理头像异步加载、选择弹窗和临时预览状态;头像选择界面由固定 JSON/Prefab 槽位组成,确认后通过 PlayerProfileStore.Changed 刷新 HUD 与详情面板。 + +**Tech Stack:** Unity 2022.3.62f3, C#, UnityEngine.UI, TextMeshProUGUI, NUnit EditMode tests, XResLoader.coLoadRes, JsonUIPrefabBuilder。 + +## Global Constraints + +- 保持 AvatarId = 0 的旧数据兼容,显示 GamerPic_01;确认后保存 1~54。 +- 头像资源使用 Client/Assets/Game/Art/UI/Texture/Icon/GamerPic/GamerPic_01.png ~ GamerPic_54.png。 +- 新 UI 使用 Doc/UIPrefabCreater JSON 生成 Prefab,不手工编辑 .prefab、.meta 或其他 Unity 序列化文件。 +- 所有资源加载使用 XResLoader.coLoadRes 异步流程,不使用新的同步资源加载 API。 +- 选择头像只修改 PlayerProfileData.AvatarId,不修改等级、货币、职业或战斗属性。 +- 保留工作区已有的 UI 资源移动、删除和字体修改,不将无关改动加入提交。 +- 设计分辨率使用 [2048, 1024];头像选择弹窗挂载 UILayer.Dialog。 +- 生产代码必须先有一个可观察失败的测试,再实现使其通过。 + +--- + +## 文件清单 + +### 新建 + +- Client/Assets/Script/xmain/Module/Player/PlayerAvatarCatalog.cs:头像数量、ID 归一化和资源路径映射。 +- Doc/UIPrefabCreater/UI_PlayerAvatarPicker.json:头像选择 Dialog 的完整节点、54 个头像资源、按钮和绑定声明。 +- Client/Assets/Game/Art/UI/Prefab/UI_PlayerAvatarPicker.prefab:由 Unity JSON Prefab Builder 生成;不手工编辑。 + +### 修改 + +- Client/Assets/Script/xmain/Module/Player/PlayerProfileModule.cs:头像 Image 缓存、异步刷新、弹窗生命周期、预览/确认/取消交互。 +- Doc/UIPrefabCreater/UI_PlayerProfilePanel.json:让资料详情头像区域可点击打开头像选择器,并补充头像按钮绑定声明。 +- Doc/UIPrefabCreater/UI_PlayerProfileHud.json:补充 HUD 头像 Image 的业务绑定声明,保持现有 HUD 布局。 +- Client/Assets/Script/Tests/EditMode/PlayerProfileModuleTests.cs:头像目录、资料更新和三个角色资料 JSON/头像选择 JSON 的 EditMode 测试。 + +### Unity 自动生成或验证 + +- 对上述 JSON 执行 JSON Prefab Builder,生成/更新对应 Prefab。 +- Unity 自动生成新 .cs.meta 和 Prefab .meta;不手工创建或改写 GUID。 + +## Task 1: Write the failing avatar catalog tests + +**Files:** +- Modify: Client/Assets/Script/Tests/EditMode/PlayerProfileModuleTests.cs +- Test target to create in Task 2: Client/Assets/Script/xmain/Module/Player/PlayerAvatarCatalog.cs + +**Interfaces:** +- Produces PlayerAvatarCatalog.Count, PlayerAvatarCatalog.DefaultAvatarId, PlayerAvatarCatalog.NormalizeId(int), and PlayerAvatarCatalog.GetAssetPath(int). + +- [ ] **Step 1: Add tests for default and invalid IDs** + +Append these tests to PlayerProfileModuleTests: + +~~~csharp +[Test] +public void AvatarCatalog_NormalizesLegacyAndInvalidIdsToDefault() +{ + Assert.That(PlayerAvatarCatalog.NormalizeId(0), Is.EqualTo(PlayerAvatarCatalog.DefaultAvatarId)); + Assert.That(PlayerAvatarCatalog.NormalizeId(-3), Is.EqualTo(PlayerAvatarCatalog.DefaultAvatarId)); + Assert.That(PlayerAvatarCatalog.NormalizeId(PlayerAvatarCatalog.Count + 1), Is.EqualTo(PlayerAvatarCatalog.DefaultAvatarId)); +} + +[Test] +public void AvatarCatalog_MapsIdsToTwoDigitGamerPicPaths() +{ + Assert.That(PlayerAvatarCatalog.GetAssetPath(1), Is.EqualTo("Assets/Game/Art/UI/Texture/Icon/GamerPic/GamerPic_01.png")); + Assert.That(PlayerAvatarCatalog.GetAssetPath(9), Is.EqualTo("Assets/Game/Art/UI/Texture/Icon/GamerPic/GamerPic_09.png")); + Assert.That(PlayerAvatarCatalog.GetAssetPath(54), Is.EqualTo("Assets/Game/Art/UI/Texture/Icon/GamerPic/GamerPic_54.png")); + Assert.That(PlayerAvatarCatalog.GetAssetPath(0), Is.EqualTo(PlayerAvatarCatalog.GetAssetPath(PlayerAvatarCatalog.DefaultAvatarId))); +} +~~~ + +- [ ] **Step 2: Run the new tests and verify the red state** + +Run the Unity EditMode filter for PlayerProfileModuleTests.AvatarCatalog through the project Unity test runner. Expected result: compilation/test failure because PlayerAvatarCatalog does not yet exist. The failure must identify the missing catalog API rather than a JSON or Unity project error. + +- [ ] **Step 3: Commit the red tests** + +~~~powershell +git add -- 'Client/Assets/Script/Tests/EditMode/PlayerProfileModuleTests.cs' +git commit -m "test: define avatar catalog behavior" +~~~ + +## Task 2: Implement the avatar catalog and make catalog tests green + +**Files:** +- Create: Client/Assets/Script/xmain/Module/Player/PlayerAvatarCatalog.cs +- Test: Client/Assets/Script/Tests/EditMode/PlayerProfileModuleTests.cs + +**Interfaces:** +- Consumes no runtime state or UI dependencies. +- Produces Count = 54, DefaultAvatarId = 1, NormalizeId(int), and GetAssetPath(int). + +- [ ] **Step 1: Implement the minimal catalog** + +Create the class with this exact behavior: + +~~~csharp +using System.Globalization; + +namespace XGame +{ + public static class PlayerAvatarCatalog + { + public const int Count = 54; + public const int DefaultAvatarId = 1; + private const string AssetRoot = "Assets/Game/Art/UI/Texture/Icon/GamerPic/GamerPic_"; + + public static int NormalizeId(int avatarId) + { + return avatarId >= DefaultAvatarId && avatarId <= Count ? avatarId : DefaultAvatarId; + } + + public static string GetAssetPath(int avatarId) + { + int normalizedId = NormalizeId(avatarId); + return AssetRoot + normalizedId.ToString("00", CultureInfo.InvariantCulture) + ".png"; + } + } +} +~~~ + +- [ ] **Step 2: Run catalog tests and verify green** + +Run the same Unity EditMode filter. Expected result: the two catalog tests pass and the existing PlayerProfileModuleTests tests remain green. + +- [ ] **Step 3: Commit the catalog** + +~~~powershell +git add -- 'Client/Assets/Script/xmain/Module/Player/PlayerAvatarCatalog.cs' 'Client/Assets/Script/Tests/EditMode/PlayerProfileModuleTests.cs' +git commit -m "feat: add player avatar catalog" +~~~ + +## Task 3: Add the failing JSON contract test + +**Files:** +- Modify: Client/Assets/Script/Tests/EditMode/PlayerProfileModuleTests.cs +- Test target to create in Task 4: Doc/UIPrefabCreater/UI_PlayerAvatarPicker.json + +**Interfaces:** +- The JSON must expose Img_PreviewAvatar, Btn_Cancel, Btn_Confirm, Btn_Close, Img_Mask, and Btn_Avatar_01 through Btn_Avatar_54. + +- [ ] **Step 1: Add the failing validation and slot contract tests** + +Add the picker validation assertion to PlayerProfileJsonFiles_PassPrefabBuilderValidation: + +~~~csharp +Assert.That(JsonUIPrefabBuilder.Validate("../Doc/UIPrefabCreater/UI_PlayerAvatarPicker.json").Ok, Is.True); +~~~ + +Add this test: + +~~~csharp +[Test] +public void AvatarPickerJson_DeclaresAllAvatarButtons() +{ + string jsonPath = System.IO.Path.Combine(Application.dataPath, "../Doc/UIPrefabCreater/UI_PlayerAvatarPicker.json"); + string json = System.IO.File.ReadAllText(jsonPath); + + for (int avatarId = 1; avatarId <= PlayerAvatarCatalog.Count; avatarId++) + { + string suffix = avatarId.ToString("00", System.Globalization.CultureInfo.InvariantCulture); + StringAssert.Contains("Btn_Avatar_" + suffix, json); + StringAssert.Contains("GamerPic_" + suffix + ".png", json); + } +} +~~~ + +- [ ] **Step 2: Run the JSON tests and verify the red state** + +Run the Unity EditMode filter for PlayerProfileModuleTests. Expected result: the new assertions fail because UI_PlayerAvatarPicker.json does not exist yet. + +- [ ] **Step 3: Commit the failing JSON contract** + +~~~powershell +git add -- 'Client/Assets/Script/Tests/EditMode/PlayerProfileModuleTests.cs' +git commit -m "test: define avatar picker prefab contract" +~~~ + +## Task 4: Create and validate the avatar picker JSON + +**Files:** +- Create: Doc/UIPrefabCreater/UI_PlayerAvatarPicker.json +- Generated by Unity: Client/Assets/Game/Art/UI/Prefab/UI_PlayerAvatarPicker.prefab +- Test: Client/Assets/Script/Tests/EditMode/PlayerProfileModuleTests.cs + +**Interfaces:** +- Produces a Dialog root, overlay, preview area, scrollable content, 54 fixed avatar buttons, and footer actions. +- Every slot is named Btn_Avatar_01 through Btn_Avatar_54; each uses the matching GamerPic_XX.png; each selected overlay Img_Selected_XX starts inactive. + +- [ ] **Step 1: Add JSON root and registered assets** + +Use schemaVersion 1, prefabPath Assets/Game/Art/UI/Prefab/UI_PlayerAvatarPicker.prefab, and reference resolution [2048, 1024]. Register these current workspace assets: + +~~~json +{ + "schemaVersion": 1, + "prefabName": "UI_PlayerAvatarPicker", + "prefabPath": "Assets/Game/Art/UI/Prefab/UI_PlayerAvatarPicker.prefab", + "description": "Dialog avatar picker with preview, 54 fixed avatar buttons, cancel and confirm actions.", + "canvas": { + "canvasScaler": { + "referenceResolution": [2048, 1024], + "matchWidthOrHeight": 0.5 + } + }, + "assets": { + "sprites": { + "ui_overlay_dim": "Assets/Game/Art/UI/Texture/UI/Common/ui_overlay_dim.png", + "ui_panel_popup": "Assets/Game/Art/UI/Texture/UI/Common/ui_panel_popup.png", + "ui_panel_header": "Assets/Game/Art/UI/Texture/UI/Common/ui_panel_header.png", + "ui_panel_content": "Assets/Game/Art/UI/Texture/UI/Common/ui_panel_content.png", + "ui_panel_footer": "Assets/Game/Art/UI/Texture/UI/Common/ui_panel_footer.png", + "ui_list_item_normal": "Assets/Game/Art/UI/Texture/UI/Common/ui_list_item_normal.png", + "ui_list_item_selected": "Assets/Game/Art/UI/Texture/UI/Common/ui_list_item_selected.png", + "slot_selected": "Assets/Game/Art/UI/Texture/UI/Common/slot_selected.png", + "btn_primary_normal": "Assets/Game/Art/UI/Texture/UI/Common/btn_primary_normal.png", + "btn_primary_pressed": "Assets/Game/Art/UI/Texture/UI/Common/btn_primary_pressed.png", + "btn_secondary_normal": "Assets/Game/Art/UI/Texture/UI/Common/btn_secondary_normal.png", + "btn_secondary_pressed": "Assets/Game/Art/UI/Texture/UI/Common/btn_secondary_pressed.png", + "btn_close": "Assets/Game/Art/UI/Texture/UI/Common/btn_close.png", + "avatar_frame": "Assets/Game/Art/UI/Texture/UI/Common/avatar_frame.png" + } + } +} +~~~ + +Add GamerPic_01.png through GamerPic_54.png to the same assets.sprites map with keys avatar_01 through avatar_54. + +- [ ] **Step 2: Add the fixed layout nodes** + +The JSON nodes must have this hierarchy and behavior: + + UI_PlayerAvatarPicker + ├─ Img_Mask Image + Button + ├─ Panel_AvatarPicker sliced popup Image + │ ├─ Img_Header / Txt_Title title “选择头像” + │ ├─ Btn_Close close Button + │ ├─ Panel_Preview + │ │ ├─ Img_PreviewFrame avatar frame Image + │ │ ├─ Img_PreviewAvatar preview Image + │ │ └─ Txt_PreviewHint preview hint text + │ ├─ Scroll_Avatars ScrollRect, vertical only + │ │ └─ Content_Avatars GridLayoutGroup, six fixed columns + │ │ └─ AvatarSlot_01..54 six columns by nine rows + │ │ ├─ Btn_Avatar_XX Image + Button + │ │ ├─ Img_Avatar_XX matching GamerPic Image + │ │ └─ Img_Selected_XX slot_selected Image, inactive + │ └─ Panel_Footer + │ ├─ Btn_Cancel secondary Button + │ └─ Btn_Confirm primary Button + +Use a ScrollRect and GridLayoutGroup supported by the existing JSON builder. Use six columns, nine rows, 116×116 cell size, 18×18 spacing, 24 padding, and enough content height for all 54 nodes. Add bindings for Img_Mask, Btn_Close, Img_PreviewAvatar, Btn_Cancel, Btn_Confirm, Scroll_Avatars, and every avatar button/preview/selected node. Add events PlayerProfile.CancelAvatarPicker for mask/close/cancel, PlayerProfile.ConfirmAvatarPicker for confirm, and PlayerProfile.SelectAvatar_XX for each avatar button. + +- [ ] **Step 3: Validate and generate the Prefab through Unity** + +Run the project JSON Prefab Builder in Unity against Doc/UIPrefabCreater/UI_PlayerAvatarPicker.json. Expected result: UI_PlayerAvatarPicker.prefab is generated at Client/Assets/Game/Art/UI/Prefab/UI_PlayerAvatarPicker.prefab and JsonUIPrefabBuilder.Validate passes. + +- [ ] **Step 4: Run JSON tests and verify green** + +Run the Unity EditMode filter for PlayerProfileModuleTests. Expected result: all existing profile JSON checks and the avatar picker validation/button contract tests pass. + +- [ ] **Step 5: Commit JSON and generated Prefab** + +~~~powershell +git add -- 'Doc/UIPrefabCreater/UI_PlayerAvatarPicker.json' 'Client/Assets/Game/Art/UI/Prefab/UI_PlayerAvatarPicker.prefab' +git commit -m "feat: add avatar picker prefab" +~~~ + +## Task 5: Extend profile JSON bindings for avatar entry and HUD image + +**Files:** +- Modify: Doc/UIPrefabCreater/UI_PlayerProfilePanel.json +- Modify: Doc/UIPrefabCreater/UI_PlayerProfileHud.json +- Generated by Unity: the two corresponding profile Prefabs +- Test: Client/Assets/Script/Tests/EditMode/PlayerProfileModuleTests.cs + +**Interfaces:** +- UI_PlayerProfilePanel exposes a Button on Img_AvatarFrame with event PlayerProfile.OpenAvatarPicker. +- Both existing Prefabs keep their layout and expose Img_AvatarIcon for runtime Image.sprite replacement. + +- [ ] **Step 1: Add detail avatar click binding** + +In UI_PlayerProfilePanel.json, add a Button component to Img_AvatarFrame with onClick PlayerProfile.OpenAvatarPicker; add Img_AvatarFrame and Img_AvatarIcon to bindings; add the event to root events. + +- [ ] **Step 2: Declare HUD avatar binding** + +In UI_PlayerProfileHud.json, add Img_AvatarIcon to bindings. Keep Btn_Profile opening the detail panel. + +- [ ] **Step 3: Extend JSON validation** + +Assert that HUD, detail, and picker JSON all return Ok == true from JsonUIPrefabBuilder.Validate. + +- [ ] **Step 4: Regenerate and verify** + +Run the JSON builder for changed files, then run PlayerProfileModuleTests. Expected result: both profile Prefabs regenerate without validation errors and all JSON tests pass. + +- [ ] **Step 5: Commit JSON binding changes and generated Prefabs** + +~~~powershell +git add -- 'Doc/UIPrefabCreater/UI_PlayerProfilePanel.json' 'Doc/UIPrefabCreater/UI_PlayerProfileHud.json' 'Client/Assets/Game/Art/UI/Prefab/UI_PlayerProfilePanel.prefab' 'Client/Assets/Game/Art/UI/Prefab/UI_PlayerProfileHud.prefab' +git commit -m "feat: bind avatar entry in profile UI" +~~~ + +## Task 6: Implement avatar loading, preview, confirm, cancel, and refresh + +**Files:** +- Modify: Client/Assets/Script/xmain/Module/Player/PlayerProfileModule.cs +- Test: Client/Assets/Script/Tests/EditMode/PlayerProfileModuleTests.cs + +**Interfaces:** +- Consumes PlayerAvatarCatalog, the three generated Prefabs, and PlayerProfileStore. +- Produces OpenAvatarPicker, CancelAvatarPicker, ConfirmAvatarPicker, and per-slot selection callbacks that set pendingAvatarId only. + +- [ ] **Step 1: Add the confirm-only mutation test** + +Add this test: + +~~~csharp +[Test] +public void ConfirmedAvatarUpdate_ChangesOnlyAvatarId() +{ + PlayerProfileData original = PlayerProfileData.CreateDefault(); + original.Nickname = "Alice"; + original.Gold = 123; + original.AvatarId = 1; + PlayerProfileStore.Replace(original); + + PlayerProfileStore.Update(profile => profile.AvatarId = 17); + + PlayerProfileData current = PlayerProfileStore.Current; + Assert.That(current.AvatarId, Is.EqualTo(17)); + Assert.That(current.Nickname, Is.EqualTo("Alice")); + Assert.That(current.Gold, Is.EqualTo(123)); +} +~~~ + +Run it before changing PlayerProfileModule. Expected result: it passes against the existing store and documents the exact update operation the module must use. + +- [ ] **Step 2: Add state and cached Image fields** + +Add these fields to PlayerProfileModule: + +~~~csharp +private const string AvatarPickerPrefabPath = "Assets/Game/Art/UI/Prefab/UI_PlayerAvatarPicker.prefab"; +private GameObject avatarPickerView; +private bool openingAvatarPicker; +private int avatarPickerVersion; +private int pendingAvatarId; +private Image hudAvatarImage; +private Image detailAvatarImage; +private readonly Dictionary avatarSpriteCache = new Dictionary(); +~~~ + +Cache Img_AvatarIcon in the HUD/detail setup and reset all new fields in Close. + +- [ ] **Step 3: Implement asynchronous avatar refresh** + +Add a coroutine that loads PlayerAvatarCatalog.GetAssetPath(avatarId) with XResLoader.coLoadRes(path, typeof(Sprite), ...), caches successful sprites, and assigns an Image only if it still exists and the current normalized profile AvatarId equals the requested ID. If loading returns null, leave the Prefab’s ico_user placeholder and log [PlayerProfileModule] load avatar failed: . + +Call the coroutine from RefreshHud and RefreshDetail. Never call XResLoader.LoadRes synchronously. + +- [ ] **Step 4: Bind the detail avatar entry** + +In BindDetailEvents, bind the Button on Img_AvatarFrame to OpenAvatarPicker. Keep Btn_Close and Img_Mask bound to CloseDetail. + +- [ ] **Step 5: Implement picker open and control caching** + +Implement OpenAvatarPicker and CoOpenAvatarPicker with the same versioned asynchronous lifecycle as OpenAsync and CoOpenDetail: + +1. Return when picker exists or is loading. +2. Increment avatarPickerVersion and load AvatarPickerPrefabPath asynchronously. +3. Instantiate on UILayer.Dialog. +4. Set pendingAvatarId = PlayerAvatarCatalog.NormalizeId(PlayerProfileStore.Current.AvatarId). +5. Bind mask, close, cancel, and confirm. +6. Loop from 1 through PlayerAvatarCatalog.Count, bind Btn_Avatar_XX, and capture the loop ID locally before adding the listener. +7. Cache Img_PreviewAvatar, selected overlays, and avatar Images, then call RefreshAvatarPicker. + +- [ ] **Step 6: Implement preview and commit semantics** + +Use this behavior: + +~~~csharp +private void SelectPendingAvatar(int avatarId) +{ + pendingAvatarId = PlayerAvatarCatalog.NormalizeId(avatarId); + RefreshAvatarPicker(); +} + +private void ConfirmAvatarPicker() +{ + int selectedId = PlayerAvatarCatalog.NormalizeId(pendingAvatarId); + PlayerProfileStore.Update(profile => profile.AvatarId = selectedId); + CloseAvatarPicker(); +} + +private void CancelAvatarPicker() +{ + CloseAvatarPicker(); +} +~~~ + +RefreshAvatarPicker must set exactly one Img_Selected_XX active, copy the selected slot Image.sprite to Img_PreviewAvatar, and set Txt_PreviewHint to 已选择头像 XX. Selecting a slot must not call PlayerProfileStore.Update. + +- [ ] **Step 7: Make Close and profile events picker-safe** + +Increment avatarPickerVersion in Close, destroy avatarPickerView, clear picker references, and ensure OnProfileChanged refreshes HUD/detail only. If the profile changes while picker is open, keep pending preview until confirm or cancel. + +- [ ] **Step 8: Run EditMode tests and compile verification** + +Run the full PlayerProfileModuleTests filter, then the project EditMode suite. Expected result: catalog, JSON, store, and existing profile tests pass with no new compile errors. + +- [ ] **Step 9: Commit module behavior** + +~~~powershell +git add -- 'Client/Assets/Script/xmain/Module/Player/PlayerProfileModule.cs' 'Client/Assets/Script/Tests/EditMode/PlayerProfileModuleTests.cs' +git commit -m "feat: add player avatar selection flow" +~~~ + +## Task 7: Verify the complete Unity UI flow + +**Files:** +- Verify: Client/Assets/Game/Art/UI/Prefab/UI_PlayerProfileHud.prefab +- Verify: Client/Assets/Game/Art/UI/Prefab/UI_PlayerProfilePanel.prefab +- Verify: Client/Assets/Game/Art/UI/Prefab/UI_PlayerAvatarPicker.prefab +- Verify: Client/Assets/Script/xmain/Module/Player/PlayerProfileModule.cs + +- [ ] **Step 1: Run the complete EditMode suite** + +Run Unity EditMode tests with results written under Client/TestResults/PlayerAvatarSelection-editmode.xml. Expected result: all tests pass with no new errors or warnings attributable to the feature. + +- [ ] **Step 2: Open the running lobby/profile flow through Unity Pipeline** + +Use the project’s unity-pipeline edit→recompile→run loop, explicitly targeting the running Client Unity Editor/Player. Verify the existing HUD profile entry opens the detail panel. + +- [ ] **Step 3: Verify picker interactions manually** + +Check that 54 slots appear in six columns and nine rows; opening highlights the saved avatar; clicking changes preview/highlight only; cancel, close, and mask preserve the original avatar; confirm updates AvatarId, closes the picker, and refreshes detail/HUD; legacy AvatarId = 0 displays and selects avatar 01. + +- [ ] **Step 4: Inspect final diff and worktree** + +Run: + +~~~powershell +git diff --check +git status --short +git diff HEAD~5 --stat +~~~ + +Confirm only avatar feature files are in the feature commits and pre-existing UI asset changes remain untouched. + +## Plan self-review + +- Spec coverage: data compatibility, fixed 54-slot layout, preview/confirm/cancel semantics, async loading, error handling, HUD/detail refresh, JSON validation, EditMode tests, and Unity UI verification are covered by Tasks 1–7. +- Placeholder scan: every implementation step specifies the target file, behavior, command, or expected verification result. +- Type consistency: PlayerAvatarCatalog.Count, DefaultAvatarId, NormalizeId(int), and GetAssetPath(int) are defined in Task 2 and consumed consistently by Tasks 1, 3, 4, and 6; runtime node names used by Task 6 are declared in Task 4. +- Scope check: implementation is limited to the existing Player module, its two profile JSON files, one new picker JSON/Prefab, and focused tests.