Files
Catacombs/combat/combat.go
tolelom 7f29995833 feat: add secret rooms and mini-bosses on floors 4/9/14/19
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>
2026-03-25 15:30:21 +09:00

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
}