Improve rock attribution.

This commit is contained in:
Greyson Parrelli
2026-07-23 11:17:56 -04:00
parent 235b1d9cc8
commit 816ad13f7e
2 changed files with 43 additions and 20 deletions
+27 -10
View File
@@ -172,7 +172,7 @@ type battleSide struct {
petBonus int // Mammoth aura: +power for later friendly pets petBonus int // Mammoth aura: +power for later friendly pets
oneShotRocks []setAsideRocks // Badger/Blowfish: on next own pet play oneShotRocks []setAsideRocks // Badger/Blowfish: on next own pet play
recurringRocks []int // Snake: on every own pet play recurringRocks []setAsideRocks // Snake: on every own pet play
lastPetRocks []lastPetVolley // Crocodile: when the enemy plays their last pet lastPetRocks []lastPetVolley // Crocodile: when the enemy plays their last pet
shieldCards []Card // Turtle set-aside cards, parallel to shields shieldCards []Card // Turtle set-aside cards, parallel to shields
} }
@@ -194,6 +194,7 @@ type queuedPlay struct {
effect Effect effect Effect
everyone bool // rock volley hits every active pet (Badger) everyone bool // rock volley hits every active pet (Badger)
release *Card // set-aside card to release once this play resolves (Blowfish/Badger/Croc) release *Card // set-aside card to release once this play resolves (Blowfish/Badger/Croc)
source *Card // set-aside pet credited as the effect's source (Blowfish/Badger/Croc/Snake)
} }
// effectCount resolves an effect's final count: base × Per statistic, // effectCount resolves an effect's final count: base × Per statistic,
@@ -389,7 +390,8 @@ func (g *Game) resolveBattle() {
setAsideRocks{dice: e.count(), everyone: e.Target == "all", src: u.Card}) setAsideRocks{dice: e.count(), everyone: e.Target == "all", src: u.Card})
setAside() setAside()
case ActionRecurringRocks: case ActionRecurringRocks:
s.recurringRocks = append(s.recurringRocks, e.count()) s.recurringRocks = append(s.recurringRocks,
setAsideRocks{dice: e.count(), src: u.Card})
setAside() setAside()
case ActionEnemyLastPetRocks: case ActionEnemyLastPetRocks:
s.lastPetRocks = append(s.lastPetRocks, lastPetVolley{dice: e.count(), src: u.Card}) s.lastPetRocks = append(s.lastPetRocks, lastPetVolley{dice: e.count(), src: u.Card})
@@ -457,7 +459,7 @@ func (g *Game) resolveBattle() {
// throwRocks rolls `dice` rock dice against one seat's pet. Rocks are // throwRocks rolls `dice` rock dice against one seat's pet. Rocks are
// not "attacks with" the pet, so no knockout applies. Reports a kill. // not "attacks with" the pet, so no knockout applies. Reports a kill.
throwRocks := func(from, target, dice int) (killed bool) { throwRocks := func(from, target, dice int, source *Card) (killed bool) {
tu := sides[target].unit tu := sides[target].unit
if tu == nil { if tu == nil {
return false return false
@@ -470,11 +472,24 @@ func (g *Game) resolveBattle() {
} }
dealt, block := hitUnit(target, roll) dealt, block := hitUnit(target, roll)
died := !tu.Alive() died := !tu.Alive()
// A set-aside pet (Snake/Blowfish/Badger/Croc) throws its rocks after a
// different pet has been played, so credit `source` explicitly; only
// fall back to the pet in play for direct throwers (Hedgehog/Rhino).
var rockTxt string
if source != nil {
switch {
case roll == 0:
rockTxt = fmt.Sprintf("%s's set-aside rocks miss %s.", source.Name, tu.Card.Name)
case died:
rockTxt = fmt.Sprintf("%s's set-aside rocks pelt %s for %d — it faints.", source.Name, tu.Card.Name, roll)
default:
rockTxt = fmt.Sprintf("%s's set-aside rocks pelt %s for %d.", source.Name, tu.Card.Name, roll)
}
} else {
thrower := pname(from) thrower := pname(from)
if su := sides[from].unit; su != nil { if su := sides[from].unit; su != nil {
thrower = su.Card.Name thrower = su.Card.Name
} }
var rockTxt string
switch { switch {
case roll == 0: case roll == 0:
rockTxt = fmt.Sprintf("%s's rocks miss %s.", thrower, tu.Card.Name) rockTxt = fmt.Sprintf("%s's rocks miss %s.", thrower, tu.Card.Name)
@@ -483,6 +498,7 @@ func (g *Game) resolveBattle() {
default: default:
rockTxt = fmt.Sprintf("%s pelts %s for %d.", thrower, tu.Card.Name, roll) rockTxt = fmt.Sprintf("%s pelts %s for %d.", thrower, tu.Card.Name, roll)
} }
}
emit(BattleEvent{ emit(BattleEvent{
Type: "rock", Seat: from, Target: target, Roll: roll, Dice: faces, Type: "rock", Seat: from, Target: target, Roll: roll, Dice: faces,
DamageAfter: tu.Damage, TargetDied: died, Text: rockTxt, DamageAfter: tu.Damage, TargetDied: died, Text: rockTxt,
@@ -561,12 +577,13 @@ func (g *Game) resolveBattle() {
for _, r := range s.oneShotRocks { for _, r := range s.oneShotRocks {
src := r.src src := r.src
plays = append(plays, queuedPlay{seat: seat, everyone: r.everyone, plays = append(plays, queuedPlay{seat: seat, everyone: r.everyone,
effect: Effect{Action: ActionThrowRock, Count: r.dice}, release: &src}) effect: Effect{Action: ActionThrowRock, Count: r.dice}, release: &src, source: &src})
} }
s.oneShotRocks = nil s.oneShotRocks = nil
for _, dice := range s.recurringRocks { for _, r := range s.recurringRocks {
src := r.src
plays = append(plays, queuedPlay{seat: seat, plays = append(plays, queuedPlay{seat: seat,
effect: Effect{Action: ActionThrowRock, Count: dice}}) effect: Effect{Action: ActionThrowRock, Count: r.dice}, source: &src})
} }
// Walk the pet's own play effects, then its active perk's, so a // Walk the pet's own play effects, then its active perk's, so a
// play-time shield knows its source (an innate pet block vs a // play-time shield knows its source (an innate pet block vs a
@@ -622,7 +639,7 @@ func (g *Game) resolveBattle() {
for _, v := range os.lastPetRocks { for _, v := range os.lastPetRocks {
src := v.src src := v.src
plays = append(plays, queuedPlay{seat: other, plays = append(plays, queuedPlay{seat: other,
effect: Effect{Action: ActionThrowRock, Count: v.dice}, release: &src}) effect: Effect{Action: ActionThrowRock, Count: v.dice}, release: &src, source: &src})
} }
os.lastPetRocks = nil os.lastPetRocks = nil
} }
@@ -669,12 +686,12 @@ func (g *Game) resolveBattle() {
} }
if q.everyone { if q.everyone {
for seat := range sides { for seat := range sides {
if throwRocks(q.seat, seat, dice) { if throwRocks(q.seat, seat, dice, q.source) {
anyDeath = true anyDeath = true
} }
} }
} else if t := nextTarget(q.seat); t >= 0 { } else if t := nextTarget(q.seat); t >= 0 {
if throwRocks(q.seat, t, dice) { if throwRocks(q.seat, t, dice, q.source) {
anyDeath = true anyDeath = true
} }
} }
+6
View File
@@ -588,6 +588,12 @@ func TestSnakeRecurringRocks(t *testing.T) {
if len(rocks) != 2 { if len(rocks) != 2 {
t.Fatalf("snake should volley on each friendly play: %+v", rocks) t.Fatalf("snake should volley on each friendly play: %+v", rocks)
} }
// The set-aside Snake threw the rocks, not the pet (A/B) in play.
for _, r := range rocks {
if !strings.Contains(r.Text, "Snake") {
t.Fatalf("rock event should credit Snake as the source, got %q", r.Text)
}
}
if res.WinnerSeat != -1 { if res.WinnerSeat != -1 {
t.Fatalf("expected draw, got %d", res.WinnerSeat) t.Fatalf("expected draw, got %d", res.WinnerSeat)
} }