Add RoomSecret (5% chance) and RoomMiniBoss room types. Add 4 mini-boss monsters at 60% of boss stats (Guardian's Herald, Warden's Shadow, Overlord's Lieutenant, Archlich's Harbinger) with IsMiniBoss flag and boss pattern logic. Secret rooms grant double treasure. Mini-boss rooms are placed on floors 4/9/14/19 at room index 1. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
119 lines
2.8 KiB
Go
119 lines
2.8 KiB
Go
package combat
|
|
|
|
import (
|
|
"math"
|
|
"math/rand"
|
|
|
|
"github.com/tolelom/catacombs/entity"
|
|
)
|
|
|
|
// CalcDamage: max(1, ATK * multiplier - DEF) * random(0.85~1.15)
|
|
func CalcDamage(atk, def int, multiplier float64) int {
|
|
base := float64(atk)*multiplier - float64(def)
|
|
if base < 1 {
|
|
base = 1
|
|
}
|
|
randomFactor := 0.85 + rand.Float64()*0.30
|
|
return int(math.Round(base * randomFactor))
|
|
}
|
|
|
|
type AttackIntent struct {
|
|
PlayerATK int
|
|
TargetIdx int
|
|
Multiplier float64
|
|
IsAoE bool
|
|
}
|
|
|
|
type AttackResult struct {
|
|
TargetIdx int
|
|
Damage int
|
|
CoopApplied bool
|
|
IsAoE bool
|
|
}
|
|
|
|
func ResolveAttacks(intents []AttackIntent, monsters []*entity.Monster, coopBonus float64) []AttackResult {
|
|
targetCount := make(map[int]int)
|
|
targetOrder := make(map[int]int)
|
|
for i, intent := range intents {
|
|
if !intent.IsAoE {
|
|
targetCount[intent.TargetIdx]++
|
|
if _, ok := targetOrder[intent.TargetIdx]; !ok {
|
|
targetOrder[intent.TargetIdx] = i
|
|
}
|
|
}
|
|
}
|
|
|
|
results := make([]AttackResult, len(intents))
|
|
for i, intent := range intents {
|
|
if intent.IsAoE {
|
|
totalDmg := 0
|
|
for _, m := range monsters {
|
|
if !m.IsDead() {
|
|
dmg := CalcDamage(intent.PlayerATK, m.DEF, intent.Multiplier)
|
|
m.TakeDamage(dmg)
|
|
totalDmg += dmg
|
|
}
|
|
}
|
|
results[i] = AttackResult{TargetIdx: -1, Damage: totalDmg, IsAoE: true}
|
|
} else {
|
|
if intent.TargetIdx < 0 || intent.TargetIdx >= len(monsters) {
|
|
continue
|
|
}
|
|
m := monsters[intent.TargetIdx]
|
|
dmg := CalcDamage(intent.PlayerATK, m.DEF, intent.Multiplier)
|
|
coopApplied := false
|
|
if targetCount[intent.TargetIdx] >= 2 && targetOrder[intent.TargetIdx] != i {
|
|
dmg = int(math.Round(float64(dmg) * (1.0 + coopBonus)))
|
|
coopApplied = true
|
|
}
|
|
m.TakeDamage(dmg)
|
|
results[i] = AttackResult{
|
|
TargetIdx: intent.TargetIdx,
|
|
Damage: dmg,
|
|
CoopApplied: coopApplied,
|
|
}
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
func AttemptFlee(fleeChance float64) bool {
|
|
return rand.Float64() < fleeChance
|
|
}
|
|
|
|
func MonsterAI(m *entity.Monster, players []*entity.Player, turnNumber int) (targetIdx int, isAoE bool) {
|
|
if (m.IsBoss || m.IsMiniBoss) && turnNumber > 0 && turnNumber%3 == 0 {
|
|
return -1, true // AoE every 3 turns for all bosses and mini-bosses
|
|
}
|
|
if m.TauntTarget {
|
|
for i, p := range players {
|
|
if !p.IsDead() && p.Class == entity.ClassWarrior {
|
|
return i, false
|
|
}
|
|
}
|
|
// No living warrior found — clear taunt
|
|
m.TauntTarget = false
|
|
m.TauntTurns = 0
|
|
}
|
|
if rand.Float64() < 0.3 {
|
|
minHP := int(^uint(0) >> 1)
|
|
minIdx := -1
|
|
for i, p := range players {
|
|
if !p.IsDead() && p.HP < minHP {
|
|
minHP = p.HP
|
|
minIdx = i
|
|
}
|
|
}
|
|
if minIdx >= 0 {
|
|
return minIdx, false
|
|
}
|
|
// Fall through to default targeting if no alive player found
|
|
}
|
|
for i, p := range players {
|
|
if !p.IsDead() {
|
|
return i, false
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|