Mounts gmCharGenMainUI (enum 0x10000039, root 0x100003CC) via CharacterCreationUiController/CharacterCreationUiMountCoordinator, cloning CharacterManagementUiController's recipe. Master shell ports SetProgressState @0x004e7a10 (Olthoi tab-hide + redirect) and ListenToElementMessage @0x004e9450 (Back/Next/Finish/Help/Exit/Random nav) verbatim, with free tab navigation over all six pages. Heritage, Profession, Skills, and Town pages bind to CC3's RuntimeCharacterCreationState commands; Appearance and Summary mount as content-inert placeholders for CC6b/CC5. Live-DAT probing (CharacterCreationLiveDatTests) found two widget- mapping surprises the decomp's DynamicCast hints don't predict: the Profession slider's value field imports as an editable UiField (wired for direct numeric entry), and the avail/health/stamina/mana/credits displays author as UIElement_Button hosts whose Type-12 value child is swallowed by UiButton.ConsumesDatChildren — substituted with the button's own Label. No new DatWidgetFactory widget types were needed. Threads the installed DAT's real ChargenOptions into Runtime via the new RuntimeCharacterCreationState.InstallOptions, called from ContentEffectsAudioCompositionPhase.Compose (mirrors InstallSpellMetadata's pattern); headless keeps ChargenOptions.Empty unchanged. Wires CC3's F14 status-hook gap (ApplyCharacterCreated/ ApplyCreationFailed) to SessionStatusWriter for both graphical and headless hosts, and adds the CharacterCreation view/command seam through CurrentGameRuntimeAdapter and DeferredGameRuntimeStateCommands alongside CharacterSelection's existing shape. Register: AD-101/102/103, AP-212/213, TS-82 filed for the auto-gender- select interim default, the omitted ToD-account gate, the button-Label widget substitution, the Random-button approximation, the flat-listbox Skills simplification, and the Appearance/Summary placeholders. Runtime 1713/0 (was 1707), App 5117/13 skips (was 5101/6), Headless 165/0 unaffected, full solution Release build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
817 lines
31 KiB
C#
817 lines
31 KiB
C#
using System.Numerics;
|
|
using AcDream.App.UI;
|
|
using AcDream.App.UI.Layout;
|
|
using AcDream.Core.CharGen;
|
|
using AcDream.Runtime;
|
|
using AcDream.Runtime.Session;
|
|
|
|
namespace AcDream.App.Tests.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Campaign CC slice CC4 — controller binding tests for the character-
|
|
/// creation master shell + Heritage/Profession/Skills/Town pages, using a
|
|
/// hand-built layout fixture (no installed DAT — see
|
|
/// <see cref="CharacterCreationLiveDatTests"/> for the live-DAT id/type
|
|
/// sweep this pairs with). Mirrors <c>CharacterManagementUiControllerTests</c>'
|
|
/// fixture pattern.
|
|
/// </summary>
|
|
public sealed class CharacterCreationUiControllerTests
|
|
{
|
|
private const uint AluvianId = 1u;
|
|
private const uint OlthoiId = (uint)ChargenHeritageGroup.Olthoi;
|
|
private const uint GenderKey = 1u;
|
|
private const uint SkillTrainOnly = 1u;
|
|
private const uint SkillSpecializable = 2u;
|
|
|
|
[Fact]
|
|
public void ActiveScreen_KeepsAuthoredRootExtent_AndDefaultsToTheHeritagePage()
|
|
{
|
|
using var environment = new EnvironmentHarness();
|
|
Assert.False(environment.Controller.Root.Visible);
|
|
|
|
environment.Controller.Open();
|
|
|
|
Assert.True(environment.Controller.Root.Visible);
|
|
Assert.Equal(800f, environment.Controller.Root.Width);
|
|
Assert.Equal(600f, environment.Controller.Root.Height);
|
|
Assert.True(environment.Page(
|
|
CharacterCreationUiController.HeritagePageElementId).Visible);
|
|
Assert.False(environment.Page(
|
|
CharacterCreationUiController.ProfessionPageElementId).Visible);
|
|
Assert.True(environment.TabButton(
|
|
CharacterCreationUiController.HeritageTabElementId).Selected);
|
|
}
|
|
|
|
[Fact]
|
|
public void TabClick_SwitchesToTheClickedPage_FreeOfValidation()
|
|
{
|
|
using var environment = new EnvironmentHarness();
|
|
environment.Controller.Open();
|
|
|
|
// Free navigation: jumping straight to Town from Heritage with
|
|
// nothing selected must still work (gmCharGenMainUI's tab dispatch
|
|
// is not gated — see ApplyProgressState's doc).
|
|
environment.TabButton(CharacterCreationUiController.TownTabElementId)
|
|
.OnClick!();
|
|
|
|
Assert.True(environment.Page(
|
|
CharacterCreationUiController.TownPageElementId).Visible);
|
|
Assert.False(environment.Page(
|
|
CharacterCreationUiController.HeritagePageElementId).Visible);
|
|
Assert.True(environment.TabButton(
|
|
CharacterCreationUiController.TownTabElementId).Selected);
|
|
}
|
|
|
|
[Fact]
|
|
public void Next_AdvancesOnePageAtATime_AndBackReturns()
|
|
{
|
|
using var environment = new EnvironmentHarness();
|
|
environment.Controller.Open();
|
|
|
|
environment.Button(CharacterCreationUiController.NextElementId).OnClick!();
|
|
Assert.True(environment.Page(
|
|
CharacterCreationUiController.ProfessionPageElementId).Visible);
|
|
|
|
environment.Button(CharacterCreationUiController.BackElementId).OnClick!();
|
|
Assert.True(environment.Page(
|
|
CharacterCreationUiController.HeritagePageElementId).Visible);
|
|
}
|
|
|
|
[Fact]
|
|
public void Next_AtSummary_IsANoOp()
|
|
{
|
|
using var environment = new EnvironmentHarness();
|
|
environment.Controller.Open();
|
|
environment.TabButton(CharacterCreationUiController.SummaryTabElementId)
|
|
.OnClick!();
|
|
Assert.True(environment.Page(
|
|
CharacterCreationUiController.SummaryPageElementId).Visible);
|
|
|
|
environment.Button(CharacterCreationUiController.NextElementId).OnClick!();
|
|
|
|
Assert.True(environment.Page(
|
|
CharacterCreationUiController.SummaryPageElementId).Visible);
|
|
}
|
|
|
|
[Fact]
|
|
public void Finish_StaysGhosted_NoOnClickHandler()
|
|
{
|
|
using var environment = new EnvironmentHarness();
|
|
environment.Controller.Open();
|
|
UiButton finish = environment.Button(CharacterCreationUiController.FinishElementId);
|
|
Assert.Null(finish.OnClick);
|
|
Assert.False(finish.Enabled);
|
|
}
|
|
|
|
[Fact]
|
|
public void Random_IsDisabledOnSkillsAppearanceAndSummaryPages()
|
|
{
|
|
using var environment = new EnvironmentHarness();
|
|
environment.Controller.Open();
|
|
UiButton random = environment.Button(CharacterCreationUiController.RandomElementId);
|
|
Assert.True(random.Enabled);
|
|
|
|
environment.TabButton(CharacterCreationUiController.SkillsTabElementId).OnClick!();
|
|
Assert.False(random.Enabled);
|
|
|
|
environment.TabButton(CharacterCreationUiController.AppearanceTabElementId).OnClick!();
|
|
Assert.False(random.Enabled);
|
|
|
|
environment.TabButton(CharacterCreationUiController.SummaryTabElementId).OnClick!();
|
|
Assert.False(random.Enabled);
|
|
|
|
environment.TabButton(CharacterCreationUiController.TownTabElementId).OnClick!();
|
|
Assert.True(random.Enabled);
|
|
}
|
|
|
|
/// <summary>gmCharGenMainUI::ListenToElementMessage @ 0x004e9450's
|
|
/// element 0x100003c6 case: at Heritage (the first page), Back opens
|
|
/// the exit confirmation instead of moving pages.</summary>
|
|
[Fact]
|
|
public void Back_AtHeritage_OpensExitConfirmation()
|
|
{
|
|
using var environment = new EnvironmentHarness();
|
|
environment.Controller.Open();
|
|
|
|
environment.Button(CharacterCreationUiController.BackElementId).OnClick!();
|
|
|
|
Assert.True(environment.Dialogs.IsOpen);
|
|
}
|
|
|
|
[Fact]
|
|
public void Exit_Confirm_ClosesTheScreenAndCallsRequestExit()
|
|
{
|
|
using var environment = new EnvironmentHarness();
|
|
environment.Controller.Open();
|
|
|
|
environment.Button(CharacterCreationUiController.ExitElementId).OnClick!();
|
|
Assert.True(environment.Dialogs.IsOpen);
|
|
|
|
environment.ConfirmActiveDialog(confirmed: true);
|
|
|
|
Assert.False(environment.Controller.Root.Visible);
|
|
Assert.Equal(1, environment.Runtime.RequestExitCalls);
|
|
}
|
|
|
|
[Fact]
|
|
public void Exit_Cancel_LeavesTheScreenOpen()
|
|
{
|
|
using var environment = new EnvironmentHarness();
|
|
environment.Controller.Open();
|
|
|
|
environment.Button(CharacterCreationUiController.ExitElementId).OnClick!();
|
|
environment.ConfirmActiveDialog(confirmed: false);
|
|
|
|
Assert.True(environment.Controller.Root.Visible);
|
|
Assert.Equal(0, environment.Runtime.RequestExitCalls);
|
|
}
|
|
|
|
/// <summary>gmCGHeritagePage::ListenToElementMessage @ 0x00483860's
|
|
/// per-button SetHeritageGroup literal, plus CC4's interim
|
|
/// auto-gender-select seam (register AD-100).</summary>
|
|
[Fact]
|
|
public void HeritageButton_SelectsHeritage_AndAutoSelectsFirstGender()
|
|
{
|
|
using var environment = new EnvironmentHarness();
|
|
environment.Controller.Open();
|
|
|
|
environment.Button(0x100003BFu).OnClick!(); // Aluvian
|
|
|
|
Assert.Equal(AluvianId, environment.Runtime.LastSelectedHeritage);
|
|
Assert.Equal(GenderKey, environment.Runtime.LastSelectedGender);
|
|
}
|
|
|
|
/// <summary>gmCharGenMainUI::SetProgressState @ 0x004e7a10's Olthoi
|
|
/// branch: Profession/Skills/Town tabs hide, and paging past the
|
|
/// hidden range redirects to Appearance/Summary.</summary>
|
|
[Fact]
|
|
public void OlthoiHeritage_HidesProfessionSkillsAndTownTabs()
|
|
{
|
|
using var environment = new EnvironmentHarness();
|
|
environment.Controller.Open();
|
|
environment.Runtime.SelectHeritageDirect(OlthoiId);
|
|
|
|
environment.TabButton(CharacterCreationUiController.HeritageTabElementId)
|
|
.OnClick!();
|
|
|
|
Assert.False(environment.TabButton(
|
|
CharacterCreationUiController.ProfessionTabElementId).Visible);
|
|
Assert.False(environment.TabButton(
|
|
CharacterCreationUiController.SkillsTabElementId).Visible);
|
|
Assert.False(environment.TabButton(
|
|
CharacterCreationUiController.TownTabElementId).Visible);
|
|
|
|
// Next from Heritage would normally land on Profession; for an
|
|
// Olthoi heritage it must redirect straight to Appearance.
|
|
environment.Button(CharacterCreationUiController.NextElementId).OnClick!();
|
|
Assert.True(environment.Page(
|
|
CharacterCreationUiController.AppearancePageElementId).Visible);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProfessionTemplateButton_SelectsTemplate()
|
|
{
|
|
using var environment = new EnvironmentHarness();
|
|
environment.Controller.Open();
|
|
environment.Runtime.SelectHeritageDirect(AluvianId);
|
|
environment.TabButton(CharacterCreationUiController.ProfessionTabElementId)
|
|
.OnClick!();
|
|
|
|
environment.Button(0x100003DAu).OnClick!(); // Bow Hunter = template index 1
|
|
|
|
Assert.Equal(1u, environment.Runtime.LastSelectedTemplate);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProfessionSlider_ScalarChange_SetsTheAttribute()
|
|
{
|
|
using var environment = new EnvironmentHarness();
|
|
environment.Controller.Open();
|
|
environment.Runtime.SelectHeritageDirect(AluvianId);
|
|
environment.TabButton(CharacterCreationUiController.ProfessionTabElementId)
|
|
.OnClick!();
|
|
|
|
UiElement strengthContainer = Assert.IsAssignableFrom<UiElement>(
|
|
environment.Screen.FindElement(0x100003E6u));
|
|
var slider = Assert.IsType<UiScrollbar>(
|
|
UiElement.FindDescendant(strengthContainer, 0x100002EEu));
|
|
|
|
slider.ScalarChanged!(1f); // top of the [10,100] range
|
|
|
|
Assert.Equal(ChargenAttributeId.Strength, environment.Runtime.LastAttributeSet);
|
|
Assert.Equal(100, environment.Runtime.LastAttributeValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProfessionValueField_DirectEntry_SetsTheAttribute()
|
|
{
|
|
using var environment = new EnvironmentHarness();
|
|
environment.Controller.Open();
|
|
environment.Runtime.SelectHeritageDirect(AluvianId);
|
|
environment.TabButton(CharacterCreationUiController.ProfessionTabElementId)
|
|
.OnClick!();
|
|
|
|
UiElement strengthContainer = Assert.IsAssignableFrom<UiElement>(
|
|
environment.Screen.FindElement(0x100003E6u));
|
|
var field = Assert.IsType<UiField>(
|
|
UiElement.FindDescendant(strengthContainer, 0x100002EFu));
|
|
|
|
field.OnSubmit!("42");
|
|
|
|
Assert.Equal(ChargenAttributeId.Strength, environment.Runtime.LastAttributeSet);
|
|
Assert.Equal(42, environment.Runtime.LastAttributeValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void SkillsRow_Click_TrainsThenSpecializes()
|
|
{
|
|
using var environment = new EnvironmentHarness();
|
|
environment.Controller.Open();
|
|
environment.Runtime.SelectHeritageDirect(AluvianId);
|
|
environment.TabButton(CharacterCreationUiController.SkillsTabElementId)
|
|
.OnClick!();
|
|
|
|
// Rows are built in ascending skill-id order (RebuildRows' 1..54
|
|
// walk over IsCostable ids) — SkillSpecializable's row is identified
|
|
// by its label prefix (FormatSkillLabel's "{name}: ..." shape)
|
|
// rather than instance identity, since the controller owns the
|
|
// row->skillId map privately.
|
|
string skillName = ItemAppraisalTextFormatter.SkillName((int)SkillSpecializable);
|
|
UiButton row = environment.SkillsList().ViewportForTest!.Children
|
|
.OfType<UiButton>()
|
|
.Single(candidate => candidate.Label!.StartsWith(
|
|
skillName + ":", StringComparison.Ordinal));
|
|
|
|
row.OnClick!();
|
|
Assert.Equal(ChargenSkillAdvancementClass.Trained,
|
|
environment.Runtime.GetSkillLevel(SkillSpecializable));
|
|
|
|
row.OnClick!();
|
|
Assert.Equal(ChargenSkillAdvancementClass.Specialized,
|
|
environment.Runtime.GetSkillLevel(SkillSpecializable));
|
|
|
|
row.OnDoubleClick!();
|
|
Assert.Equal(ChargenSkillAdvancementClass.Trained,
|
|
environment.Runtime.GetSkillLevel(SkillSpecializable));
|
|
}
|
|
|
|
[Fact]
|
|
public void TownButton_SelectsTheLiteralStartAreaIndex()
|
|
{
|
|
using var environment = new EnvironmentHarness();
|
|
environment.Controller.Open();
|
|
environment.TabButton(CharacterCreationUiController.TownTabElementId)
|
|
.OnClick!();
|
|
|
|
// Holtburg (0x1000040d) -> startArea 0 per SetTown's literal map.
|
|
environment.Button(0x1000040Du).OnClick!();
|
|
Assert.Equal(0, environment.Runtime.LastSelectedStartArea);
|
|
|
|
// Yaraq (0x1000040e) -> startArea 2.
|
|
environment.Button(0x1000040Eu).OnClick!();
|
|
Assert.Equal(2, environment.Runtime.LastSelectedStartArea);
|
|
}
|
|
|
|
private static IEnumerable<UiElement> Descendants(UiElement root)
|
|
{
|
|
yield return root;
|
|
foreach (UiElement child in root.Children)
|
|
foreach (UiElement descendant in Descendants(child))
|
|
yield return descendant;
|
|
}
|
|
|
|
// ── Fixture ──────────────────────────────────────────────────────────
|
|
|
|
private sealed class EnvironmentHarness : IDisposable
|
|
{
|
|
private readonly List<ImportedLayout> _dialogLayouts = [];
|
|
|
|
public EnvironmentHarness()
|
|
{
|
|
Host = new UiRoot { Width = 800f, Height = 600f };
|
|
Screen = BuildScreen();
|
|
Runtime = new FakeRuntime();
|
|
Dialogs = new RetailDialogFactory(Host, type =>
|
|
{
|
|
ImportedLayout layout = RetailDialogFactoryTests.BuildDialogLayout(type);
|
|
_dialogLayouts.Add(layout);
|
|
return layout;
|
|
});
|
|
Controller = Assert.IsType<CharacterCreationUiController>(
|
|
CharacterCreationUiController.CreateDetached(
|
|
Host,
|
|
Screen,
|
|
ResolveSkillRowTemplate,
|
|
Dialogs,
|
|
Runtime.Bindings,
|
|
new CharacterCreationUiController.DialogStrings(
|
|
"Are you sure you want to leave?")));
|
|
Controller.AttachAndTick();
|
|
}
|
|
|
|
public UiRoot Host { get; }
|
|
public ImportedLayout Screen { get; }
|
|
public FakeRuntime Runtime { get; }
|
|
public RetailDialogFactory Dialogs { get; }
|
|
public CharacterCreationUiController Controller { get; }
|
|
|
|
public UiButton Button(uint id) =>
|
|
Assert.IsType<UiButton>(Screen.FindElement(id));
|
|
|
|
public UiButton TabButton(uint id) => Button(id);
|
|
|
|
public UiElement Page(uint id) =>
|
|
Assert.IsAssignableFrom<UiElement>(Screen.FindElement(id));
|
|
|
|
public UiTemplateListBox SkillsList() =>
|
|
Assert.IsType<UiTemplateListBox>(Screen.FindElement(0x100003F7u));
|
|
|
|
/// <summary>Confirms or cancels the MOST RECENTLY opened confirmation
|
|
/// dialog, using <see cref="RetailConfirmationDialogView"/>'s real
|
|
/// button ids off the layout the factory's <c>createLayout</c>
|
|
/// callback actually returned — same lookup shape
|
|
/// <c>CharacterManagementUiControllerTests</c> uses.</summary>
|
|
public void ConfirmActiveDialog(bool confirmed)
|
|
{
|
|
ImportedLayout dialog = _dialogLayouts[^1];
|
|
uint buttonId = confirmed
|
|
? RetailConfirmationDialogView.AcceptButtonId
|
|
: RetailConfirmationDialogView.RejectButtonId;
|
|
UiButton button = Assert.IsType<UiButton>(dialog.FindElement(buttonId));
|
|
button.OnClick!();
|
|
}
|
|
|
|
private static UiElement? ResolveSkillRowTemplate(
|
|
uint templateLayoutId,
|
|
uint templateElementId) =>
|
|
BuildSkillRowTemplate(templateElementId);
|
|
|
|
public void Dispose()
|
|
{
|
|
Controller.Dispose();
|
|
Dialogs.Dispose();
|
|
}
|
|
}
|
|
|
|
private sealed class FakeRuntime
|
|
{
|
|
private static readonly RuntimeGenerationToken Generation = new(3u);
|
|
|
|
public FakeRuntime()
|
|
{
|
|
View = new FakeView(BuildOptions());
|
|
Bindings = new CharacterCreationRuntimeBindings(
|
|
() => ProvideView ? View : null,
|
|
SelectHeritage,
|
|
SelectGender,
|
|
SelectTemplate,
|
|
SetAttribute,
|
|
(_, _) => Result(RuntimeCommandStatus.Accepted),
|
|
skillId => SetSkillLevel(skillId, ChargenSkillAdvancementClass.Trained),
|
|
skillId => SetSkillLevel(skillId, ChargenSkillAdvancementClass.Specialized),
|
|
skillId => SetSkillLevel(skillId, ChargenSkillAdvancementClass.Untrained),
|
|
SelectStartArea,
|
|
_ => Result(RuntimeCommandStatus.Accepted),
|
|
() => RequestExitCalls++,
|
|
ResolveText: _ => null,
|
|
OpenOnStart: false);
|
|
}
|
|
|
|
public FakeView View { get; }
|
|
public CharacterCreationRuntimeBindings Bindings { get; }
|
|
public bool ProvideView { get; set; } = true;
|
|
public int RequestExitCalls { get; private set; }
|
|
public uint LastSelectedHeritage { get; private set; }
|
|
public uint LastSelectedGender { get; private set; }
|
|
public uint LastSelectedTemplate { get; private set; }
|
|
public ChargenAttributeId LastAttributeSet { get; private set; }
|
|
public int LastAttributeValue { get; private set; }
|
|
public int LastSelectedStartArea { get; private set; } = -1;
|
|
|
|
public void SelectHeritageDirect(uint heritageId) => SelectHeritage(heritageId);
|
|
|
|
public ChargenSkillAdvancementClass GetSkillLevel(uint skillId) =>
|
|
View.GetSkillLevel(skillId);
|
|
|
|
private RuntimeCommandResult SelectHeritage(uint heritageId)
|
|
{
|
|
LastSelectedHeritage = heritageId;
|
|
View.Snapshot = View.Snapshot with { HeritageId = heritageId };
|
|
return Result(RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
private RuntimeCommandResult SelectGender(uint genderKey)
|
|
{
|
|
LastSelectedGender = genderKey;
|
|
View.Snapshot = View.Snapshot with { GenderKey = genderKey };
|
|
return Result(RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
private RuntimeCommandResult SelectTemplate(uint templateIndex)
|
|
{
|
|
LastSelectedTemplate = templateIndex;
|
|
View.Snapshot = View.Snapshot with { Template = templateIndex };
|
|
return Result(RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
private RuntimeCommandResult SetAttribute(ChargenAttributeId attribute, int value)
|
|
{
|
|
LastAttributeSet = attribute;
|
|
LastAttributeValue = value;
|
|
return Result(RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
private RuntimeCommandResult SetSkillLevel(
|
|
uint skillId,
|
|
ChargenSkillAdvancementClass targetClass)
|
|
{
|
|
View.SetSkillLevel(skillId, targetClass);
|
|
return Result(RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
private RuntimeCommandResult SelectStartArea(int startAreaIndex)
|
|
{
|
|
LastSelectedStartArea = startAreaIndex;
|
|
View.Snapshot = View.Snapshot with { StartArea = startAreaIndex };
|
|
return Result(RuntimeCommandStatus.Accepted);
|
|
}
|
|
|
|
private static RuntimeCommandResult Result(RuntimeCommandStatus status) =>
|
|
new(status, Generation);
|
|
|
|
private static ChargenOptions BuildOptions()
|
|
{
|
|
var gender = new ChargenGenderOptions(
|
|
GenderKey: (int)GenderKey,
|
|
Name: "Male",
|
|
Scale: 1u,
|
|
SetupId: 0x2000054u,
|
|
SoundTableId: 0u,
|
|
IconId: 0u,
|
|
BasePaletteId: 0u,
|
|
SkinPalSetId: 0u,
|
|
PhysicsTableId: 0u,
|
|
MotionTableId: 0u,
|
|
CombatTableId: 0u,
|
|
BaseObjDesc: ChargenObjDesc.Empty,
|
|
HairColors: [],
|
|
HairStyles: [],
|
|
EyeColors: [],
|
|
EyeStrips: [],
|
|
NoseStrips: [],
|
|
MouthStrips: [],
|
|
Headgears: [],
|
|
Shirts: [],
|
|
Pants: [],
|
|
Footwear: [],
|
|
ClothingColors: []);
|
|
|
|
var templates = new List<ChargenTemplate>
|
|
{
|
|
new(
|
|
"Custom",
|
|
IconId: 0u,
|
|
TitleStringId: 0u,
|
|
Attributes: new ChargenAttributeValues(10, 10, 10, 10, 10, 10),
|
|
NormalSkills: [],
|
|
PrimarySkills: []),
|
|
new(
|
|
"Bow Hunter",
|
|
IconId: 0u,
|
|
TitleStringId: 0u,
|
|
Attributes: new ChargenAttributeValues(16, 10, 10, 10, 10, 10),
|
|
NormalSkills: [SkillTrainOnly],
|
|
PrimarySkills: []),
|
|
};
|
|
|
|
var skillCosts = new Dictionary<uint, ChargenSkillCost>
|
|
{
|
|
[SkillTrainOnly] = new(SkillTrainOnly, NormalCost: 2, PrimaryCost: 6),
|
|
[SkillSpecializable] = new(SkillSpecializable, NormalCost: 2, PrimaryCost: 6),
|
|
};
|
|
|
|
var aluvian = new ChargenHeritageOptions(
|
|
AluvianId,
|
|
"Aluvian",
|
|
IconId: 0u,
|
|
SetupId: 0x2000054u,
|
|
EnvironmentSetupId: 0u,
|
|
AttributeCredits: 66u,
|
|
SkillCredits: 50u,
|
|
PrimaryStartAreaIndices: [0, 1],
|
|
SecondaryStartAreaIndices: [],
|
|
SkillCostsBySkillId: skillCosts,
|
|
Templates: templates,
|
|
GendersByKey: new Dictionary<int, ChargenGenderOptions> { [(int)GenderKey] = gender });
|
|
|
|
var olthoi = new ChargenHeritageOptions(
|
|
OlthoiId,
|
|
"Olthoi",
|
|
IconId: 0u,
|
|
SetupId: 0x2000054u,
|
|
EnvironmentSetupId: 0u,
|
|
AttributeCredits: 60u,
|
|
SkillCredits: 0u,
|
|
PrimaryStartAreaIndices: [0],
|
|
SecondaryStartAreaIndices: [],
|
|
SkillCostsBySkillId: new Dictionary<uint, ChargenSkillCost>(),
|
|
Templates:
|
|
[
|
|
new ChargenTemplate(
|
|
"Custom",
|
|
IconId: 0u,
|
|
TitleStringId: 0u,
|
|
Attributes: new ChargenAttributeValues(10, 10, 10, 10, 10, 10),
|
|
NormalSkills: [],
|
|
PrimarySkills: []),
|
|
],
|
|
GendersByKey: new Dictionary<int, ChargenGenderOptions> { [(int)GenderKey] = gender });
|
|
|
|
var starterAreas = new List<ChargenStarterArea>
|
|
{
|
|
new(0, "Holtburg", [new ChargenPosition(1u, Vector3.Zero, Quaternion.Identity)]),
|
|
new(1, "Shoushi", [new ChargenPosition(2u, Vector3.Zero, Quaternion.Identity)]),
|
|
new(2, "Yaraq", [new ChargenPosition(3u, Vector3.Zero, Quaternion.Identity)]),
|
|
new(3, "Sanamar", [new ChargenPosition(4u, Vector3.Zero, Quaternion.Identity)]),
|
|
};
|
|
|
|
return new ChargenOptions(
|
|
starterAreas,
|
|
new Dictionary<uint, ChargenHeritageOptions>
|
|
{
|
|
[AluvianId] = aluvian,
|
|
[OlthoiId] = olthoi,
|
|
},
|
|
new Dictionary<uint, ChargenSkillCost>());
|
|
}
|
|
}
|
|
|
|
private sealed class FakeView(ChargenOptions options) : IRuntimeCharacterCreationView
|
|
{
|
|
private readonly Dictionary<uint, ChargenSkillAdvancementClass> _skillLevels = [];
|
|
|
|
public RuntimeCharacterCreationSnapshot Snapshot { get; set; } =
|
|
new(
|
|
new RuntimeGenerationToken(3u),
|
|
IsActive: true,
|
|
Revision: 1,
|
|
HeritageId: 0u,
|
|
GenderKey: 0u,
|
|
Appearance: RuntimeCharacterCreationAppearance.Default,
|
|
Template: RuntimeCharacterCreationSnapshot.TemplateUnset,
|
|
Attributes: default,
|
|
AttributeLockMask: 0u,
|
|
TotalAttributeCredits: 66u,
|
|
RemainingAttributeCredits: 66,
|
|
TotalSkillCredits: 50u,
|
|
RemainingSkillCredits: 50,
|
|
Name: string.Empty,
|
|
StartArea: -1,
|
|
Slot: 0u,
|
|
VerificationPending: false,
|
|
LastLocalRefusal: default,
|
|
LastRejection: null,
|
|
LastCreated: null);
|
|
|
|
public ChargenOptions Options { get; } = options;
|
|
|
|
public ChargenSkillAdvancementClass GetSkillLevel(uint skillId) =>
|
|
_skillLevels.TryGetValue(skillId, out ChargenSkillAdvancementClass level)
|
|
? level
|
|
: ChargenSkillAdvancementClass.Inactive;
|
|
|
|
public void SetSkillLevel(uint skillId, ChargenSkillAdvancementClass level) =>
|
|
_skillLevels[skillId] = level;
|
|
|
|
public IDisposable Subscribe(IRuntimeCharacterCreationObserver observer) =>
|
|
NullSubscription.Instance;
|
|
|
|
private sealed class NullSubscription : IDisposable
|
|
{
|
|
public static readonly NullSubscription Instance = new();
|
|
public void Dispose() { }
|
|
}
|
|
}
|
|
|
|
private static ImportedLayout BuildScreen()
|
|
{
|
|
var root = new ElementInfo
|
|
{
|
|
Id = CharacterCreationUiController.RootElementId,
|
|
Type = 3u,
|
|
Width = 800f,
|
|
Height = 600f,
|
|
};
|
|
|
|
root.Children.Add(ContainerInfo(CharacterCreationUiController.ProgressBarElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.BackElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.NextElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.FinishElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.HelpElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.ExitElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.RandomElementId));
|
|
root.Children.Add(ContainerInfo(CharacterCreationUiController.MasterPageElementId));
|
|
|
|
root.Children.Add(BuildHeritagePage());
|
|
root.Children.Add(BuildProfessionPage());
|
|
root.Children.Add(BuildSkillsPage());
|
|
root.Children.Add(ContainerInfo(CharacterCreationUiController.AppearancePageElementId));
|
|
root.Children.Add(BuildTownPage());
|
|
root.Children.Add(ContainerInfo(CharacterCreationUiController.SummaryPageElementId));
|
|
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.HeritageTabElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.ProfessionTabElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.SkillsTabElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.AppearanceTabElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.TownTabElementId));
|
|
root.Children.Add(ButtonInfo(CharacterCreationUiController.SummaryTabElementId));
|
|
|
|
return LayoutImporter.Build(root, _ => (0u, 0, 0), null);
|
|
}
|
|
|
|
private static ElementInfo BuildHeritagePage()
|
|
{
|
|
var page = new ElementInfo
|
|
{
|
|
Id = CharacterCreationUiController.HeritagePageElementId,
|
|
Type = 3u,
|
|
Width = 800f,
|
|
Height = 500f,
|
|
};
|
|
page.Children.Add(ButtonInfo(0x100003BFu)); // Aluvian
|
|
page.Children.Add(ButtonInfo(0x100005C7u)); // Olthoi
|
|
page.Children.Add(TextInfo(0x100003C4u));
|
|
return page;
|
|
}
|
|
|
|
private static ElementInfo BuildProfessionPage()
|
|
{
|
|
var page = new ElementInfo
|
|
{
|
|
Id = CharacterCreationUiController.ProfessionPageElementId,
|
|
Type = 3u,
|
|
Width = 800f,
|
|
Height = 500f,
|
|
};
|
|
page.Children.Add(ButtonInfo(0x100003D9u)); // Custom
|
|
page.Children.Add(ButtonInfo(0x100003DAu)); // Bow Hunter
|
|
|
|
var strengthSlider = ContainerInfo(0x100003E6u);
|
|
strengthSlider.Children.Add(ButtonInfo(0x100002ECu));
|
|
strengthSlider.Children.Add(ScrollbarInfo(0x100002EEu));
|
|
strengthSlider.Children.Add(EditableFieldInfo(0x100002EFu));
|
|
page.Children.Add(strengthSlider);
|
|
|
|
page.Children.Add(ButtonInfo(0x100003E2u)); // Available (consumed-child badge)
|
|
page.Children.Add(ButtonInfo(0x100003E3u)); // Health
|
|
page.Children.Add(ButtonInfo(0x100003E4u)); // Stamina
|
|
page.Children.Add(ButtonInfo(0x100003E5u)); // Mana
|
|
return page;
|
|
}
|
|
|
|
private static ElementInfo BuildSkillsPage()
|
|
{
|
|
var page = new ElementInfo
|
|
{
|
|
Id = CharacterCreationUiController.SkillsPageElementId,
|
|
Type = 3u,
|
|
Width = 800f,
|
|
Height = 500f,
|
|
};
|
|
var list = new ElementInfo
|
|
{
|
|
Id = 0x100003F7u,
|
|
Type = 5u,
|
|
X = 20f,
|
|
Y = 40f,
|
|
Width = 300f,
|
|
Height = 320f,
|
|
};
|
|
list.TemplateList.Add(new UiTemplateListEntry(0x21000038u, 0x100003FEu));
|
|
page.Children.Add(list);
|
|
page.Children.Add(ButtonInfo(0x100003F9u)); // credits badge
|
|
page.Children.Add(TextInfo(0x100003FBu));
|
|
page.Children.Add(TextInfo(0x100003FCu));
|
|
return page;
|
|
}
|
|
|
|
private static ElementInfo BuildTownPage()
|
|
{
|
|
var page = new ElementInfo
|
|
{
|
|
Id = CharacterCreationUiController.TownPageElementId,
|
|
Type = 3u,
|
|
Width = 800f,
|
|
Height = 500f,
|
|
};
|
|
page.Children.Add(ButtonInfo(0x1000040Bu)); // Sanamar
|
|
page.Children.Add(ButtonInfo(0x1000040Du)); // Holtburg
|
|
page.Children.Add(ButtonInfo(0x1000040Eu)); // Yaraq
|
|
page.Children.Add(ButtonInfo(0x1000040Fu)); // Shoushi
|
|
page.Children.Add(TextInfo(0x10000409u));
|
|
return page;
|
|
}
|
|
|
|
private static UiElement BuildSkillRowTemplate(uint templateElementId) =>
|
|
LayoutImporter.Build(
|
|
new ElementInfo
|
|
{
|
|
Id = templateElementId,
|
|
Type = 1u,
|
|
Width = 280f,
|
|
Height = 16f,
|
|
},
|
|
_ => (0u, 0, 0),
|
|
null).Root;
|
|
|
|
private static ElementInfo ContainerInfo(uint id) => new()
|
|
{
|
|
Id = id,
|
|
Type = 3u,
|
|
Width = 200f,
|
|
Height = 60f,
|
|
};
|
|
|
|
private static ElementInfo ButtonInfo(uint id) => new()
|
|
{
|
|
Id = id,
|
|
Type = 1u,
|
|
Width = 100f,
|
|
Height = 30f,
|
|
};
|
|
|
|
private static ElementInfo TextInfo(uint id) => new()
|
|
{
|
|
Id = id,
|
|
Type = 12u,
|
|
Width = 200f,
|
|
Height = 60f,
|
|
};
|
|
|
|
private static ElementInfo ScrollbarInfo(uint id) => new()
|
|
{
|
|
Id = id,
|
|
Type = 11u,
|
|
Width = 120f,
|
|
Height = 12f,
|
|
};
|
|
|
|
private static ElementInfo EditableFieldInfo(uint id)
|
|
{
|
|
var info = new ElementInfo
|
|
{
|
|
Id = id,
|
|
Type = 12u,
|
|
Width = 40f,
|
|
Height = 16f,
|
|
};
|
|
var state = new UiStateInfo { Id = UiStateInfo.DirectStateId };
|
|
state.Properties.Values[0x16u] = new UiPropertyValue
|
|
{
|
|
Kind = UiPropertyKind.Bool,
|
|
BoolValue = true,
|
|
};
|
|
info.States[UiStateInfo.DirectStateId] = state;
|
|
return info;
|
|
}
|
|
}
|