Fix a bunch of bugs.
This commit is contained in:
+20
-6
@@ -671,15 +671,19 @@ func (g *Game) runBattle() (*BattleResult, *PendingBattleDecision) {
|
||||
}
|
||||
}
|
||||
|
||||
// hurt fires Hurt effects on a pet that was damaged and survived.
|
||||
// hurt fires Hurt effects on a pet that took damage. It fires even when the
|
||||
// hit was fatal — the Lizard drops its Bee even when killed in one shot —
|
||||
// except for self-buffs marked SurviveOnly (Peacock, Gorilla), which need
|
||||
// the pet to live on.
|
||||
hurt := func(seat int, u *BattleUnit) {
|
||||
if !u.Alive() {
|
||||
return
|
||||
}
|
||||
alive := u.Alive()
|
||||
for _, e := range u.effects() {
|
||||
if e.Trigger != TriggerHurt || !allowed(e, u) {
|
||||
continue
|
||||
}
|
||||
if !alive && e.SurviveOnly {
|
||||
continue
|
||||
}
|
||||
if !spend(seat, e, u.Card.Name) {
|
||||
continue
|
||||
}
|
||||
@@ -787,6 +791,10 @@ func (g *Game) runBattle() (*BattleResult, *PendingBattleDecision) {
|
||||
emitPrevent(target, tu.Card.Name, prev)
|
||||
}
|
||||
if died {
|
||||
// Hurt still fires on a fatal hit (Lizard's Bee), before the faint.
|
||||
if dealt > 0 {
|
||||
hurt(target, tu)
|
||||
}
|
||||
faint(target, tu)
|
||||
sides[target].unit = nil
|
||||
return true
|
||||
@@ -994,8 +1002,10 @@ func (g *Game) runBattle() (*BattleResult, *PendingBattleDecision) {
|
||||
// when its own side is already out (Crocodile's last-pet volley).
|
||||
anyDeath := false
|
||||
for _, q := range plays {
|
||||
// Effects sourced from a specific unit fizzle if it's gone.
|
||||
if q.unit != nil && sides[q.seat].unit != q.unit {
|
||||
// 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).
|
||||
if q.unit != nil && sides[q.seat].unit != q.unit && !q.effect.Posthumous {
|
||||
continue
|
||||
}
|
||||
if q.unit != nil && !allowed(q.effect, q.unit) {
|
||||
@@ -1293,6 +1303,10 @@ func (g *Game) runBattle() (*BattleResult, *PendingBattleDecision) {
|
||||
dealt := []int{dealtA, dealtB}
|
||||
for seat, u := range []*BattleUnit{ua, ub} {
|
||||
if !u.Alive() {
|
||||
// Hurt still fires on a fatal clash (Lizard's Bee), before the faint.
|
||||
if dealt[seat] > 0 {
|
||||
hurt(seat, u)
|
||||
}
|
||||
faint(seat, u)
|
||||
sides[seat].unit = nil
|
||||
} else {
|
||||
|
||||
@@ -141,6 +141,38 @@ func TestBattleDamageMarkers(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A Hurt effect still fires on a fatal hit: the Lizard drops a Bee on top of
|
||||
// its deck even when killed in one clash.
|
||||
func TestLizardBeeOnFatalHit(t *testing.T) {
|
||||
g, _, _ := testGame(t)
|
||||
res := forceBattle(t, g,
|
||||
[]Card{g.goldenPet(t, "Lizard")},
|
||||
[]Card{g.pet("Wall", 20)}, // one clash kills the 2-power Lizard
|
||||
)
|
||||
bees := 0
|
||||
for _, ev := range eventsOfType(res, "summon") {
|
||||
if ev.Card != nil && ev.Card.Name == "Bee" {
|
||||
bees++
|
||||
}
|
||||
}
|
||||
if bees == 0 {
|
||||
t.Fatal("Lizard should add a Bee even when killed in one hit")
|
||||
}
|
||||
}
|
||||
|
||||
// A Hurt effect marked SurviveOnly does NOT fire on a fatal hit: the Peacock's
|
||||
// "if this pet hasn't fainted" apple is skipped when the clash kills it.
|
||||
func TestPeacockNoEatOnFatalHit(t *testing.T) {
|
||||
g, _, _ := testGame(t)
|
||||
res := forceBattle(t, g,
|
||||
[]Card{g.realPet(t, "Peacock")},
|
||||
[]Card{g.pet("Wall", 20)}, // one clash kills the 2-power Peacock
|
||||
)
|
||||
if len(eventsOfType(res, "eat")) != 0 {
|
||||
t.Fatal("Peacock should not eat an apple when it faints")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBattleEqualPowerBothDie(t *testing.T) {
|
||||
g, _, _ := testGame(t)
|
||||
res := forceBattle(t, g,
|
||||
@@ -486,7 +518,8 @@ func TestDolphinThrowsThreeRocks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Camel pushes an apple onto its own stack whenever it's hurt and survives.
|
||||
// Camel pushes an apple onto its own stack whenever it's hurt — including the
|
||||
// hit that finishes it, since Hurt now fires on faint.
|
||||
func TestCamelHurtSummonsApple(t *testing.T) {
|
||||
g, _, _ := testGame(t)
|
||||
res := forceBattle(t, g,
|
||||
@@ -494,10 +527,16 @@ func TestCamelHurtSummonsApple(t *testing.T) {
|
||||
[]Card{g.pet("Chip", 1), g.pet("Chip2", 3)},
|
||||
)
|
||||
// Clash 1: camel takes 1 (survives) → apple onto A's stack. Clash 2 vs
|
||||
// Chip2: both die. A reveals apple + Ally (3 power) and wins.
|
||||
// Chip2: camel dies but its Hurt still fires → a second apple. A reveals
|
||||
// both apples onto Ally and wins.
|
||||
summons := eventsOfType(res, "summon")
|
||||
if len(summons) != 1 || summons[0].Seat != 0 || summons[0].Card.Food != FoodApple {
|
||||
t.Fatalf("camel should summon one apple onto its own stack: %+v", summons)
|
||||
if len(summons) != 2 {
|
||||
t.Fatalf("camel should summon an apple each time it's hurt (twice): %+v", summons)
|
||||
}
|
||||
for _, s := range summons {
|
||||
if s.Seat != 0 || s.Card.Food != FoodApple {
|
||||
t.Fatalf("camel apples should land on its own stack: %+v", s)
|
||||
}
|
||||
}
|
||||
if res.WinnerSeat != 0 {
|
||||
t.Fatalf("apple-buffed ally should win, got %d", res.WinnerSeat)
|
||||
|
||||
+12
-3
@@ -258,6 +258,15 @@ type Effect struct {
|
||||
// effect to fire (Golden pack). It's auto-paid when affordable and the
|
||||
// effect is skipped otherwise — battles take no player input.
|
||||
CostTrumpet int `json:"costTrumpet,omitempty"`
|
||||
// SurviveOnly marks a Hurt effect that fires only when the pet lives through
|
||||
// the hit. By default a Hurt effect still fires on a fatal hit (the Lizard
|
||||
// drops its Bee even when killed); self-buffs that are pointless once the
|
||||
// pet is gone (Peacock's apple, Gorilla's shield) opt out with this.
|
||||
SurviveOnly bool `json:"surviveOnly,omitempty"`
|
||||
// Posthumous marks a Play effect that still resolves even after its own pet
|
||||
// has left play — so the Manatee still adds its Apples after rocking itself
|
||||
// to death.
|
||||
Posthumous bool `json:"posthumous,omitempty"`
|
||||
}
|
||||
|
||||
// count normalizes the zero value to 1.
|
||||
@@ -361,7 +370,7 @@ var petTiers = [MaxRounds][]petTemplate{
|
||||
},
|
||||
{
|
||||
Name: "Peacock", Power: 2, Suits: []Suit{SuitBlue, SuitRed},
|
||||
Effects: []Effect{{Trigger: TriggerHurt, Action: ActionEatApple}},
|
||||
Effects: []Effect{{Trigger: TriggerHurt, Action: ActionEatApple, SurviveOnly: true}},
|
||||
EffectText: "Hurt: if this pet hasn't fainted, it eats 1 Apple",
|
||||
},
|
||||
{
|
||||
@@ -500,7 +509,7 @@ var petTiers = [MaxRounds][]petTemplate{
|
||||
{ // Tier 6
|
||||
{
|
||||
Name: "Gorilla", Power: 6, Suits: []Suit{SuitBlue, SuitYellow},
|
||||
Effects: []Effect{{Trigger: TriggerHurt, Action: ActionShieldSelf}},
|
||||
Effects: []Effect{{Trigger: TriggerHurt, Action: ActionShieldSelf, SurviveOnly: true}},
|
||||
EffectText: "Hurt: the next time this pet is hit, prevent all damage",
|
||||
},
|
||||
{
|
||||
@@ -727,7 +736,7 @@ var goldenPetTiers = [MaxRounds][]petTemplate{
|
||||
Name: "Manatee", Power: 3, Suits: []Suit{SuitBlue, SuitYellow},
|
||||
Effects: []Effect{
|
||||
{Trigger: TriggerPlay, Action: ActionThrowRock, Count: 2, Target: "self"},
|
||||
{Trigger: TriggerPlay, Action: ActionSummonTop, Card: "apple", Count: 4},
|
||||
{Trigger: TriggerPlay, Action: ActionSummonTop, Card: "apple", Count: 4, Posthumous: true},
|
||||
},
|
||||
EffectText: "Play: throw 2 Rocks at itself, then add 4 Apples on top of your deck",
|
||||
},
|
||||
|
||||
@@ -136,6 +136,35 @@ func TestManateeSelfRock(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Even when its own rocks are lethal, the Manatee still stacks all 4 apples —
|
||||
// the summon is posthumous, so it fires after the Manatee faints.
|
||||
func TestManateeSelfRockFatalStillAddsApples(t *testing.T) {
|
||||
g, _, _ := testGame(t)
|
||||
g.RollDie = func() int { return 2 } // 2 rocks = 4 damage, past its 3 power
|
||||
res := forceBattle(t, g,
|
||||
[]Card{g.goldenPet(t, "Manatee")},
|
||||
[]Card{g.pet("Wall", 20)},
|
||||
)
|
||||
var self *BattleEvent
|
||||
for i, ev := range res.Events {
|
||||
if ev.Type == "rock" && ev.Target == 0 {
|
||||
self = &res.Events[i]
|
||||
}
|
||||
}
|
||||
if self == nil || !self.TargetDied {
|
||||
t.Fatalf("Manatee's own rocks should faint it: %+v", eventsOfType(res, "rock"))
|
||||
}
|
||||
apples := 0
|
||||
for _, ev := range eventsOfType(res, "summon") {
|
||||
if ev.Card != nil && ev.Card.Name == "Apple" {
|
||||
apples++
|
||||
}
|
||||
}
|
||||
if apples != 4 {
|
||||
t.Fatalf("Manatee should still add 4 apples after fainting, got %d", apples)
|
||||
}
|
||||
}
|
||||
|
||||
// Poison Dart Frog, once set aside, throws rocks each time a Bee is played.
|
||||
func TestPoisonDartFrogBeeRocks(t *testing.T) {
|
||||
g, _, _ := testGame(t)
|
||||
|
||||
Reference in New Issue
Block a user