diff --git a/.gitignore b/.gitignore index d817ecb..63177fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ /bin/ /data/ .env +card-captures/ # frontend web/node_modules/ web/dist/ + diff --git a/internal/game/battle.go b/internal/game/battle.go index a255e9e..3ba469a 100644 --- a/internal/game/battle.go +++ b/internal/game/battle.go @@ -1320,7 +1320,10 @@ func (g *Game) runBattle() *BattleResult { // This runs before the "battle over" check so a parting shot fires even // when its own side is already out (Crocodile's last-pet volley). anyDeath := false - for _, q := range plays { + // Index-based so effects queued mid-loop (Abomination mimicking a Play + // ability) are resolved in the same pass. + for i := 0; i < len(plays); i++ { + q := plays[i] // Effects sourced from a specific unit fizzle if it's gone — unless // they're posthumous (the Manatee still adds its Apples after rocking // itself to death). @@ -1626,8 +1629,9 @@ func (g *Game) runBattle() *BattleResult { } case ActionGainAbility: // Abomination: copy a random pet's effects from the highest tier - // discard pile. Gained abilities fire on their natural triggers going - // forward; a gained Play ability doesn't retro-fire this turn. + // discard pile. The copied effects join Abomination's own (so its + // Faint/Hurt abilities fire later), and any copied Play ability + // resolves right now — it's queued onto this same pass. bestTier := 0 for tier, pile := range g.Discards { if tier <= bestTier { @@ -1652,6 +1656,16 @@ func (g *Game) runBattle() *BattleResult { q.unit.Card.Effects = append(append([]Effect(nil), q.unit.Card.Effects...), pick.Effects...) emit(BattleEvent{Type: "eat", Seat: q.seat, Bonus: q.unit.Bonus, Text: fmt.Sprintf("%s mimics %s's ability.", q.unit.Card.Name, pick.Name)}) + // Fire the mimicked Play abilities immediately. Skip another + // gainAbility so mimicking can't chain into an infinite loop, + // and skip the enter-time shields (armed only on reveal). + for _, pe := range pick.Effects { + if pe.Trigger != TriggerPlay || pe.Action == ActionGainAbility || + pe.Action == ActionShieldSelf || pe.Action == ActionPreventSelf { + continue + } + plays = append(plays, queuedPlay{seat: q.seat, unit: q.unit, effect: pe}) + } } } } diff --git a/internal/game/unicorn_test.go b/internal/game/unicorn_test.go index d339c2f..4004096 100644 --- a/internal/game/unicorn_test.go +++ b/internal/game/unicorn_test.go @@ -860,6 +860,35 @@ func TestChimeraSummonFromDiscard(t *testing.T) { } } +// Abomination mimics a discard pet's ability, firing its Play effect at once. +func TestAbominationMimicsPlayAbility(t *testing.T) { + g, _, _ := unicornGame(t) + // The tier 1 discard pile holds a Barghest (Play → Spook the enemy pet). + g.Discards = map[int][]Card{1: {g.unicornPet(t, "Barghest")}} + res := forceBattle(t, g, + []Card{g.unicornPet(t, "Abomination"), g.pet("Body", 4)}, + []Card{g.pet("Tank", 10)}, + ) + if len(eventsOfType(res, "ailment")) == 0 { + t.Fatal("Abomination should immediately fire the mimicked Barghest Play ability") + } +} + +// Abomination's mimicked non-Play ability fires on its natural trigger later. +func TestAbominationMimicsFaintAbility(t *testing.T) { + g, _, _ := unicornGame(t) + // Discard holds an Ant (Faint → apple on deck); Abomination gains it and the + // apple appears when Abomination faints. + g.Discards = map[int][]Card{1: {g.tier1(t, "Ant")}} + res := forceBattle(t, g, + []Card{g.unicornPet(t, "Abomination")}, + []Card{g.pet("Killer", 12)}, + ) + if seat0Summons(res, "Apple") == 0 { + t.Fatal("Abomination should gain the Ant's Faint ability and drop an apple on faint") + } +} + // Pixiu spends Mana to summon the top of the tier 6 shop deck. func TestPixiuSummonFromTierDeck(t *testing.T) { g, _, _ := unicornGame(t)