Add pets for tiers 4-6.
This commit is contained in:
+362
-91
@@ -5,8 +5,9 @@ package game
|
||||
type BattleUnit struct {
|
||||
Card Card `json:"card"`
|
||||
Foods []Card `json:"foods,omitempty"`
|
||||
Bonus int `json:"bonus"` // total power added by foods (and eating)
|
||||
Bonus int `json:"bonus"` // total power added by foods, eating, auras
|
||||
Damage int `json:"damage"` // damage markers accumulated this battle
|
||||
Shield int `json:"shield"` // hits that will be fully prevented (Gorilla, Melon)
|
||||
}
|
||||
|
||||
func (u *BattleUnit) Power() int { return u.Card.Power + u.Bonus }
|
||||
@@ -43,15 +44,25 @@ func (u *BattleUnit) prevention() int {
|
||||
return total
|
||||
}
|
||||
|
||||
// takeHit applies one attack of the given strength and returns the damage
|
||||
// actually dealt after prevention.
|
||||
func (u *BattleUnit) takeHit(amount int) int {
|
||||
if amount <= 0 {
|
||||
return 0
|
||||
// hasKnockout reports whether the unit's clash attacks KO (Scorpion).
|
||||
func (u *BattleUnit) hasKnockout() bool {
|
||||
for _, e := range u.effects() {
|
||||
if e.Trigger == TriggerPassive && e.Action == ActionKnockout {
|
||||
return true
|
||||
}
|
||||
}
|
||||
dealt := max(0, amount-u.prevention())
|
||||
u.Damage += dealt
|
||||
return dealt
|
||||
return false
|
||||
}
|
||||
|
||||
// appleCount counts apples among the given foods.
|
||||
func appleCount(foods []Card) int {
|
||||
n := 0
|
||||
for _, f := range foods {
|
||||
if f.Food == FoodApple {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// isBee reports whether a card is a summoned Bee.
|
||||
@@ -59,19 +70,26 @@ func isBee(c Card) bool { return c.IsPet() && c.Name == "Bee" }
|
||||
|
||||
// BattleEvent is one step of the battle, in order, for clients to animate.
|
||||
type BattleEvent struct {
|
||||
// "prep": Card starts the battle already in play for Seat (Monkey).
|
||||
// "reveal": Seat flipped Card off their stack (food or pet).
|
||||
// "summon": an effect put Card on top of Seat's stack.
|
||||
// "mill": Card was discarded off the top of Seat's stack (Chili).
|
||||
// "rock": Seat's pet threw rocks at Target's pet in play.
|
||||
// "clash": the pets in play traded blows.
|
||||
// "shield": Seat's pet blocked a hit entirely.
|
||||
// "strip": Seat's pet discarded all of Target's pet's foods (Skunk).
|
||||
// "steal": Seat's pet stole Count apples from Target's pet (Wolverine).
|
||||
// "eat": Seat's pet ate apples; Bonus is its new total.
|
||||
// "heal": Seat's pet healed; DamageAfter is its new damage total.
|
||||
Type string `json:"type"`
|
||||
Seat int `json:"seat,omitempty"`
|
||||
Target int `json:"target,omitempty"`
|
||||
Card *Card `json:"card,omitempty"`
|
||||
Count int `json:"count,omitempty"`
|
||||
// clash: per-seat damage totals / deaths after the exchange.
|
||||
Damage []int `json:"damage,omitempty"`
|
||||
Died []bool `json:"died,omitempty"`
|
||||
// rock: dice total rolled and the target pet's resulting state.
|
||||
// rock: dice total rolled; rock/strip/steal: target pet's fate.
|
||||
Roll int `json:"roll"`
|
||||
DamageAfter int `json:"damageAfter,omitempty"`
|
||||
TargetDied bool `json:"targetDied,omitempty"`
|
||||
@@ -90,21 +108,64 @@ type BattleResult struct {
|
||||
Trophies int `json:"trophies"` // awarded to the winner
|
||||
}
|
||||
|
||||
// setAsideRocks is a fainted pet's pending rock payout.
|
||||
type setAsideRocks struct {
|
||||
dice int
|
||||
everyone bool // Badger: hits every active pet, the owner's own included
|
||||
}
|
||||
|
||||
// battleSide is one seat's live state during the simulation.
|
||||
type battleSide struct {
|
||||
stack []Card // remaining face-down cards, top first
|
||||
pending []Card // foods revealed and waiting for a pet
|
||||
unit *BattleUnit
|
||||
beesFainted int // friendly bees fainted so far (Dog)
|
||||
delayedRocks []int // set-aside Badgers: rocks to throw when the next pet plays
|
||||
stack []Card // remaining face-down cards, top first
|
||||
pending []Card // foods revealed (or prepped) waiting for a pet
|
||||
unit *BattleUnit
|
||||
beesFainted int // friendly bees fainted so far (Dog)
|
||||
petsFainted int // friendly pets fainted so far (Shark)
|
||||
shields int // Turtle charges: next friendly hit fully prevented
|
||||
beeBonus int // Turkey aura: +power for later friendly bees
|
||||
petBonus int // Mammoth aura: +power for later friendly pets
|
||||
|
||||
oneShotRocks []setAsideRocks // Badger/Blowfish: on next own pet play
|
||||
recurringRocks []int // Snake: on every own pet play
|
||||
lastPetRocks []int // Crocodile: when the enemy plays their last pet
|
||||
}
|
||||
|
||||
// hasPetInStack reports whether any pet remains face-down in the stack.
|
||||
func (s *battleSide) hasPetInStack() bool {
|
||||
for _, c := range s.stack {
|
||||
if c.IsPet() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// queuedPlay is a play-time effect waiting to resolve after reveals.
|
||||
type queuedPlay struct {
|
||||
seat int
|
||||
unit *BattleUnit // the pet whose play queued this (must still be in play for eats)
|
||||
effect Effect
|
||||
badger bool // delayedRocks payout: hits EVERY active pet, own included
|
||||
seat int
|
||||
unit *BattleUnit // the unit whose play queued this; nil for set-asides
|
||||
effect Effect
|
||||
everyone bool // rock volley hits every active pet (Badger)
|
||||
}
|
||||
|
||||
// effectCount resolves an effect's final count: base × Per statistic,
|
||||
// limited by Cap.
|
||||
func effectCount(e Effect, s *battleSide, u *BattleUnit) int {
|
||||
n := e.count()
|
||||
switch e.Per {
|
||||
case PerFaintedBees:
|
||||
n *= s.beesFainted
|
||||
case PerFaintedPets:
|
||||
n *= s.petsFainted
|
||||
case PerEatenApples:
|
||||
n *= appleCount(u.Foods)
|
||||
case PerPower:
|
||||
n *= u.Power()
|
||||
}
|
||||
if e.Cap > 0 && n > e.Cap {
|
||||
n = e.Cap
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// resolveBattle simulates the battle from the players' arranged decks,
|
||||
@@ -113,37 +174,100 @@ type queuedPlay struct {
|
||||
// The battle is a stack machine: each side reveals cards off the top of
|
||||
// their deck until a pet is in play (foods along the way attach to it; only
|
||||
// the last-applied perk counts). If anyone can no longer field a pet the
|
||||
// battle ends. Otherwise play effects resolve — rocks roll dice (faces
|
||||
// 0,0,1,1,2,2) and can faint a pet before the clash. Then the two pets deal
|
||||
// their full Power to each other simultaneously as damage markers; a pet
|
||||
// with Damage >= Power faints, firing Faint effects. Survivors that took
|
||||
// damage fire Hurt effects. Garlic prevents 1 damage from every attack that
|
||||
// hits its pet; a clash that deals no damage to anyone ends the battle as a
|
||||
// battle ends. Otherwise play effects resolve (rocks, strips, steals,
|
||||
// mills — any of which can faint a pet before the clash). Then the two pets
|
||||
// deal their full Power to each other simultaneously as damage markers; a
|
||||
// pet with Damage >= Power faints, firing Faint effects. Survivors that
|
||||
// took damage fire Hurt effects. Shields (Turtle, Gorilla, Melon) block
|
||||
// entire hits; Garlic shaves 1 from each; Scorpion KOs whatever its clash
|
||||
// attack manages to hurt. A clash that changes nothing ends the battle as a
|
||||
// stalemate.
|
||||
func (g *Game) resolveBattle() {
|
||||
n := len(g.Players)
|
||||
res := &BattleResult{Round: g.Round, WinnerSeat: -1, StackSizes: make([]int, n)}
|
||||
sides := make([]*battleSide, n)
|
||||
emit := func(ev BattleEvent) { res.Events = append(res.Events, ev) }
|
||||
|
||||
for _, p := range g.Players {
|
||||
sides[p.Seat] = &battleSide{stack: append([]Card(nil), p.Deck...)}
|
||||
s := &battleSide{stack: append([]Card(nil), p.Deck...)}
|
||||
sides[p.Seat] = s
|
||||
res.StackSizes[p.Seat] = len(p.Deck)
|
||||
}
|
||||
emit := func(ev BattleEvent) { res.Events = append(res.Events, ev) }
|
||||
// Battle-prep effects that start apples in play (Monkey): they attach
|
||||
// to the owner's first pet.
|
||||
for _, p := range g.Players {
|
||||
for _, c := range p.Deck {
|
||||
for _, e := range c.Effects {
|
||||
if e.Trigger == TriggerBattlePrep && e.Action == ActionApplesInPlay {
|
||||
for range e.count() {
|
||||
apple := g.newApple()
|
||||
sides[p.Seat].pending = append(sides[p.Seat].pending, apple)
|
||||
emit(BattleEvent{Type: "prep", Seat: p.Seat, Card: &apple})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
summon := func(seat int, c Card) {
|
||||
s := sides[seat]
|
||||
s.stack = append([]Card{c}, s.stack...)
|
||||
emit(BattleEvent{Type: "summon", Seat: seat, Card: &c})
|
||||
}
|
||||
mintFor := func(kind string) Card {
|
||||
if kind == "bee" {
|
||||
return g.newBee()
|
||||
}
|
||||
return g.newApple()
|
||||
}
|
||||
|
||||
// allowed gates battle-time effects on their conditions.
|
||||
allowed := func(e Effect, u *BattleUnit) bool {
|
||||
if g.Round < e.MinRound {
|
||||
return false
|
||||
}
|
||||
switch e.Condition {
|
||||
case ConditionHasPerk:
|
||||
return u != nil && u.activePerk() != nil
|
||||
case ConditionTripled:
|
||||
return false // shop-time condition; never true in battle
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// hitUnit applies one attack against a seat's pet: shields block the
|
||||
// whole hit, Garlic shaves per-attack damage. Returns damage dealt and
|
||||
// whether a shield blocked it.
|
||||
hitUnit := func(seat, amount int) (dealt int, blocked bool) {
|
||||
u := sides[seat].unit
|
||||
if u == nil || amount <= 0 {
|
||||
return 0, false
|
||||
}
|
||||
if u.Shield > 0 {
|
||||
u.Shield--
|
||||
return 0, true
|
||||
}
|
||||
if sides[seat].shields > 0 {
|
||||
sides[seat].shields--
|
||||
return 0, true
|
||||
}
|
||||
dealt = max(0, amount-u.prevention())
|
||||
u.Damage += dealt
|
||||
return dealt, false
|
||||
}
|
||||
|
||||
// faint fires the unit's faint effects (its own and its perk's) in
|
||||
// effect order, and tracks fainted bees.
|
||||
faint := func(seat int, u *BattleUnit) {
|
||||
// effect order, updates faint counters, and notifies enemy pets
|
||||
// (Hippo's heal).
|
||||
var faint func(seat int, u *BattleUnit)
|
||||
faint = func(seat int, u *BattleUnit) {
|
||||
s := sides[seat]
|
||||
s.petsFainted++
|
||||
if isBee(u.Card) {
|
||||
sides[seat].beesFainted++
|
||||
s.beesFainted++
|
||||
}
|
||||
for _, e := range u.effects() {
|
||||
if e.Trigger != TriggerFaint {
|
||||
if e.Trigger != TriggerFaint || !allowed(e, u) {
|
||||
continue
|
||||
}
|
||||
switch e.Action {
|
||||
@@ -152,12 +276,8 @@ func (g *Game) resolveBattle() {
|
||||
if e.Target == "enemy" {
|
||||
target = (seat + 1) % n
|
||||
}
|
||||
for range e.count() {
|
||||
if e.Card == "bee" {
|
||||
summon(target, g.newBee())
|
||||
} else {
|
||||
summon(target, g.newApple())
|
||||
}
|
||||
for range effectCount(e, s, u) {
|
||||
summon(target, mintFor(e.Card))
|
||||
}
|
||||
case ActionRecycleApples:
|
||||
recycled := 0
|
||||
@@ -168,7 +288,34 @@ func (g *Game) resolveBattle() {
|
||||
}
|
||||
}
|
||||
case ActionDelayedRocks:
|
||||
sides[seat].delayedRocks = append(sides[seat].delayedRocks, e.count())
|
||||
s.oneShotRocks = append(s.oneShotRocks,
|
||||
setAsideRocks{dice: e.count(), everyone: e.Target == "all"})
|
||||
case ActionRecurringRocks:
|
||||
s.recurringRocks = append(s.recurringRocks, e.count())
|
||||
case ActionEnemyLastPetRocks:
|
||||
s.lastPetRocks = append(s.lastPetRocks, e.count())
|
||||
case ActionShieldNext:
|
||||
s.shields += e.count()
|
||||
case ActionBeeAura:
|
||||
s.beeBonus += e.count()
|
||||
case ActionPetAura:
|
||||
s.petBonus += e.count()
|
||||
}
|
||||
}
|
||||
// Enemy Faints triggers on surviving pets elsewhere (Hippo).
|
||||
for other, os := range sides {
|
||||
if other == seat || os.unit == nil || !os.unit.Alive() {
|
||||
continue
|
||||
}
|
||||
for _, e := range os.unit.effects() {
|
||||
if e.Trigger != TriggerEnemyFaint || e.Action != ActionHeal || !allowed(e, os.unit) {
|
||||
continue
|
||||
}
|
||||
healed := min(effectCount(e, os, os.unit), os.unit.Damage)
|
||||
if healed > 0 {
|
||||
os.unit.Damage -= healed
|
||||
emit(BattleEvent{Type: "heal", Seat: other, DamageAfter: os.unit.Damage})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,30 +326,28 @@ func (g *Game) resolveBattle() {
|
||||
return
|
||||
}
|
||||
for _, e := range u.effects() {
|
||||
if e.Trigger != TriggerHurt {
|
||||
if e.Trigger != TriggerHurt || !allowed(e, u) {
|
||||
continue
|
||||
}
|
||||
switch e.Action {
|
||||
case ActionEatApple:
|
||||
for range e.count() {
|
||||
for range effectCount(e, sides[seat], u) {
|
||||
u.Foods = append(u.Foods, g.newApple())
|
||||
u.Bonus++
|
||||
}
|
||||
emit(BattleEvent{Type: "eat", Seat: seat, Bonus: u.Bonus})
|
||||
case ActionSummonTop:
|
||||
for range e.count() {
|
||||
if e.Card == "bee" {
|
||||
summon(seat, g.newBee())
|
||||
} else {
|
||||
summon(seat, g.newApple())
|
||||
}
|
||||
for range effectCount(e, sides[seat], u) {
|
||||
summon(seat, mintFor(e.Card))
|
||||
}
|
||||
case ActionShieldSelf:
|
||||
u.Shield += e.count()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// throwRocks rolls `dice` rock dice against one target's pet, handling
|
||||
// prevention, death (with faint), and hurt. Reports whether it killed.
|
||||
// throwRocks rolls `dice` rock dice against one seat's pet. Rocks are
|
||||
// not "attacks with" the pet, so no knockout applies. Reports a kill.
|
||||
throwRocks := func(from, target, dice int) (killed bool) {
|
||||
tu := sides[target].unit
|
||||
if tu == nil {
|
||||
@@ -212,12 +357,15 @@ func (g *Game) resolveBattle() {
|
||||
for range dice {
|
||||
roll += g.rollRockDie()
|
||||
}
|
||||
dealt := tu.takeHit(roll)
|
||||
dealt, blocked := hitUnit(target, roll)
|
||||
died := !tu.Alive()
|
||||
emit(BattleEvent{
|
||||
Type: "rock", Seat: from, Target: target, Roll: roll,
|
||||
DamageAfter: tu.Damage, TargetDied: died,
|
||||
})
|
||||
if blocked {
|
||||
emit(BattleEvent{Type: "shield", Seat: target})
|
||||
}
|
||||
if died {
|
||||
faint(target, tu)
|
||||
sides[target].unit = nil
|
||||
@@ -229,41 +377,95 @@ func (g *Game) resolveBattle() {
|
||||
return false
|
||||
}
|
||||
|
||||
// nextTarget finds the seat whose pet a standard enemy-directed play
|
||||
// effect hits.
|
||||
nextTarget := func(from int) int {
|
||||
for off := 1; off < n; off++ {
|
||||
cand := (from + off) % n
|
||||
if sides[cand].unit != nil {
|
||||
return cand
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// The exchange loop terminates: every clash kills at least one pet or
|
||||
// is a detected stalemate, and summons are finite. The guard is just
|
||||
// insurance as effects get richer.
|
||||
// is a detected stalemate, and summons/auras are finite. The guard is
|
||||
// just insurance as effects get richer.
|
||||
for range 10_000 {
|
||||
// Reveal until every side has a pet in play or runs out. Play
|
||||
// effects queue up and resolve after all reveals (simultaneous).
|
||||
var plays []queuedPlay
|
||||
newlyPlayed := make([]bool, n)
|
||||
for seat, s := range sides {
|
||||
for s.unit == nil && len(s.stack) > 0 {
|
||||
c := s.stack[0]
|
||||
s.stack = s.stack[1:]
|
||||
emit(BattleEvent{Type: "reveal", Seat: seat, Card: &c})
|
||||
if c.IsFood() {
|
||||
emit(BattleEvent{Type: "reveal", Seat: seat, Card: &c})
|
||||
s.pending = append(s.pending, c)
|
||||
continue
|
||||
}
|
||||
u := &BattleUnit{Card: c, Foods: s.pending}
|
||||
for _, f := range s.pending {
|
||||
if f.Food == FoodApple {
|
||||
u.Bonus++
|
||||
}
|
||||
u.Bonus += appleCount(s.pending)
|
||||
u.Bonus += s.petBonus
|
||||
if isBee(c) {
|
||||
u.Bonus += s.beeBonus
|
||||
}
|
||||
// Pets carry their starting bonus (foods + auras) in the
|
||||
// reveal event so clients can display it directly.
|
||||
emit(BattleEvent{Type: "reveal", Seat: seat, Card: &c, Bonus: u.Bonus})
|
||||
s.pending = nil
|
||||
s.unit = u
|
||||
// Set-aside Badgers pay out when the next pet plays,
|
||||
// before that pet's own play effects.
|
||||
for _, dice := range s.delayedRocks {
|
||||
plays = append(plays, queuedPlay{seat: seat, unit: u, badger: true,
|
||||
newlyPlayed[seat] = true
|
||||
// Set-aside payouts fire before the new pet's own play
|
||||
// effects.
|
||||
for _, r := range s.oneShotRocks {
|
||||
plays = append(plays, queuedPlay{seat: seat, everyone: r.everyone,
|
||||
effect: Effect{Action: ActionThrowRock, Count: r.dice}})
|
||||
}
|
||||
s.oneShotRocks = nil
|
||||
for _, dice := range s.recurringRocks {
|
||||
plays = append(plays, queuedPlay{seat: seat,
|
||||
effect: Effect{Action: ActionThrowRock, Count: dice}})
|
||||
}
|
||||
s.delayedRocks = nil
|
||||
for _, e := range u.effects() {
|
||||
if e.Trigger == TriggerPlay {
|
||||
plays = append(plays, queuedPlay{seat: seat, unit: u, effect: e})
|
||||
if e.Trigger != TriggerPlay {
|
||||
continue
|
||||
}
|
||||
// Shields apply the instant the pet enters play, ahead
|
||||
// of any queued rocks (Melon).
|
||||
if e.Action == ActionShieldSelf {
|
||||
u.Shield += e.count()
|
||||
continue
|
||||
}
|
||||
plays = append(plays, queuedPlay{seat: seat, unit: u, effect: e})
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cross-side play triggers: Rhino rocks anyone who just played;
|
||||
// Crocodile volleys when the enemy plays their last pet.
|
||||
for seat := range sides {
|
||||
if !newlyPlayed[seat] {
|
||||
continue
|
||||
}
|
||||
for other, os := range sides {
|
||||
if other == seat {
|
||||
continue
|
||||
}
|
||||
if os.unit != nil {
|
||||
for _, e := range os.unit.effects() {
|
||||
if e.Trigger == TriggerEnemyPlay && e.Action == ActionThrowRock && allowed(e, os.unit) {
|
||||
plays = append(plays, queuedPlay{seat: other, unit: os.unit, effect: e})
|
||||
}
|
||||
}
|
||||
}
|
||||
if !sides[seat].hasPetInStack() {
|
||||
for _, dice := range os.lastPetRocks {
|
||||
plays = append(plays, queuedPlay{seat: other,
|
||||
effect: Effect{Action: ActionThrowRock, Count: dice}})
|
||||
}
|
||||
os.lastPetRocks = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,41 +482,96 @@ func (g *Game) resolveBattle() {
|
||||
break
|
||||
}
|
||||
|
||||
// Resolve play effects. Rocks land before the clash and can faint
|
||||
// pets outright.
|
||||
rockDeath := false
|
||||
// Resolve play effects. Any of these can faint a pet before the
|
||||
// clash.
|
||||
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 {
|
||||
continue
|
||||
}
|
||||
if q.unit != nil && !allowed(q.effect, q.unit) {
|
||||
continue
|
||||
}
|
||||
switch q.effect.Action {
|
||||
case ActionThrowRock:
|
||||
if q.badger {
|
||||
// At EACH active pet, the owner's own included.
|
||||
dice := q.effect.count()
|
||||
if q.unit != nil {
|
||||
dice = effectCount(q.effect, sides[q.seat], q.unit)
|
||||
}
|
||||
if q.everyone {
|
||||
for seat := range sides {
|
||||
if throwRocks(q.seat, seat, q.effect.count()) {
|
||||
rockDeath = true
|
||||
if throwRocks(q.seat, seat, dice) {
|
||||
anyDeath = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
target := -1
|
||||
for off := 1; off < n; off++ {
|
||||
cand := (q.seat + off) % n
|
||||
if sides[cand].unit != nil {
|
||||
target = cand
|
||||
break
|
||||
}
|
||||
}
|
||||
if target >= 0 && throwRocks(q.seat, target, q.effect.count()) {
|
||||
rockDeath = true
|
||||
} else if t := nextTarget(q.seat); t >= 0 {
|
||||
if throwRocks(q.seat, t, dice) {
|
||||
anyDeath = true
|
||||
}
|
||||
}
|
||||
case ActionEatApple:
|
||||
// Skip if the eater already died (e.g. to Badger rocks).
|
||||
if sides[q.seat].unit != q.unit {
|
||||
case ActionStripFoods:
|
||||
t := nextTarget(q.seat)
|
||||
if t < 0 {
|
||||
continue
|
||||
}
|
||||
count := q.effect.count()
|
||||
if q.effect.Per == PerFaintedBees {
|
||||
count *= sides[q.seat].beesFainted
|
||||
tu := sides[t].unit
|
||||
tu.Bonus -= appleCount(tu.Foods)
|
||||
tu.Foods = nil
|
||||
died := !tu.Alive()
|
||||
emit(BattleEvent{Type: "strip", Seat: q.seat, Target: t, TargetDied: died})
|
||||
if died {
|
||||
faint(t, tu)
|
||||
sides[t].unit = nil
|
||||
anyDeath = true
|
||||
}
|
||||
case ActionStealApples:
|
||||
t := nextTarget(q.seat)
|
||||
if t < 0 {
|
||||
continue
|
||||
}
|
||||
tu := sides[t].unit
|
||||
steal := min(q.effect.count(), appleCount(tu.Foods))
|
||||
if steal == 0 {
|
||||
continue
|
||||
}
|
||||
moved := 0
|
||||
kept := tu.Foods[:0]
|
||||
for _, f := range tu.Foods {
|
||||
if f.Food == FoodApple && moved < steal {
|
||||
q.unit.Foods = append(q.unit.Foods, f)
|
||||
moved++
|
||||
continue
|
||||
}
|
||||
kept = append(kept, f)
|
||||
}
|
||||
tu.Foods = kept
|
||||
tu.Bonus -= moved
|
||||
q.unit.Bonus += moved
|
||||
died := !tu.Alive()
|
||||
emit(BattleEvent{Type: "steal", Seat: q.seat, Target: t, Count: moved, TargetDied: died})
|
||||
if died {
|
||||
faint(t, tu)
|
||||
sides[t].unit = nil
|
||||
anyDeath = true
|
||||
}
|
||||
case ActionMillEnemy:
|
||||
t := (q.seat + 1) % n
|
||||
ts := sides[t]
|
||||
for len(ts.stack) > 0 {
|
||||
top := ts.stack[0]
|
||||
if top.IsPet() && !isBee(top) {
|
||||
break
|
||||
}
|
||||
ts.stack = ts.stack[1:]
|
||||
emit(BattleEvent{Type: "mill", Seat: t, Card: &top})
|
||||
}
|
||||
case ActionSummonTop:
|
||||
for range effectCount(q.effect, sides[q.seat], q.unit) {
|
||||
summon(q.seat, mintFor(q.effect.Card))
|
||||
}
|
||||
case ActionEatApple:
|
||||
count := effectCount(q.effect, sides[q.seat], q.unit)
|
||||
if count > 0 {
|
||||
for range count {
|
||||
q.unit.Foods = append(q.unit.Foods, g.newApple())
|
||||
@@ -324,21 +581,35 @@ func (g *Game) resolveBattle() {
|
||||
}
|
||||
}
|
||||
}
|
||||
if rockDeath {
|
||||
if anyDeath {
|
||||
continue // refill before any clash
|
||||
}
|
||||
|
||||
// Clash. Two-player for now; >2-player battle pairings come later
|
||||
// (the surrounding state is already per-seat).
|
||||
ua, ub := sides[0].unit, sides[1].unit
|
||||
dealtA := ua.takeHit(ub.Power())
|
||||
dealtB := ub.takeHit(ua.Power())
|
||||
powA, powB := ua.Power(), ub.Power()
|
||||
dealtA, blockedA := hitUnit(0, powB)
|
||||
dealtB, blockedB := hitUnit(1, powA)
|
||||
// Scorpion: a clash attack that hurts, KOs.
|
||||
if dealtA > 0 && ub.hasKnockout() {
|
||||
ua.Damage = max(ua.Damage, ua.Power())
|
||||
}
|
||||
if dealtB > 0 && ua.hasKnockout() {
|
||||
ub.Damage = max(ub.Damage, ub.Power())
|
||||
}
|
||||
emit(BattleEvent{
|
||||
Type: "clash",
|
||||
Damage: []int{ua.Damage, ub.Damage},
|
||||
Died: []bool{!ua.Alive(), !ub.Alive()},
|
||||
})
|
||||
if ua.Alive() && ub.Alive() && dealtA == 0 && dealtB == 0 {
|
||||
if blockedA {
|
||||
emit(BattleEvent{Type: "shield", Seat: 0})
|
||||
}
|
||||
if blockedB {
|
||||
emit(BattleEvent{Type: "shield", Seat: 1})
|
||||
}
|
||||
if ua.Alive() && ub.Alive() && dealtA == 0 && dealtB == 0 && !blockedA && !blockedB {
|
||||
break // stalemate: nothing can ever change
|
||||
}
|
||||
dealt := []int{dealtA, dealtB}
|
||||
|
||||
Reference in New Issue
Block a user