fix #208: keep combat bar hidden in peace mode
Treat combat-window visibility as gameplay-state owned so character layout restoration cannot replay a stale visible bit. Reset combat state to NonCombat at character-session entry, matching retail ClientCombatSystem::Begin. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
00fe993f6f
commit
8e9c538519
8 changed files with 109 additions and 14 deletions
|
|
@ -46,9 +46,33 @@ Copy this block when adding a new issue:
|
|||
|
||||
---
|
||||
|
||||
## #207 — Repeat attack continues after movement begins
|
||||
## #208 — Combat bar appears after logging in peacefully
|
||||
|
||||
**Status:** IN-PROGRESS — fix shipped 2026-07-12, pending live gate
|
||||
**Severity:** MEDIUM
|
||||
**Component:** retained UI / combat lifecycle
|
||||
|
||||
**Description:** The basic attack bar appeared immediately after login even
|
||||
though the character always enters the world in peace mode.
|
||||
|
||||
**Root cause:** The combat controller mounted the bar hidden and correctly
|
||||
followed `CombatState`, but the later per-character layout restore reapplied a
|
||||
previously saved `Visible=true` bit. Window persistence therefore overruled the
|
||||
mode-owned visibility from retail `gmCombatUI::RecvNotice_SetCombatMode`.
|
||||
|
||||
**Resolution:** Retained-window persistence now distinguishes state-managed
|
||||
visibility from user-managed visibility. The combat window still restores its
|
||||
position, but ignores and no longer saves show/hide transitions. Character
|
||||
session entry also resets `CombatState` to retail's initial NonCombat mode.
|
||||
|
||||
**Research:** `docs/research/2026-07-11-retail-combat-bar-pseudocode.md`
|
||||
|
||||
**Acceptance:** Log in while in peace mode: no attack bar is visible. Enter
|
||||
melee or missile combat: it appears. Return to peace: it disappears.
|
||||
|
||||
## #207 — Repeat attack continues after movement begins
|
||||
|
||||
**Status:** DONE — 2026-07-12, user confirmed movement stops repeat attacks
|
||||
**Severity:** HIGH
|
||||
**Component:** combat / input
|
||||
|
||||
|
|
@ -73,7 +97,7 @@ attack loop stops immediately and movement proceeds normally.
|
|||
|
||||
## #206 — Relogging into a monster leaves the player stuck
|
||||
|
||||
**Status:** IN-PROGRESS — fix shipped 2026-07-12, pending live gate
|
||||
**Status:** DONE — 2026-07-12, user confirmed occupied-position relog works
|
||||
**Severity:** HIGH
|
||||
**Component:** physics / login lifecycle
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,12 @@ The current acdream port binds the basic page because the persisted gameplay
|
|||
default has `AdvancedCombatUI = false`. The separate advanced-combat surface is
|
||||
still tracked by divergence row AP-110.
|
||||
|
||||
Visibility is combat-state-owned, not a user window preference. Per-character
|
||||
window persistence may restore the bar's authored position, but must not restore
|
||||
or save its visible bit: `RecvNotice_SetCombatMode` is the sole visibility
|
||||
authority. Character-session initialization likewise starts at `NonCombat`, as
|
||||
shown by `ClientCombatSystem::Begin` (`0x0056A460`).
|
||||
|
||||
## Press / charge / release
|
||||
|
||||
```text
|
||||
|
|
|
|||
|
|
@ -2486,6 +2486,7 @@ public sealed class GameWindow : IDisposable
|
|||
_worldState.MarkPersistent(chosen.Id);
|
||||
AcDream.App.Streaming.EntityVanishProbe.PlayerGuid = chosen.Id; // TEMP #138-B probe
|
||||
Console.WriteLine($"live: entering world as 0x{chosen.Id:X8} {chosen.Name}");
|
||||
Combat.Clear();
|
||||
_liveSession.EnterWorld(user, characterIndex: 0);
|
||||
|
||||
_activeToonKey = chosen.Name;
|
||||
|
|
|
|||
|
|
@ -147,7 +147,8 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
Host.WindowManager,
|
||||
persistence.Store,
|
||||
persistence.CharacterKey,
|
||||
persistence.ScreenSize);
|
||||
persistence.ScreenSize,
|
||||
stateManagedVisibilityWindows: [WindowNames.Combat]);
|
||||
}
|
||||
|
||||
if (bindings.Probe.Enabled)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ public sealed class RetailWindowLayoutPersistence : IDisposable
|
|||
private readonly SettingsStore _store;
|
||||
private readonly Func<string> _characterKey;
|
||||
private readonly Func<(int Width, int Height)> _screenSize;
|
||||
private readonly HashSet<string> _stateManagedVisibilityWindows;
|
||||
private readonly List<RetailWindowHandle> _attached = new();
|
||||
private bool _restoring;
|
||||
private bool _disposed;
|
||||
|
|
@ -24,12 +25,16 @@ public sealed class RetailWindowLayoutPersistence : IDisposable
|
|||
RetailWindowManager manager,
|
||||
SettingsStore store,
|
||||
Func<string> characterKey,
|
||||
Func<(int Width, int Height)> screenSize)
|
||||
Func<(int Width, int Height)> screenSize,
|
||||
IEnumerable<string>? stateManagedVisibilityWindows = null)
|
||||
{
|
||||
_manager = manager ?? throw new ArgumentNullException(nameof(manager));
|
||||
_store = store ?? throw new ArgumentNullException(nameof(store));
|
||||
_characterKey = characterKey ?? throw new ArgumentNullException(nameof(characterKey));
|
||||
_screenSize = screenSize ?? throw new ArgumentNullException(nameof(screenSize));
|
||||
_stateManagedVisibilityWindows = stateManagedVisibilityWindows is null
|
||||
? new HashSet<string>(StringComparer.Ordinal)
|
||||
: new HashSet<string>(stateManagedVisibilityWindows, StringComparer.Ordinal);
|
||||
|
||||
foreach (RetailWindowHandle handle in manager.Windows)
|
||||
Attach(handle);
|
||||
|
|
@ -54,7 +59,11 @@ public sealed class RetailWindowLayoutPersistence : IDisposable
|
|||
character, resolution, handle.Name, fallback);
|
||||
if (saved is not { } layout) continue;
|
||||
|
||||
Apply(handle, layout, screen);
|
||||
Apply(
|
||||
handle,
|
||||
layout,
|
||||
screen,
|
||||
restoreVisibility: !_stateManagedVisibilityWindows.Contains(handle.Name));
|
||||
// Lazily migrate legacy position-only entries into the complete schema.
|
||||
_store.SaveWindowLayout(character, resolution, handle.Name, Capture(handle));
|
||||
}
|
||||
|
|
@ -70,9 +79,12 @@ public sealed class RetailWindowLayoutPersistence : IDisposable
|
|||
_attached.Add(handle);
|
||||
handle.Moved += OnChanged;
|
||||
handle.Resized += OnChanged;
|
||||
if (!_stateManagedVisibilityWindows.Contains(handle.Name))
|
||||
{
|
||||
handle.Shown += OnChanged;
|
||||
handle.Hidden += OnChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChanged(RetailWindowHandle handle)
|
||||
{
|
||||
|
|
@ -111,7 +123,8 @@ public sealed class RetailWindowLayoutPersistence : IDisposable
|
|||
private static void Apply(
|
||||
RetailWindowHandle handle,
|
||||
UiWindowLayout layout,
|
||||
(int Width, int Height) screen)
|
||||
(int Width, int Height) screen,
|
||||
bool restoreVisibility)
|
||||
{
|
||||
UiElement frame = handle.OuterFrame;
|
||||
float width = ClampDimension(layout.Width, frame.Width, frame.MinWidth, frame.MaxWidth, screen.Width);
|
||||
|
|
@ -128,9 +141,12 @@ public sealed class RetailWindowLayoutPersistence : IDisposable
|
|||
Collapsed: layout.Collapsed,
|
||||
Maximized: layout.Maximized));
|
||||
|
||||
if (restoreVisibility)
|
||||
{
|
||||
if (layout.Visible) handle.Show();
|
||||
else handle.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
private (int Width, int Height) ValidScreenSize()
|
||||
{
|
||||
|
|
@ -168,9 +184,12 @@ public sealed class RetailWindowLayoutPersistence : IDisposable
|
|||
{
|
||||
handle.Moved -= OnChanged;
|
||||
handle.Resized -= OnChanged;
|
||||
if (!_stateManagedVisibilityWindows.Contains(handle.Name))
|
||||
{
|
||||
handle.Shown -= OnChanged;
|
||||
handle.Hidden -= OnChanged;
|
||||
}
|
||||
}
|
||||
_attached.Clear();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -170,5 +170,14 @@ public sealed class CombatState
|
|||
public void OnCombatCommenceAttack()
|
||||
=> AttackCommenced?.Invoke();
|
||||
|
||||
public void Clear() => _healthByGuid.Clear();
|
||||
/// <summary>
|
||||
/// Reset character-session combat state. Retail
|
||||
/// <c>ClientCombatSystem::Begin</c> (0x0056A460) initializes the mode to
|
||||
/// peace/non-combat along with the transient attack state.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_healthByGuid.Clear();
|
||||
SetCombatMode(CombatMode.NonCombat);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,39 @@ public sealed class RetailWindowLayoutPersistenceTests : IDisposable
|
|||
Assert.Null(store.LoadWindowLayout("default", "800x600", "radar", default));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Restore_StateManagedWindow_PreservesAuthoritativeVisibility()
|
||||
{
|
||||
var store = new SettingsStore(PathName);
|
||||
store.SaveWindowLayout(
|
||||
"Alice",
|
||||
"800x600",
|
||||
"combat",
|
||||
new UiWindowLayout(44f, 55f, 200f, 100f, true, false, false));
|
||||
|
||||
var root = new UiRoot { Width = 800, Height = 600 };
|
||||
RetailWindowHandle handle = Mount(root, "combat");
|
||||
handle.Hide();
|
||||
using var persistence = new RetailWindowLayoutPersistence(
|
||||
root.WindowManager,
|
||||
store,
|
||||
() => "Alice",
|
||||
() => (800, 600),
|
||||
stateManagedVisibilityWindows: ["combat"]);
|
||||
|
||||
persistence.RestoreAll();
|
||||
|
||||
Assert.Equal((44f, 55f), (handle.Left, handle.Top));
|
||||
Assert.False(handle.IsVisible);
|
||||
|
||||
// Combat mode owns subsequent show/hide transitions as well; they must
|
||||
// not turn into saved user visibility preferences.
|
||||
handle.Show();
|
||||
UiWindowLayout saved = Assert.IsType<UiWindowLayout>(
|
||||
store.LoadWindowLayout("Alice", "800x600", "combat", default));
|
||||
Assert.False(saved.Visible);
|
||||
}
|
||||
|
||||
private static RetailWindowHandle Mount(
|
||||
UiRoot root,
|
||||
string name,
|
||||
|
|
|
|||
|
|
@ -128,15 +128,17 @@ public sealed class CombatStateTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_ResetsHealthCache()
|
||||
public void Clear_ResetsCharacterSessionToPeaceMode()
|
||||
{
|
||||
var state = new CombatState();
|
||||
state.OnUpdateHealth(1, 0.5f);
|
||||
state.OnUpdateHealth(2, 0.8f);
|
||||
state.SetCombatMode(CombatMode.Melee);
|
||||
|
||||
state.Clear();
|
||||
|
||||
Assert.Equal(1f, state.GetHealthPercent(1)); // back to default
|
||||
Assert.Equal(0, state.TrackedTargetCount);
|
||||
Assert.Equal(CombatMode.NonCombat, state.CurrentMode);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue