feat(ui): wire Display GL knobs + per-toon Character key — Settings goes live
Phase L.0 polish — the Display + Character tabs were persisting to disk
but didn't yet drive runtime behavior. This commit flips the live
switches.
DISPLAY ↔ GL window:
· FOV slider (degrees) → camera FovY (radians) on Orbit + Fly + Chase,
pushed every frame so dragging is visible immediately. Brainstorm
said FOV is a live-preview slider; this delivers it.
· VSync → _window.VSync, change-detected per-frame so flipping the
checkbox is instant. Applied at startup too so saved-VSync takes
effect before the first frame.
· Resolution → _window.Size on Save (TryParseResolution parses
"WIDTHxHEIGHT"). Live preview would be too jarring; resize is on
Save only.
· Fullscreen → _window.WindowState (Silk.NET borderless mode), also
on Save only.
· ShowFps → wraps the title-bar perf string. true → full perf line;
false → just "acdream" for a cleaner alt-tab. Default true matches
pre-L.0 behavior.
Defaults rebalanced — FieldOfView 75→60° (matches Orbit/Fly/Chase
FovY = π/3), VSync true→false (matches the previous WindowOptions),
ShowFps false→true (preserves the existing perf-in-title behavior).
Net effect: a user who never opens Display tab + later opens it +
Saves without touching anything sees ZERO visual change. Tests pinned
to the new defaults.
ApplyDisplayWindowState helper consolidates the window-side
mutations. Called from the SettingsVM construction site (apply
persisted at startup) and from the onSaveDisplay callback (apply
saved on demand). Malformed resolution strings are silently ignored
to avoid crashing mid-session if settings.json gets hand-edited.
CHARACTER ↔ active toon:
· _activeToonKey field replaces the hard-coded "default" — starts as
"default" (used for any pre-login Settings interaction), gets
swapped to the actual character.Name immediately after EnterWorld
in BeginLiveSessionAsync.
· onSaveCharacter callback closes over _activeToonKey by reference
(lambda captures `this`), so saves always write to the current
toon's slot without rebinding the lambda.
· After EnterWorld lands the chosen toon's name, the host loads
that toon's bag via SettingsStore.LoadCharacter and calls a new
SettingsVM.LoadCharacterContext to swap BOTH persisted snapshot
AND draft atomically — HasUnsavedChanges stays false on login so
the user doesn't see a "pending changes" indicator just because
they switched toons.
Per-toon storage already worked at the SettingsStore layer (commit
73749d1); this commit just plumbs the actual character name through
to the toonKey instead of always using "default".
2 new tests for LoadCharacterContext: atomic persisted+draft swap,
and pending edits getting wiped on swap (so pre-login bleed-through
can't write to the new toon's slot).
dotnet build green (0 warnings); dotnet test 1,309 / 1,309 green
(243 Core.Net + 393 UI.Abstractions + 673 Core).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
73749d176a
commit
fc1e1933aa
5 changed files with 200 additions and 28 deletions
|
|
@ -11,15 +11,19 @@ namespace AcDream.UI.Abstractions.Tests.Panels.Settings;
|
|||
public sealed class DisplaySettingsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Default_values_match_brainstorm_agreement()
|
||||
public void Default_values_match_pre_L0_runtime_state()
|
||||
{
|
||||
// Defaults pinned to match the camera FovY (60° = π/3) and the
|
||||
// pre-L.0 window options (VSync off, FPS in title bar). Opening
|
||||
// Display + Save without touching anything must NOT change the
|
||||
// user's visual experience.
|
||||
var d = DisplaySettings.Default;
|
||||
Assert.Equal("1920x1080", d.Resolution);
|
||||
Assert.False(d.Fullscreen);
|
||||
Assert.True(d.VSync);
|
||||
Assert.Equal(75f, d.FieldOfView);
|
||||
Assert.False(d.VSync);
|
||||
Assert.Equal(60f, d.FieldOfView);
|
||||
Assert.Equal(1.0f, d.Gamma);
|
||||
Assert.False(d.ShowFps);
|
||||
Assert.True(d.ShowFps);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -43,8 +47,8 @@ public sealed class DisplaySettingsTests
|
|||
public void Equality_is_value_based()
|
||||
{
|
||||
var a = DisplaySettings.Default;
|
||||
var b = DisplaySettings.Default with { ShowFps = true };
|
||||
var c = DisplaySettings.Default with { ShowFps = true };
|
||||
var b = DisplaySettings.Default with { Fullscreen = true };
|
||||
var c = DisplaySettings.Default with { Fullscreen = true };
|
||||
Assert.NotEqual(a, b);
|
||||
Assert.Equal(b, c);
|
||||
}
|
||||
|
|
@ -56,7 +60,8 @@ public sealed class DisplaySettingsTests
|
|||
Assert.Equal(90f, d.FieldOfView);
|
||||
// Other fields untouched.
|
||||
Assert.Equal("1920x1080", d.Resolution);
|
||||
Assert.True(d.VSync);
|
||||
Assert.False(d.VSync);
|
||||
Assert.True(d.ShowFps);
|
||||
}
|
||||
|
||||
private static int ParseWidth(string res)
|
||||
|
|
|
|||
|
|
@ -271,7 +271,9 @@ public sealed class SettingsVMTests
|
|||
public void SetDisplay_marks_unsaved_changes()
|
||||
{
|
||||
var (vm, _, _, _, _, _, _, _, _, _) = Build();
|
||||
vm.SetDisplay(vm.DisplayDraft with { ShowFps = true });
|
||||
// Default ShowFps is true → flip to false to ensure the with-
|
||||
// expression actually mutates a field.
|
||||
vm.SetDisplay(vm.DisplayDraft with { ShowFps = false });
|
||||
Assert.True(vm.HasUnsavedChanges);
|
||||
}
|
||||
|
||||
|
|
@ -584,4 +586,39 @@ public sealed class SettingsVMTests
|
|||
Assert.Equal(CharacterSettings.Default, vm.CharacterDraft);
|
||||
Assert.True(vm.HasUnsavedChanges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadCharacterContext_swaps_persisted_and_draft_atomically()
|
||||
{
|
||||
// Simulates the post-EnterWorld toon swap — host loads the
|
||||
// chosen toon's bag from disk and pushes it via
|
||||
// LoadCharacterContext. BOTH persisted and draft must update
|
||||
// so HasUnsavedChanges stays false; otherwise the user would
|
||||
// see a "pending changes" indicator on every login.
|
||||
var (vm, _, _, _, _, _, _, _, _, _) = Build();
|
||||
var newToonBag = CharacterSettings.Default with { DefaultChatChannel = "Allegiance", AutoAttack = true };
|
||||
|
||||
vm.LoadCharacterContext(newToonBag);
|
||||
|
||||
Assert.Equal(newToonBag, vm.CharacterDraft);
|
||||
Assert.False(vm.HasUnsavedChanges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadCharacterContext_clears_pending_unsaved_character_edits()
|
||||
{
|
||||
// If the user had pending character edits from the previous
|
||||
// toon (or pre-login session), swapping to a new toon's bag
|
||||
// must wipe them — Save is per-toon, and bleed-through would
|
||||
// write the pre-login bag's edits to the new toon's slot.
|
||||
var (vm, _, _, _, _, _, _, _, _, _) = Build();
|
||||
vm.SetCharacter(vm.CharacterDraft with { AutoAttack = true });
|
||||
Assert.True(vm.HasUnsavedChanges);
|
||||
|
||||
vm.LoadCharacterContext(CharacterSettings.Default with { DefaultChatChannel = "Fellowship" });
|
||||
|
||||
Assert.Equal("Fellowship", vm.CharacterDraft.DefaultChatChannel);
|
||||
Assert.False(vm.CharacterDraft.AutoAttack);
|
||||
Assert.False(vm.HasUnsavedChanges);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue