Files
AIC-Project/docs/superpowers/plans/2026-07-27-unified-character-catalog.md

321 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Unified Character Catalog 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:** Make `CharacterConfig.json` the sole runtime source for both character selection and lobby prefab loading.
**Architecture:** Add deterministic figure-resolution helpers to `CharacterConfigData`, where a one-based figure is the configured items list position. `LobbyWorldController` loads that resource configuration, uses it to translate the persisted ID and incoming network figures, then loads the resolved `prefabPath`; it no longer owns an actor-path list.
**Tech Stack:** Unity 2022.3.62f3, C#, Unity Test Framework 1.1.33, NUnit.
## Global Constraints
- Keep the existing wire protocol unchanged: `LobbyJoinMsg.Figure` remains a one-based `int`.
- Preserve the generated alphabetical configuration order, so every client built from identical content derives identical figures.
- Do not modify character prefabs, the game server, or the users unrelated working-tree changes.
- A missing, empty, or invalid character config must log an explicit error and must not index a hard-coded fallback list.
---
### Task 1: Character configuration figure resolver
**Files:**
- Create: `Client/Assets/Script/Tests/EditMode/XWorld.Link.EditModeTests.asmdef`
- Create: `Client/Assets/Script/Tests/EditMode/CharacterConfigDataTests.cs`
- Modify: `Client/Assets/Script/xmain/Character/CharacterConfigData.cs`
**Interfaces:**
- Consumes: the ordered `CharacterConfigData.characters` list generated into `Assets/Resources/Config/CharacterConfig.json`.
- Produces: `int GetFigure(string id)`, `int GetFigure(CharacterConfigItem item)`, and `CharacterConfigItem GetByFigure(int figure)` on `CharacterConfigData`.
- [ ] **Step 1: Create the EditMode test assembly**
Create `Client/Assets/Script/Tests/EditMode/XWorld.Link.EditModeTests.asmdef`:
```json
{
"name": "XWorld.Link.EditModeTests",
"rootNamespace": "",
"references": ["XWorld.Link"],
"includePlatforms": ["Editor"],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false,
"optionalUnityReferences": ["TestAssemblies"]
}
```
- [ ] **Step 2: Write the failing catalog-mapping tests**
Create `Client/Assets/Script/Tests/EditMode/CharacterConfigDataTests.cs`:
```csharp
using System.Collections.Generic;
using NUnit.Framework;
using XGame;
public sealed class CharacterConfigDataTests
{
private static CharacterConfigData CreateConfig()
{
return new CharacterConfigData
{
defaultCharacterId = "Assasin",
characters = new List<CharacterConfigItem>
{
new CharacterConfigItem { id = "Assasin", prefabPath = "Assets/Game/Art/Actor/Prefab/Assasin.prefab" },
new CharacterConfigItem { id = "bls", prefabPath = "Assets/Game/Art/Actor/Prefab/bls.prefab" },
new CharacterConfigItem { id = "boy", prefabPath = "Assets/Game/Art/Actor/Prefab/boy.prefab" },
new CharacterConfigItem { id = "captain", prefabPath = "Assets/Game/Art/Actor/Prefab/captain.prefab" },
new CharacterConfigItem { id = "cityboy_sk", prefabPath = "Assets/Game/Art/Actor/Prefab/cityboy_sk.prefab" },
}
};
}
[Test]
public void GetFigureReturnsGeneratedListIndexForCityboy()
{
Assert.That(CreateConfig().GetFigure("cityboy_sk"), Is.EqualTo(5));
}
[Test]
public void GetFigureUsesDefaultForAnUnknownId()
{
Assert.That(CreateConfig().GetFigure("missing"), Is.EqualTo(1));
}
[Test]
public void GetByFigureReturnsTheConfiguredPrefabPath()
{
CharacterConfigItem item = CreateConfig().GetByFigure(5);
Assert.That(item.id, Is.EqualTo("cityboy_sk"));
Assert.That(item.prefabPath, Is.EqualTo("Assets/Game/Art/Actor/Prefab/cityboy_sk.prefab"));
}
[Test]
public void GetByFigureUsesDefaultForAnInvalidIndex()
{
Assert.That(CreateConfig().GetByFigure(99).id, Is.EqualTo("Assasin"));
}
[Test]
public void EmptyConfigHasNoUsableFigure()
{
CharacterConfigData config = new CharacterConfigData();
Assert.That(config.GetFigure("cityboy_sk"), Is.EqualTo(0));
Assert.That(config.GetByFigure(1), Is.Null);
}
}
```
- [ ] **Step 3: Run the test to verify RED**
Run from the Unity editor Test Runner: `Window > General > Test Runner > EditMode > Run All`.
Expected: compilation fails because `CharacterConfigData` does not yet define `GetFigure` or `GetByFigure`.
- [ ] **Step 4: Implement the minimal configuration helpers**
In `Client/Assets/Script/xmain/Character/CharacterConfigData.cs`, make `GetDefault` and `Find` null-safe and add these members to `CharacterConfigData`:
```csharp
public CharacterConfigItem GetDefault()
{
CharacterConfigItem item = Find(defaultCharacterId);
return item ?? (characters != null && characters.Count > 0 ? characters[0] : null);
}
public CharacterConfigItem Find(string id)
{
if (string.IsNullOrEmpty(id) || characters == null)
{
return null;
}
for (int i = 0; i < characters.Count; i++)
{
CharacterConfigItem item = characters[i];
if (item != null && string.Equals(item.id, id, StringComparison.OrdinalIgnoreCase))
{
return item;
}
}
return null;
}
public int GetFigure(string id)
{
return GetFigure(Find(id) ?? GetDefault());
}
public int GetFigure(CharacterConfigItem item)
{
if (item == null || characters == null)
{
return 0;
}
for (int i = 0; i < characters.Count; i++)
{
if (ReferenceEquals(characters[i], item))
{
return i + 1;
}
}
return 0;
}
public CharacterConfigItem GetByFigure(int figure)
{
int index = figure - 1;
if (characters != null && index >= 0 && index < characters.Count && characters[index] != null)
{
return characters[index];
}
return GetDefault();
}
```
- [ ] **Step 5: Run the EditMode tests to verify GREEN**
Run from the Unity editor Test Runner: `Window > General > Test Runner > EditMode > Run All`.
Expected: all five `CharacterConfigDataTests` pass.
- [ ] **Step 6: Commit the independently tested resolver**
```powershell
git add -- Client/Assets/Script/xmain/Character/CharacterConfigData.cs Client/Assets/Script/Tests/EditMode/XWorld.Link.EditModeTests.asmdef Client/Assets/Script/Tests/EditMode/CharacterConfigDataTests.cs
git commit -m "feat: resolve character figures from config"
```
### Task 2: Use the catalog to load lobby avatars
**Files:**
- Modify: `Client/Assets/Script/xmain/Client/LobbyWorldController.cs`
**Interfaces:**
- Consumes: `CharacterConfigData.GetFigure(string)` and `CharacterConfigData.GetByFigure(int)` from Task 1, plus `Resources/Config/CharacterConfig.json`.
- Produces: lobby avatar loading that resolves all selected and remote figures through configured `prefabPath` values.
- [ ] **Step 1: Replace the lobbys hard-coded paths with resource-config resolution**
In `LobbyWorldController.cs`:
1. Delete `ActorPaths` and add:
```csharp
private const string CharacterConfigResourcePath = "Config/CharacterConfig";
private CharacterConfigData characterConfig;
```
2. Add these instance methods:
```csharp
private bool EnsureCharacterConfig()
{
if (characterConfig != null)
{
return characterConfig.GetDefault() != null;
}
TextAsset asset = Resources.Load<TextAsset>(CharacterConfigResourcePath);
if (asset == null)
{
Debug.LogError("[LobbyWorldController] missing character config: Resources/" + CharacterConfigResourcePath + ".json");
return false;
}
characterConfig = JsonUtility.FromJson<CharacterConfigData>(asset.text);
if (characterConfig == null || characterConfig.GetDefault() == null)
{
Debug.LogError("[LobbyWorldController] character config is empty or invalid.");
characterConfig = null;
return false;
}
return true;
}
private int ResolveFigure(int figure)
{
if (!EnsureCharacterConfig())
{
return 0;
}
return characterConfig.GetFigure(characterConfig.GetByFigure(figure));
}
private int GetSelectedFigure()
{
if (!EnsureCharacterConfig())
{
return 0;
}
return characterConfig.GetFigure(PlayerPrefs.GetString(SelectedCharacterKey, string.Empty));
}
private string GetPrefabPath(int figure)
{
if (!EnsureCharacterConfig())
{
return null;
}
CharacterConfigItem item = characterConfig.GetByFigure(figure);
return item == null ? null : item.prefabPath;
}
```
3. In `Initialize`, call `EnsureCharacterConfig()` before creating an avatar; if it returns `false`, return without joining the lobby. Replace the existing `Mathf.Clamp(..., ActorPaths.Length)` assignment with `selfFigure = figure <= 0 ? GetSelectedFigure() : ResolveFigure(figure);` and return with an error if the result is `0`.
4. In `RefreshSelectedFigure`, replace the clamped figure assignment with `int nextFigure = GetSelectedFigure();` and return after logging if it is `0`.
5. In `GetOrCreateAvatar`, replace its clamped assignment with `figure = ResolveFigure(figure);`; if that returns `0`, log `[LobbyWorldController] cannot resolve figure because the character catalog is unavailable.` and return `null`. Update each caller to return early when the result is `null` before dereferencing the avatar.
6. In `CoLoadActorPrefab`, resolve `string prefabPath = GetPrefabPath(figure);`; log and exit if it is null or empty; otherwise call `XResLoader.coLoadRes(prefabPath, typeof(GameObject), ...)`.
7. Delete the static `GetSelectedFigure` implementation at the end of the class.
- [ ] **Step 2: Run EditMode tests and compile the client**
Run `Window > General > Test Runner > EditMode > Run All` in Unity, then allow Unity to complete script compilation without Console errors.
Expected: all `CharacterConfigDataTests` pass and no compiler errors reference `ActorPaths` or a static call to `GetSelectedFigure`.
- [ ] **Step 3: Perform the play-mode regression check**
In the Unity editor:
1. Run `Tools > Character > Generate Character Config`.
2. Enter Play mode and open the character selector.
3. Select `cityboy_sk`.
4. Join or return to the lobby.
Expected: the selected lobby avatar is `cityboy_sk`, not `Assasin`; Console contains no missing-config or failed-prefab error. Repeat once with `Assasin` to confirm existing selection still loads.
- [ ] **Step 4: Commit the lobby integration**
```powershell
git add -- Client/Assets/Script/xmain/Client/LobbyWorldController.cs Client/Assets/Script/Tests/EditMode/CharacterConfigDataTests.cs
git commit -m "fix: load lobby characters from config"
```
## Self-review
- Spec coverage: Task 1 supplies deterministic ID/figure and figure/item mapping; Task 2 removes the duplicate lobby list, loads configured prefab paths, preserves the integer protocol, and verifies `cityboy_sk` in play mode.
- Placeholder scan: no incomplete markers or unspecified error behavior remain.
- Type consistency: Task 1 defines all three `CharacterConfigData` methods used by Task 2; `ResolveFigure` and `GetSelectedFigure` return an `int`, and `GetPrefabPath` returns a `string`.