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

11 KiB
Raw Permalink Blame History

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:

{
  "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:

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:

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
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:
private const string CharacterConfigResourcePath = "Config/CharacterConfig";
private CharacterConfigData characterConfig;
  1. Add these instance methods:
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;
}
  1. 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.

  2. In RefreshSelectedFigure, replace the clamped figure assignment with int nextFigure = GetSelectedFigure(); and return after logging if it is 0.

  3. 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.

  4. In CoLoadActorPrefab, resolve string prefabPath = GetPrefabPath(figure);; log and exit if it is null or empty; otherwise call XResLoader.coLoadRes(prefabPath, typeof(GameObject), ...).

  5. 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
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.