feat: game session, turn system, lobby, and room events
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
138
game/event.go
Normal file
138
game/event.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
|
||||
"github.com/tolelom/catacombs/dungeon"
|
||||
"github.com/tolelom/catacombs/entity"
|
||||
)
|
||||
|
||||
func (s *GameSession) EnterRoom(roomIdx int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.state.Floor.CurrentRoom = roomIdx
|
||||
dungeon.UpdateVisibility(s.state.Floor)
|
||||
room := s.state.Floor.Rooms[roomIdx]
|
||||
|
||||
if room.Cleared {
|
||||
return
|
||||
}
|
||||
|
||||
switch room.Type {
|
||||
case dungeon.RoomCombat:
|
||||
s.spawnMonsters()
|
||||
s.state.Phase = PhaseCombat
|
||||
s.state.CombatTurn = 0
|
||||
case dungeon.RoomBoss:
|
||||
s.spawnBoss()
|
||||
s.state.Phase = PhaseCombat
|
||||
s.state.CombatTurn = 0
|
||||
case dungeon.RoomShop:
|
||||
s.generateShopItems()
|
||||
s.state.Phase = PhaseShop
|
||||
case dungeon.RoomTreasure:
|
||||
s.grantTreasure()
|
||||
room.Cleared = true
|
||||
case dungeon.RoomEvent:
|
||||
s.triggerEvent()
|
||||
room.Cleared = true
|
||||
case dungeon.RoomEmpty:
|
||||
room.Cleared = true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GameSession) spawnMonsters() {
|
||||
count := 1 + rand.Intn(5) // 1~5 monsters
|
||||
floor := s.state.FloorNum
|
||||
s.state.Monsters = make([]*entity.Monster, count)
|
||||
|
||||
// Pick appropriate monster type for floor
|
||||
var mt entity.MonsterType
|
||||
switch {
|
||||
case floor <= 5:
|
||||
mt = entity.MonsterSlime
|
||||
case floor <= 10:
|
||||
mt = entity.MonsterSkeleton
|
||||
case floor <= 14:
|
||||
mt = entity.MonsterOrc
|
||||
default:
|
||||
mt = entity.MonsterDarkKnight
|
||||
}
|
||||
|
||||
// Solo mode: 50% HP
|
||||
for i := 0; i < count; i++ {
|
||||
m := entity.NewMonster(mt, floor)
|
||||
if s.state.SoloMode {
|
||||
m.HP = m.HP / 2
|
||||
if m.HP < 1 {
|
||||
m.HP = 1
|
||||
}
|
||||
m.MaxHP = m.HP
|
||||
}
|
||||
s.state.Monsters[i] = m
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GameSession) spawnBoss() {
|
||||
var mt entity.MonsterType
|
||||
switch s.state.FloorNum {
|
||||
case 5:
|
||||
mt = entity.MonsterBoss5
|
||||
case 10:
|
||||
mt = entity.MonsterBoss10
|
||||
case 15:
|
||||
mt = entity.MonsterBoss15
|
||||
case 20:
|
||||
mt = entity.MonsterBoss20
|
||||
default:
|
||||
mt = entity.MonsterBoss5
|
||||
}
|
||||
boss := entity.NewMonster(mt, s.state.FloorNum)
|
||||
if s.state.SoloMode {
|
||||
boss.HP = boss.HP / 2
|
||||
boss.MaxHP = boss.HP
|
||||
}
|
||||
s.state.Monsters = []*entity.Monster{boss}
|
||||
}
|
||||
|
||||
func (s *GameSession) grantTreasure() {
|
||||
// Random item for each player
|
||||
for _, p := range s.state.Players {
|
||||
if rand.Float64() < 0.5 {
|
||||
p.Inventory = append(p.Inventory, entity.Item{
|
||||
Name: "Iron Sword", Type: entity.ItemWeapon, Bonus: 3 + rand.Intn(6),
|
||||
})
|
||||
} else {
|
||||
p.Inventory = append(p.Inventory, entity.Item{
|
||||
Name: "Iron Shield", Type: entity.ItemArmor, Bonus: 2 + rand.Intn(4),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GameSession) generateShopItems() {
|
||||
s.state.ShopItems = []entity.Item{
|
||||
{Name: "HP Potion", Type: entity.ItemConsumable, Bonus: 30, Price: 20},
|
||||
{Name: "Iron Sword", Type: entity.ItemWeapon, Bonus: 3 + rand.Intn(6), Price: 40 + rand.Intn(41)},
|
||||
{Name: "Iron Shield", Type: entity.ItemArmor, Bonus: 2 + rand.Intn(4), Price: 30 + rand.Intn(31)},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GameSession) triggerEvent() {
|
||||
// Random event: 50% trap, 50% blessing
|
||||
for _, p := range s.state.Players {
|
||||
if p.IsDead() {
|
||||
continue
|
||||
}
|
||||
if rand.Float64() < 0.5 {
|
||||
// Trap: 10~20 damage
|
||||
dmg := 10 + rand.Intn(11)
|
||||
p.TakeDamage(dmg)
|
||||
} else {
|
||||
// Blessing: heal 15~25
|
||||
heal := 15 + rand.Intn(11)
|
||||
p.Heal(heal)
|
||||
}
|
||||
}
|
||||
}
|
||||
94
game/lobby.go
Normal file
94
game/lobby.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type RoomStatus int
|
||||
|
||||
const (
|
||||
RoomWaiting RoomStatus = iota
|
||||
RoomPlaying
|
||||
)
|
||||
|
||||
type LobbyRoom struct {
|
||||
Code string
|
||||
Name string
|
||||
Players []string
|
||||
Status RoomStatus
|
||||
Session *GameSession
|
||||
}
|
||||
|
||||
type Lobby struct {
|
||||
mu sync.RWMutex
|
||||
rooms map[string]*LobbyRoom
|
||||
}
|
||||
|
||||
func NewLobby() *Lobby {
|
||||
return &Lobby{rooms: make(map[string]*LobbyRoom)}
|
||||
}
|
||||
|
||||
func (l *Lobby) CreateRoom(name string) string {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
code := generateCode()
|
||||
for l.rooms[code] != nil {
|
||||
code = generateCode()
|
||||
}
|
||||
l.rooms[code] = &LobbyRoom{
|
||||
Code: code,
|
||||
Name: name,
|
||||
Status: RoomWaiting,
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
func (l *Lobby) JoinRoom(code, playerName string) error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
room, ok := l.rooms[code]
|
||||
if !ok {
|
||||
return fmt.Errorf("room %s not found", code)
|
||||
}
|
||||
if len(room.Players) >= 4 {
|
||||
return fmt.Errorf("room %s is full", code)
|
||||
}
|
||||
if room.Status != RoomWaiting {
|
||||
return fmt.Errorf("room %s already in progress", code)
|
||||
}
|
||||
room.Players = append(room.Players, playerName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Lobby) GetRoom(code string) *LobbyRoom {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
return l.rooms[code]
|
||||
}
|
||||
|
||||
func (l *Lobby) ListRooms() []*LobbyRoom {
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
result := make([]*LobbyRoom, 0, len(l.rooms))
|
||||
for _, r := range l.rooms {
|
||||
result = append(result, r)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (l *Lobby) RemoveRoom(code string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
delete(l.rooms, code)
|
||||
}
|
||||
|
||||
func generateCode() string {
|
||||
const letters = "ABCDEFGHJKLMNPQRSTUVWXYZ"
|
||||
b := make([]byte, 4)
|
||||
for i := range b {
|
||||
b[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
40
game/lobby_test.go
Normal file
40
game/lobby_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package game
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCreateRoom(t *testing.T) {
|
||||
lobby := NewLobby()
|
||||
code := lobby.CreateRoom("Test Room")
|
||||
if len(code) != 4 {
|
||||
t.Errorf("Room code length: got %d, want 4", len(code))
|
||||
}
|
||||
rooms := lobby.ListRooms()
|
||||
if len(rooms) != 1 {
|
||||
t.Errorf("Room count: got %d, want 1", len(rooms))
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoinRoom(t *testing.T) {
|
||||
lobby := NewLobby()
|
||||
code := lobby.CreateRoom("Test Room")
|
||||
err := lobby.JoinRoom(code, "player1")
|
||||
if err != nil {
|
||||
t.Errorf("Join failed: %v", err)
|
||||
}
|
||||
room := lobby.GetRoom(code)
|
||||
if len(room.Players) != 1 {
|
||||
t.Errorf("Player count: got %d, want 1", len(room.Players))
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoinRoomFull(t *testing.T) {
|
||||
lobby := NewLobby()
|
||||
code := lobby.CreateRoom("Test Room")
|
||||
for i := 0; i < 4; i++ {
|
||||
lobby.JoinRoom(code, "player")
|
||||
}
|
||||
err := lobby.JoinRoom(code, "player5")
|
||||
if err == nil {
|
||||
t.Error("Should reject 5th player")
|
||||
}
|
||||
}
|
||||
133
game/session.go
Normal file
133
game/session.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/tolelom/catacombs/dungeon"
|
||||
"github.com/tolelom/catacombs/entity"
|
||||
)
|
||||
|
||||
type GamePhase int
|
||||
|
||||
const (
|
||||
PhaseExploring GamePhase = iota
|
||||
PhaseCombat
|
||||
PhaseShop
|
||||
PhaseResult
|
||||
)
|
||||
|
||||
type PlayerAction struct {
|
||||
Type ActionType
|
||||
TargetIdx int
|
||||
}
|
||||
|
||||
type ActionType int
|
||||
|
||||
const (
|
||||
ActionAttack ActionType = iota
|
||||
ActionSkill
|
||||
ActionItem
|
||||
ActionFlee
|
||||
ActionWait
|
||||
)
|
||||
|
||||
type GameState struct {
|
||||
Floor *dungeon.Floor
|
||||
Players []*entity.Player
|
||||
Monsters []*entity.Monster
|
||||
Phase GamePhase
|
||||
FloorNum int
|
||||
TurnNum int
|
||||
CombatTurn int // reset per combat encounter
|
||||
SoloMode bool
|
||||
GameOver bool
|
||||
Victory bool
|
||||
ShopItems []entity.Item
|
||||
}
|
||||
|
||||
type GameSession struct {
|
||||
mu sync.Mutex
|
||||
state GameState
|
||||
actions map[string]PlayerAction // playerName -> action
|
||||
actionCh chan playerActionMsg
|
||||
}
|
||||
|
||||
type playerActionMsg struct {
|
||||
PlayerName string
|
||||
Action PlayerAction
|
||||
}
|
||||
|
||||
func NewGameSession() *GameSession {
|
||||
return &GameSession{
|
||||
state: GameState{
|
||||
FloorNum: 1,
|
||||
},
|
||||
actions: make(map[string]PlayerAction),
|
||||
actionCh: make(chan playerActionMsg, 4),
|
||||
}
|
||||
}
|
||||
|
||||
// StartGame determines solo mode from actual player count at game start
|
||||
func (s *GameSession) StartGame() {
|
||||
s.mu.Lock()
|
||||
s.state.SoloMode = len(s.state.Players) == 1
|
||||
s.mu.Unlock()
|
||||
s.StartFloor()
|
||||
}
|
||||
|
||||
func (s *GameSession) AddPlayer(p *entity.Player) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.state.Players = append(s.state.Players, p)
|
||||
}
|
||||
|
||||
func (s *GameSession) StartFloor() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.state.Floor = dungeon.GenerateFloor(s.state.FloorNum)
|
||||
s.state.Phase = PhaseExploring
|
||||
s.state.TurnNum = 0
|
||||
|
||||
// Revive dead players at 30% HP
|
||||
for _, p := range s.state.Players {
|
||||
if p.IsDead() {
|
||||
p.Revive(0.30)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GameSession) GetState() GameState {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.state
|
||||
}
|
||||
|
||||
func (s *GameSession) SubmitAction(playerName string, action PlayerAction) {
|
||||
s.actionCh <- playerActionMsg{PlayerName: playerName, Action: action}
|
||||
}
|
||||
|
||||
// BuyItem handles shop purchases
|
||||
func (s *GameSession) BuyItem(playerName string, itemIdx int) bool {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.state.Phase != PhaseShop || itemIdx < 0 || itemIdx >= len(s.state.ShopItems) {
|
||||
return false
|
||||
}
|
||||
item := s.state.ShopItems[itemIdx]
|
||||
for _, p := range s.state.Players {
|
||||
if p.Name == playerName && p.Gold >= item.Price {
|
||||
p.Gold -= item.Price
|
||||
p.Inventory = append(p.Inventory, item)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// LeaveShop exits the shop phase
|
||||
func (s *GameSession) LeaveShop() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.state.Phase = PhaseExploring
|
||||
s.state.Floor.Rooms[s.state.Floor.CurrentRoom].Cleared = true
|
||||
}
|
||||
29
game/session_test.go
Normal file
29
game/session_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tolelom/catacombs/entity"
|
||||
)
|
||||
|
||||
func TestSessionTurnTimeout(t *testing.T) {
|
||||
s := NewGameSession()
|
||||
p := entity.NewPlayer("test", entity.ClassWarrior)
|
||||
s.AddPlayer(p)
|
||||
s.StartFloor()
|
||||
|
||||
// Don't submit any action, wait for timeout
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
s.RunTurn()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
// Turn completed via timeout
|
||||
case <-time.After(7 * time.Second):
|
||||
t.Error("Turn did not timeout within 7 seconds")
|
||||
}
|
||||
}
|
||||
271
game/turn.go
Normal file
271
game/turn.go
Normal file
@@ -0,0 +1,271 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user