Files
Catacombs/game/turn.go
2026-03-23 23:55:08 +09:00

272 lines
6.2 KiB
Go

package game
import (
"math/rand"
"time"
"github.com/tolelom/catacombs/combat"
"github.com/tolelom/catacombs/dungeon"
"github.com/tolelom/catacombs/entity"
)
const TurnTimeout = 5 * time.Second
func (s *GameSession) RunTurn() {
s.mu.Lock()
s.state.TurnNum++
s.state.CombatTurn++
s.actions = make(map[string]PlayerAction)
aliveCount := 0
for _, p := range s.state.Players {
if !p.IsDead() {
aliveCount++
}
}
s.mu.Unlock()
// Collect actions with timeout
timer := time.NewTimer(TurnTimeout)
collected := 0
for collected < aliveCount {
select {
case msg := <-s.actionCh:
s.mu.Lock()
s.actions[msg.PlayerName] = msg.Action
s.mu.Unlock()
collected++
case <-timer.C:
goto resolve
}
}
timer.Stop()
resolve:
s.mu.Lock()
defer s.mu.Unlock()
// Default action for players who didn't submit: Wait
for _, p := range s.state.Players {
if !p.IsDead() {
if _, ok := s.actions[p.Name]; !ok {
s.actions[p.Name] = PlayerAction{Type: ActionWait}
}
}
}
s.resolvePlayerActions()
s.resolveMonsterActions()
}
func (s *GameSession) resolvePlayerActions() {
var intents []combat.AttackIntent
// Track which monsters were alive before this turn (for gold awards)
aliveBeforeTurn := make(map[int]bool)
for i, m := range s.state.Monsters {
if !m.IsDead() {
aliveBeforeTurn[i] = true
}
}
// Check if ALL alive players chose flee — only then the party flees
fleeCount := 0
aliveCount := 0
for _, p := range s.state.Players {
if p.IsDead() {
continue
}
aliveCount++
if action, ok := s.actions[p.Name]; ok && action.Type == ActionFlee {
fleeCount++
}
}
if fleeCount == aliveCount && aliveCount > 0 {
if combat.AttemptFlee() {
s.state.Phase = PhaseExploring
return
}
// Flee failed — all fleeing players waste their turn, continue to monster phase
return
}
for _, p := range s.state.Players {
if p.IsDead() {
continue
}
action, ok := s.actions[p.Name]
if !ok {
continue
}
switch action.Type {
case ActionAttack:
intents = append(intents, combat.AttackIntent{
PlayerATK: p.EffectiveATK(),
TargetIdx: action.TargetIdx,
Multiplier: 1.0,
IsAoE: false,
})
case ActionSkill:
switch p.Class {
case entity.ClassWarrior:
// Taunt: mark all monsters to target this warrior
for _, m := range s.state.Monsters {
if !m.IsDead() {
m.TauntTarget = true
m.TauntTurns = 2
}
}
case entity.ClassMage:
intents = append(intents, combat.AttackIntent{
PlayerATK: p.EffectiveATK(),
TargetIdx: -1,
Multiplier: 0.8,
IsAoE: true,
})
case entity.ClassHealer:
if action.TargetIdx >= 0 && action.TargetIdx < len(s.state.Players) {
s.state.Players[action.TargetIdx].Heal(30)
}
case entity.ClassRogue:
// Scout: reveal neighboring rooms
currentRoom := s.state.Floor.Rooms[s.state.Floor.CurrentRoom]
for _, neighborIdx := range currentRoom.Neighbors {
s.state.Floor.Rooms[neighborIdx].Visited = true
}
}
case ActionItem:
// Use first consumable from inventory
for i, item := range p.Inventory {
if item.Type == entity.ItemConsumable {
p.Heal(item.Bonus)
p.Inventory = append(p.Inventory[:i], p.Inventory[i+1:]...)
break
}
}
case ActionFlee:
// Individual flee does nothing if not unanimous (already handled above)
case ActionWait:
// Defensive stance — no action
}
}
if len(intents) > 0 && len(s.state.Monsters) > 0 {
combat.ResolveAttacks(intents, s.state.Monsters)
}
// Award gold only for monsters that JUST died this turn
for i, m := range s.state.Monsters {
if m.IsDead() && aliveBeforeTurn[i] {
goldReward := 5 + s.state.FloorNum
if goldReward > 15 {
goldReward = 15
}
for _, p := range s.state.Players {
if !p.IsDead() {
p.Gold += goldReward
}
}
// Boss kill: drop relic
if m.IsBoss {
s.grantBossRelic()
}
}
}
// Filter out dead monsters
alive := make([]*entity.Monster, 0)
for _, m := range s.state.Monsters {
if !m.IsDead() {
alive = append(alive, m)
}
}
s.state.Monsters = alive
// Check if combat is over
if len(s.state.Monsters) == 0 {
s.state.Floor.Rooms[s.state.Floor.CurrentRoom].Cleared = true
// Check if this was the boss room -> advance floor
if s.state.Floor.Rooms[s.state.Floor.CurrentRoom].Type == dungeon.RoomBoss {
s.advanceFloor()
} else {
s.state.Phase = PhaseExploring
}
}
}
func (s *GameSession) advanceFloor() {
if s.state.FloorNum >= 20 {
s.state.Phase = PhaseResult
s.state.Victory = true
s.state.GameOver = true
return
}
s.state.FloorNum++
s.state.Floor = dungeon.GenerateFloor(s.state.FloorNum)
s.state.Phase = PhaseExploring
s.state.CombatTurn = 0
// Revive dead players at 30% HP
for _, p := range s.state.Players {
if p.IsDead() {
p.Revive(0.30)
}
}
}
func (s *GameSession) grantBossRelic() {
relics := []entity.Relic{
{Name: "Vampiric Ring", Effect: entity.RelicHealOnKill, Value: 5, Price: 100},
{Name: "Power Amulet", Effect: entity.RelicATKBoost, Value: 3, Price: 120},
{Name: "Iron Ward", Effect: entity.RelicDEFBoost, Value: 2, Price: 100},
{Name: "Gold Charm", Effect: entity.RelicGoldBoost, Value: 5, Price: 150},
}
for _, p := range s.state.Players {
if !p.IsDead() {
r := relics[rand.Intn(len(relics))]
p.Relics = append(p.Relics, r)
}
}
}
func (s *GameSession) resolveMonsterActions() {
if s.state.Phase != PhaseCombat {
return
}
for _, m := range s.state.Monsters {
if m.IsDead() {
continue
}
targetIdx, isAoE := combat.MonsterAI(m, s.state.Players, s.state.CombatTurn)
if isAoE {
// Boss AoE: 0.5x damage to all
for _, p := range s.state.Players {
if !p.IsDead() {
dmg := combat.CalcDamage(m.ATK, p.EffectiveDEF(), 0.5)
p.TakeDamage(dmg)
}
}
} else {
if targetIdx >= 0 && targetIdx < len(s.state.Players) {
p := s.state.Players[targetIdx]
if !p.IsDead() {
dmg := combat.CalcDamage(m.ATK, p.EffectiveDEF(), 1.0)
p.TakeDamage(dmg)
}
}
}
m.TickTaunt()
}
// Check party wipe
allPlayersDead := true
for _, p := range s.state.Players {
if !p.IsDead() {
allPlayersDead = false
break
}
}
if allPlayersDead {
s.state.Phase = PhaseResult
}
}