321 lines
11 KiB
Markdown
321 lines
11 KiB
Markdown
# 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 item’s 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 user’s 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 lobby’s 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`.
|