Trigger buffs last until end of turn; trigger cards go through the trigger zone first
Order is now: reveal -> trigger zone -> resolve effects -> hand (drive check) or damage (damage check). Damage is added only after the trigger resolves, so heal compares the pre-damage count. Temporary power/crit are cleared at end of turn instead of end of battle.
This commit is contained in:
parent
ebbfb46834
commit
c2480bab63
|
|
@ -114,7 +114,7 @@
|
|||
- [ ] Check Timing: сбор → сортировка → разрешение по одному → повтор.
|
||||
- [ ] Стоимости: CB, SB, discard, rest self, retire self, put to soul, return to deck.
|
||||
- [ ] Цели: авто по фильтру и выбор игроком, «до N» / «ровно N».
|
||||
- [~] Слои модификаторов силы/крита: временные бонусы до конца боя есть, постоянных слоёв нет.
|
||||
- [~] Слои модификаторов силы/крита: усиления от триггеров живут до конца хода, постоянных слоёв нет.
|
||||
- [x] Валидация интентов: `Rejected(reason)` без изменения состояния.
|
||||
- [ ] Список легальных интентов на каждый момент (для UI и античита).
|
||||
- [~] Детерминированный RNG: один поток на партию, состояние в `GameState`.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ namespace Vanguard.Rules.Engine;
|
|||
/// * Boost не реализован;
|
||||
/// * триггер требует выбора целей через ResolveTriggerCommand:
|
||||
/// +5000 любой карте в кругах Авангарда или Арьергарда, затем второй эффект;
|
||||
/// правило одинаково для drive check и damage check.
|
||||
/// правило одинаково для drive check и damage check;
|
||||
/// * порядок проверки: карта → зона триггера → эффекты → рука (drive) или урон (damage);
|
||||
/// * усиления от триггеров живут до конца хода.
|
||||
/// </summary>
|
||||
public sealed class GameEngine
|
||||
{
|
||||
|
|
@ -324,6 +326,8 @@ public sealed class GameEngine
|
|||
State.Phase = Phase.End;
|
||||
_events.Add(new PhaseChangedEvent(State.ActivePlayer, Phase.End));
|
||||
|
||||
ClearTurnModifiers();
|
||||
|
||||
if (State.IsFinished)
|
||||
return CommandResult.Ok();
|
||||
|
||||
|
|
@ -368,7 +372,15 @@ public sealed class GameEngine
|
|||
}
|
||||
}
|
||||
|
||||
// ---------- бой ----------
|
||||
/// <summary>Усиления от триггеров живут до конца хода, а не до конца боя.</summary>
|
||||
private void ClearTurnModifiers()
|
||||
{
|
||||
foreach (var unit in AllUnits(0).Concat(AllUnits(1)))
|
||||
{
|
||||
unit.TemporaryPower = 0;
|
||||
unit.TemporaryCritical = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void BeginResolution(BattleState battle)
|
||||
{
|
||||
|
|
@ -418,6 +430,7 @@ public sealed class GameEngine
|
|||
{
|
||||
Player = player,
|
||||
PrintingId = card.PrintingId,
|
||||
Card = card,
|
||||
Trigger = def.Trigger,
|
||||
Context = TriggerContext.DriveCheck,
|
||||
GrantsPower = true,
|
||||
|
|
@ -480,16 +493,23 @@ public sealed class GameEngine
|
|||
State.PendingTrigger = null;
|
||||
_events.Add(new TriggerAppliedEvent(player, command.PowerTarget, command.EffectTarget));
|
||||
|
||||
if (pending.Context == TriggerContext.DriveCheck && State.Battle is { } battle)
|
||||
if (pending.Context == TriggerContext.DriveCheck)
|
||||
{
|
||||
State.Step = Step.Drive;
|
||||
ContinueDriveChecks(battle);
|
||||
// эффекты сработали — теперь карта уходит из зоны триггера в руку
|
||||
var p = State.Players[player];
|
||||
p.Trigger.Remove(pending.Card);
|
||||
p.Hand.Add(pending.Card);
|
||||
|
||||
if (State.Battle is { } battle)
|
||||
{
|
||||
State.Step = Step.Drive;
|
||||
ContinueDriveChecks(battle);
|
||||
}
|
||||
|
||||
return CommandResult.Ok();
|
||||
}
|
||||
|
||||
if (pending.Context == TriggerContext.DamageCheck)
|
||||
FinishDamage(pending);
|
||||
|
||||
FinishDamage(pending);
|
||||
return CommandResult.Ok();
|
||||
}
|
||||
|
||||
|
|
@ -550,12 +570,6 @@ public sealed class GameEngine
|
|||
defender.Drop.Add(guard);
|
||||
}
|
||||
|
||||
foreach (var unit in AllUnits(0).Concat(AllUnits(1)))
|
||||
{
|
||||
unit.TemporaryPower = 0;
|
||||
unit.TemporaryCritical = 0;
|
||||
}
|
||||
|
||||
State.Battle = null;
|
||||
State.PendingTrigger = null;
|
||||
State.Step = Step.None;
|
||||
|
|
@ -573,18 +587,18 @@ public sealed class GameEngine
|
|||
if (card is null)
|
||||
return;
|
||||
|
||||
card.FaceUp = false;
|
||||
State.Players[player].Damage.Add(card);
|
||||
_events.Add(new DamageTakenEvent(player, 1, State.Players[player].Damage.Count));
|
||||
|
||||
var def = State.Catalog.CardOf(card.PrintingId);
|
||||
if (def.Trigger != TriggerType.None)
|
||||
{
|
||||
// карта попадает в зону триггера, эффекты срабатывают до урона
|
||||
State.Players[player].Trigger.Add(card);
|
||||
_events.Add(new TriggerResolvedEvent(player, card.PrintingId, def.Trigger));
|
||||
|
||||
State.PendingTrigger = new PendingTrigger
|
||||
{
|
||||
Player = player,
|
||||
PrintingId = card.PrintingId,
|
||||
Card = card,
|
||||
Trigger = def.Trigger,
|
||||
Context = TriggerContext.DamageCheck,
|
||||
GrantsPower = true,
|
||||
|
|
@ -595,30 +609,39 @@ public sealed class GameEngine
|
|||
return;
|
||||
}
|
||||
|
||||
if (State.Players[player].Damage.Count >= MaxDamage)
|
||||
{
|
||||
Defeat(player);
|
||||
PutIntoDamage(player, card);
|
||||
|
||||
if (State.IsFinished)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PutIntoDamage(int player, CardInstance card)
|
||||
{
|
||||
card.FaceUp = false;
|
||||
var p = State.Players[player];
|
||||
p.Damage.Add(card);
|
||||
_events.Add(new DamageTakenEvent(player, 1, p.Damage.Count));
|
||||
|
||||
if (p.Damage.Count >= MaxDamage)
|
||||
Defeat(player);
|
||||
}
|
||||
|
||||
/// <summary>Продолжает урон после разрешения триггера damage check.</summary>
|
||||
private void FinishDamage(PendingTrigger pending)
|
||||
{
|
||||
if (State.IsFinished)
|
||||
return;
|
||||
|
||||
var player = pending.Player;
|
||||
var p = State.Players[pending.Player];
|
||||
p.Trigger.Remove(pending.Card);
|
||||
PutIntoDamage(pending.Player, pending.Card);
|
||||
|
||||
if (State.Players[player].Damage.Count >= MaxDamage)
|
||||
{
|
||||
Defeat(player);
|
||||
if (State.IsFinished)
|
||||
return;
|
||||
}
|
||||
|
||||
if (pending.RemainingDamage > 0)
|
||||
DealDamage(player, pending.RemainingDamage);
|
||||
DealDamage(pending.Player, pending.RemainingDamage);
|
||||
|
||||
if (State.PendingTrigger is not null)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ public sealed class PendingTrigger
|
|||
{
|
||||
public required int Player { get; init; }
|
||||
public required string PrintingId { get; init; }
|
||||
public required CardInstance Card { get; init; }
|
||||
public required TriggerType Trigger { get; init; }
|
||||
public required TriggerContext Context { get; init; }
|
||||
|
||||
|
|
|
|||
|
|
@ -166,17 +166,38 @@ public class GameEngineTests
|
|||
var pending = engine.State.PendingTrigger;
|
||||
Assert.NotNull(pending);
|
||||
Assert.Equal(TriggerContext.DamageCheck, pending!.Context);
|
||||
Assert.Single(engine.State.Players[1].Damage);
|
||||
|
||||
// до разрешения карта лежит в зоне триггера, а не в уроне
|
||||
Assert.Empty(engine.State.Players[1].Damage);
|
||||
Assert.Contains(engine.State.Players[1].Trigger, c => c.PrintingId == Nike);
|
||||
|
||||
Assert.True(engine.Execute(new ResolveTriggerCommand(-1, -1)).Accepted);
|
||||
|
||||
Assert.Null(engine.State.PendingTrigger);
|
||||
Assert.Null(engine.State.Battle);
|
||||
Assert.Single(engine.State.Players[1].Damage);
|
||||
Assert.Empty(engine.State.Players[1].Trigger);
|
||||
Assert.Contains(engine.State.Players[1].Damage, c => c.PrintingId == Nike);
|
||||
Assert.Contains(engine.Events, e => e is TriggerAppliedEvent { Player: 1 });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Trigger_Power_Lasts_Until_End_Of_Turn()
|
||||
{
|
||||
var engine = ReadyToAttack();
|
||||
StackDeck(engine.State.Players[0], Nike, Gallatin);
|
||||
|
||||
engine.Execute(new AttackCommand(-1, -1));
|
||||
engine.Execute(new PassGuardCommand());
|
||||
engine.Execute(new ResolveTriggerCommand(-1, -1));
|
||||
|
||||
var vanguard = engine.State.Players[0].Vanguard!;
|
||||
Assert.Equal(GameEngine.TriggerPowerBonus, vanguard.TemporaryPower);
|
||||
|
||||
Assert.True(engine.Execute(new EndTurnCommand()).Accepted);
|
||||
Assert.Equal(0, vanguard.TemporaryPower);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveTrigger_Rejects_When_Nothing_Is_Pending()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue