feat: port retail magic lifecycle and retained spell UI
Complete the retail cast-intent, target, component, enchantment, and busy-state paths; mount the DAT-authored spell bar, spellbook, component book, effects panels, and shared panel lifecycle; and add scoped input plus conformance coverage. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
7b7ffcd278
commit
07be994d97
84 changed files with 17822 additions and 1051 deletions
|
|
@ -38,6 +38,47 @@ public class InputDispatcherIsActionHeldTests
|
|||
Assert.False(dispatcher.IsActionHeld(InputAction.MovementForward));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsActionHeld_CombatBindingOnSameChord_ShadowsGamePolling()
|
||||
{
|
||||
var (dispatcher, kb, _, bindings) = Build();
|
||||
var chord = new KeyChord(Key.W, ModifierMask.None);
|
||||
bindings.Add(new Binding(chord, InputAction.MovementForward));
|
||||
bindings.Add(new Binding(
|
||||
chord,
|
||||
InputAction.CombatCastCurrentSpell,
|
||||
Scope: InputScope.MagicCombat));
|
||||
dispatcher.SetCombatScope(InputScope.MagicCombat);
|
||||
|
||||
kb.EmitKeyDown(Key.W, ModifierMask.None);
|
||||
|
||||
Assert.False(dispatcher.IsActionHeld(InputAction.MovementForward));
|
||||
Assert.True(dispatcher.IsActionHeld(InputAction.CombatCastCurrentSpell));
|
||||
|
||||
dispatcher.SetCombatScope(null);
|
||||
Assert.True(dispatcher.IsActionHeld(InputAction.MovementForward));
|
||||
Assert.False(dispatcher.IsActionHeld(InputAction.CombatCastCurrentSpell));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsActionHeld_ExplicitShiftCombatChord_ShadowsBareMovementFallback()
|
||||
{
|
||||
var (dispatcher, kb, _, bindings) = Build();
|
||||
bindings.Add(new Binding(
|
||||
new KeyChord(Key.W, ModifierMask.None),
|
||||
InputAction.MovementForward));
|
||||
bindings.Add(new Binding(
|
||||
new KeyChord(Key.W, ModifierMask.Shift),
|
||||
InputAction.CombatCastCurrentSpell,
|
||||
Scope: InputScope.MagicCombat));
|
||||
dispatcher.SetCombatScope(InputScope.MagicCombat);
|
||||
|
||||
kb.EmitKeyDown(Key.W, ModifierMask.Shift);
|
||||
|
||||
Assert.False(dispatcher.IsActionHeld(InputAction.MovementForward));
|
||||
Assert.True(dispatcher.IsActionHeld(InputAction.CombatCastCurrentSpell));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsActionHeld_returns_false_when_no_binding_for_action()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -82,6 +82,44 @@ public class InputDispatcherTests
|
|||
Assert.Equal(InputScope.Game, dispatcher.ActiveScope);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Magic_combat_scope_shadows_game_binding_for_same_chord()
|
||||
{
|
||||
var (dispatcher, kb, _, bindings, fired) = Build();
|
||||
var chord = new KeyChord(Key.Number1, ModifierMask.None);
|
||||
bindings.Add(new Binding(chord, InputAction.UseQuickSlot_1));
|
||||
bindings.Add(new Binding(chord, InputAction.UseSpellSlot_1, Scope: InputScope.MagicCombat));
|
||||
|
||||
kb.EmitKeyDown(Key.Number1, ModifierMask.None);
|
||||
dispatcher.SetCombatScope(InputScope.MagicCombat);
|
||||
kb.EmitKeyDown(Key.Number1, ModifierMask.None);
|
||||
|
||||
Assert.Equal(
|
||||
[(InputAction.UseQuickSlot_1, ActivationType.Press),
|
||||
(InputAction.UseSpellSlot_1, ActivationType.Press)],
|
||||
fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Changing_combat_scope_releases_hold_resolved_in_previous_scope()
|
||||
{
|
||||
var (dispatcher, kb, _, bindings, fired) = Build();
|
||||
bindings.Add(new Binding(
|
||||
new KeyChord(Key.Delete, ModifierMask.None),
|
||||
InputAction.CombatLowAttack,
|
||||
ActivationType.Hold,
|
||||
InputScope.MeleeCombat));
|
||||
dispatcher.SetCombatScope(InputScope.MeleeCombat);
|
||||
|
||||
kb.EmitKeyDown(Key.Delete, ModifierMask.None);
|
||||
dispatcher.SetCombatScope(InputScope.MagicCombat);
|
||||
|
||||
Assert.Equal(
|
||||
[(InputAction.CombatLowAttack, ActivationType.Press),
|
||||
(InputAction.CombatLowAttack, ActivationType.Release)],
|
||||
fired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PushScope_changes_ActiveScope()
|
||||
{
|
||||
|
|
@ -149,6 +187,40 @@ public class InputDispatcherTests
|
|||
Assert.Empty(fired); // no longer held
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hold_callback_scope_change_DoesNotDispatchStaleSnapshotChord()
|
||||
{
|
||||
var (dispatcher, kb, _, bindings, fired) = Build();
|
||||
bindings.Add(new Binding(
|
||||
new KeyChord(Key.Delete, ModifierMask.None),
|
||||
InputAction.CombatLowAttack,
|
||||
ActivationType.Hold,
|
||||
InputScope.MeleeCombat));
|
||||
bindings.Add(new Binding(
|
||||
new KeyChord(Key.End, ModifierMask.None),
|
||||
InputAction.CombatHighAttack,
|
||||
ActivationType.Hold,
|
||||
InputScope.MeleeCombat));
|
||||
dispatcher.SetCombatScope(InputScope.MeleeCombat);
|
||||
kb.EmitKeyDown(Key.Delete, ModifierMask.None);
|
||||
kb.EmitKeyDown(Key.End, ModifierMask.None);
|
||||
fired.Clear();
|
||||
|
||||
bool changed = false;
|
||||
dispatcher.Fired += (_, activation) =>
|
||||
{
|
||||
if (activation != ActivationType.Hold || changed)
|
||||
return;
|
||||
changed = true;
|
||||
dispatcher.SetCombatScope(InputScope.MagicCombat);
|
||||
};
|
||||
|
||||
dispatcher.Tick();
|
||||
|
||||
Assert.Single(fired, entry => entry.Item2 == ActivationType.Hold);
|
||||
Assert.Equal(2, fired.Count(entry => entry.Item2 == ActivationType.Release));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Release_binding_fires_only_on_KeyUp()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ public class KeyBindingsJsonTests
|
|||
Assert.Contains(loaded.All, x =>
|
||||
x.Chord == b.Chord
|
||||
&& x.Action == b.Action
|
||||
&& x.Activation == b.Activation);
|
||||
&& x.Activation == b.Activation
|
||||
&& x.Scope == b.Scope);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
|
|
|||
|
|
@ -57,6 +57,20 @@ public class KeyBindingsTests
|
|||
Assert.Null(b.Find(new KeyChord(Key.S, ModifierMask.None), ActivationType.Press));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetailDefaults_resolve_shared_number_key_by_scope()
|
||||
{
|
||||
var bindings = KeyBindings.RetailDefaults();
|
||||
var chord = new KeyChord(Key.Number1, ModifierMask.None);
|
||||
|
||||
Assert.Equal(
|
||||
InputAction.UseQuickSlot_1,
|
||||
bindings.Find(chord, ActivationType.Press, InputScope.Game)?.Action);
|
||||
Assert.Equal(
|
||||
InputAction.UseSpellSlot_1,
|
||||
bindings.Find(chord, ActivationType.Press, InputScope.MagicCombat)?.Action);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForAction_returns_all_chords_bound_to_action()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue