feat: apply relic passive effects (ATK/DEF boost, heal on kill, gold boost)
This commit is contained in:
@@ -82,6 +82,11 @@ func (p *Player) EffectiveATK() int {
|
|||||||
atk += item.Bonus
|
atk += item.Bonus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, r := range p.Relics {
|
||||||
|
if r.Effect == RelicATKBoost {
|
||||||
|
atk += r.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
return atk
|
return atk
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,5 +97,10 @@ func (p *Player) EffectiveDEF() int {
|
|||||||
def += item.Bonus
|
def += item.Bonus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, r := range p.Relics {
|
||||||
|
if r.Effect == RelicDEFBoost {
|
||||||
|
def += r.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
return def
|
return def
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,18 @@ func TestAllClasses(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRelicEffects(t *testing.T) {
|
||||||
|
p := NewPlayer("test", ClassWarrior)
|
||||||
|
p.Relics = append(p.Relics, Relic{Name: "Power Amulet", Effect: RelicATKBoost, Value: 3})
|
||||||
|
if p.EffectiveATK() != 15 {
|
||||||
|
t.Errorf("ATK with relic: got %d, want 15", p.EffectiveATK())
|
||||||
|
}
|
||||||
|
p.Relics = append(p.Relics, Relic{Name: "Iron Ward", Effect: RelicDEFBoost, Value: 2})
|
||||||
|
if p.EffectiveDEF() != 10 {
|
||||||
|
t.Errorf("DEF with relic: got %d, want 10", p.EffectiveDEF())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPlayerTakeDamage(t *testing.T) {
|
func TestPlayerTakeDamage(t *testing.T) {
|
||||||
p := NewPlayer("test", ClassWarrior)
|
p := NewPlayer("test", ClassWarrior)
|
||||||
p.TakeDamage(30)
|
p.TakeDamage(30)
|
||||||
|
|||||||
80
game/turn.go
80
game/turn.go
@@ -1,6 +1,7 @@
|
|||||||
package game
|
package game
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -15,6 +16,7 @@ func (s *GameSession) RunTurn() {
|
|||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.state.TurnNum++
|
s.state.TurnNum++
|
||||||
s.state.CombatTurn++
|
s.state.CombatTurn++
|
||||||
|
s.clearLog()
|
||||||
s.actions = make(map[string]PlayerAction)
|
s.actions = make(map[string]PlayerAction)
|
||||||
aliveCount := 0
|
aliveCount := 0
|
||||||
for _, p := range s.state.Players {
|
for _, p := range s.state.Players {
|
||||||
@@ -59,6 +61,7 @@ resolve:
|
|||||||
|
|
||||||
func (s *GameSession) resolvePlayerActions() {
|
func (s *GameSession) resolvePlayerActions() {
|
||||||
var intents []combat.AttackIntent
|
var intents []combat.AttackIntent
|
||||||
|
var intentOwners []string // track who owns each intent
|
||||||
|
|
||||||
// Track which monsters were alive before this turn (for gold awards)
|
// Track which monsters were alive before this turn (for gold awards)
|
||||||
aliveBeforeTurn := make(map[int]bool)
|
aliveBeforeTurn := make(map[int]bool)
|
||||||
@@ -68,7 +71,7 @@ func (s *GameSession) resolvePlayerActions() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if ALL alive players chose flee — only then the party flees
|
// Check if ALL alive players chose flee
|
||||||
fleeCount := 0
|
fleeCount := 0
|
||||||
aliveCount := 0
|
aliveCount := 0
|
||||||
for _, p := range s.state.Players {
|
for _, p := range s.state.Players {
|
||||||
@@ -82,10 +85,11 @@ func (s *GameSession) resolvePlayerActions() {
|
|||||||
}
|
}
|
||||||
if fleeCount == aliveCount && aliveCount > 0 {
|
if fleeCount == aliveCount && aliveCount > 0 {
|
||||||
if combat.AttemptFlee() {
|
if combat.AttemptFlee() {
|
||||||
|
s.addLog("Fled from battle!")
|
||||||
s.state.Phase = PhaseExploring
|
s.state.Phase = PhaseExploring
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Flee failed — all fleeing players waste their turn, continue to monster phase
|
s.addLog("Failed to flee!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,16 +110,17 @@ func (s *GameSession) resolvePlayerActions() {
|
|||||||
Multiplier: 1.0,
|
Multiplier: 1.0,
|
||||||
IsAoE: false,
|
IsAoE: false,
|
||||||
})
|
})
|
||||||
|
intentOwners = append(intentOwners, p.Name)
|
||||||
case ActionSkill:
|
case ActionSkill:
|
||||||
switch p.Class {
|
switch p.Class {
|
||||||
case entity.ClassWarrior:
|
case entity.ClassWarrior:
|
||||||
// Taunt: mark all monsters to target this warrior
|
|
||||||
for _, m := range s.state.Monsters {
|
for _, m := range s.state.Monsters {
|
||||||
if !m.IsDead() {
|
if !m.IsDead() {
|
||||||
m.TauntTarget = true
|
m.TauntTarget = true
|
||||||
m.TauntTurns = 2
|
m.TauntTurns = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
s.addLog(fmt.Sprintf("%s used Taunt! Enemies focus on %s for 2 turns", p.Name, p.Name))
|
||||||
case entity.ClassMage:
|
case entity.ClassMage:
|
||||||
intents = append(intents, combat.AttackIntent{
|
intents = append(intents, combat.AttackIntent{
|
||||||
PlayerATK: p.EffectiveATK(),
|
PlayerATK: p.EffectiveATK(),
|
||||||
@@ -123,35 +128,64 @@ func (s *GameSession) resolvePlayerActions() {
|
|||||||
Multiplier: 0.8,
|
Multiplier: 0.8,
|
||||||
IsAoE: true,
|
IsAoE: true,
|
||||||
})
|
})
|
||||||
|
intentOwners = append(intentOwners, p.Name)
|
||||||
case entity.ClassHealer:
|
case entity.ClassHealer:
|
||||||
if action.TargetIdx >= 0 && action.TargetIdx < len(s.state.Players) {
|
targetIdx := action.TargetIdx
|
||||||
s.state.Players[action.TargetIdx].Heal(30)
|
if targetIdx < 0 || targetIdx >= len(s.state.Players) {
|
||||||
|
targetIdx = 0 // heal self by default
|
||||||
}
|
}
|
||||||
|
target := s.state.Players[targetIdx]
|
||||||
|
before := target.HP
|
||||||
|
target.Heal(30)
|
||||||
|
s.addLog(fmt.Sprintf("%s healed %s for %d HP", p.Name, target.Name, target.HP-before))
|
||||||
case entity.ClassRogue:
|
case entity.ClassRogue:
|
||||||
// Scout: reveal neighboring rooms
|
|
||||||
currentRoom := s.state.Floor.Rooms[s.state.Floor.CurrentRoom]
|
currentRoom := s.state.Floor.Rooms[s.state.Floor.CurrentRoom]
|
||||||
for _, neighborIdx := range currentRoom.Neighbors {
|
for _, neighborIdx := range currentRoom.Neighbors {
|
||||||
s.state.Floor.Rooms[neighborIdx].Visited = true
|
s.state.Floor.Rooms[neighborIdx].Visited = true
|
||||||
}
|
}
|
||||||
|
s.addLog(fmt.Sprintf("%s scouted nearby rooms!", p.Name))
|
||||||
}
|
}
|
||||||
case ActionItem:
|
case ActionItem:
|
||||||
// Use first consumable from inventory
|
found := false
|
||||||
for i, item := range p.Inventory {
|
for i, item := range p.Inventory {
|
||||||
if item.Type == entity.ItemConsumable {
|
if item.Type == entity.ItemConsumable {
|
||||||
|
before := p.HP
|
||||||
p.Heal(item.Bonus)
|
p.Heal(item.Bonus)
|
||||||
p.Inventory = append(p.Inventory[:i], p.Inventory[i+1:]...)
|
p.Inventory = append(p.Inventory[:i], p.Inventory[i+1:]...)
|
||||||
|
s.addLog(fmt.Sprintf("%s used %s, restored %d HP", p.Name, item.Name, p.HP-before))
|
||||||
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !found {
|
||||||
|
s.addLog(fmt.Sprintf("%s has no items to use!", p.Name))
|
||||||
|
}
|
||||||
case ActionFlee:
|
case ActionFlee:
|
||||||
// Individual flee does nothing if not unanimous (already handled above)
|
s.addLog(fmt.Sprintf("%s tried to flee but party didn't agree", p.Name))
|
||||||
case ActionWait:
|
case ActionWait:
|
||||||
// Defensive stance — no action
|
s.addLog(fmt.Sprintf("%s is defending", p.Name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(intents) > 0 && len(s.state.Monsters) > 0 {
|
if len(intents) > 0 && len(s.state.Monsters) > 0 {
|
||||||
combat.ResolveAttacks(intents, s.state.Monsters)
|
results := combat.ResolveAttacks(intents, s.state.Monsters)
|
||||||
|
for i, r := range results {
|
||||||
|
owner := intentOwners[i]
|
||||||
|
if r.IsAoE {
|
||||||
|
coopStr := ""
|
||||||
|
if r.CoopApplied {
|
||||||
|
coopStr = " (co-op!)"
|
||||||
|
}
|
||||||
|
s.addLog(fmt.Sprintf("%s hit all enemies for %d total dmg%s", owner, r.Damage, coopStr))
|
||||||
|
} else if r.TargetIdx >= 0 && r.TargetIdx < len(s.state.Monsters) {
|
||||||
|
target := s.state.Monsters[r.TargetIdx]
|
||||||
|
coopStr := ""
|
||||||
|
if r.CoopApplied {
|
||||||
|
coopStr = " (co-op!)"
|
||||||
|
}
|
||||||
|
s.addLog(fmt.Sprintf("%s hit %s for %d dmg%s", owner, target.Name, r.Damage, coopStr))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Award gold only for monsters that JUST died this turn
|
// Award gold only for monsters that JUST died this turn
|
||||||
@@ -163,10 +197,20 @@ func (s *GameSession) resolvePlayerActions() {
|
|||||||
}
|
}
|
||||||
for _, p := range s.state.Players {
|
for _, p := range s.state.Players {
|
||||||
if !p.IsDead() {
|
if !p.IsDead() {
|
||||||
p.Gold += goldReward
|
bonus := 0
|
||||||
|
for _, r := range p.Relics {
|
||||||
|
if r.Effect == entity.RelicGoldBoost {
|
||||||
|
bonus += r.Value
|
||||||
|
}
|
||||||
|
if r.Effect == entity.RelicHealOnKill {
|
||||||
|
p.Heal(r.Value)
|
||||||
|
s.addLog(fmt.Sprintf("%s's relic heals %d HP", p.Name, r.Value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.Gold += goldReward + bonus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Boss kill: drop relic
|
s.addLog(fmt.Sprintf("%s defeated! +%d gold", m.Name, goldReward))
|
||||||
if m.IsBoss {
|
if m.IsBoss {
|
||||||
s.grantBossRelic()
|
s.grantBossRelic()
|
||||||
}
|
}
|
||||||
@@ -185,7 +229,7 @@ func (s *GameSession) resolvePlayerActions() {
|
|||||||
// Check if combat is over
|
// Check if combat is over
|
||||||
if len(s.state.Monsters) == 0 {
|
if len(s.state.Monsters) == 0 {
|
||||||
s.state.Floor.Rooms[s.state.Floor.CurrentRoom].Cleared = true
|
s.state.Floor.Rooms[s.state.Floor.CurrentRoom].Cleared = true
|
||||||
// Check if this was the boss room -> advance floor
|
s.addLog("Room cleared!")
|
||||||
if s.state.Floor.Rooms[s.state.Floor.CurrentRoom].Type == dungeon.RoomBoss {
|
if s.state.Floor.Rooms[s.state.Floor.CurrentRoom].Type == dungeon.RoomBoss {
|
||||||
s.advanceFloor()
|
s.advanceFloor()
|
||||||
} else {
|
} else {
|
||||||
@@ -199,13 +243,14 @@ func (s *GameSession) advanceFloor() {
|
|||||||
s.state.Phase = PhaseResult
|
s.state.Phase = PhaseResult
|
||||||
s.state.Victory = true
|
s.state.Victory = true
|
||||||
s.state.GameOver = true
|
s.state.GameOver = true
|
||||||
|
s.addLog("You conquered the Catacombs!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.state.FloorNum++
|
s.state.FloorNum++
|
||||||
s.state.Floor = dungeon.GenerateFloor(s.state.FloorNum)
|
s.state.Floor = dungeon.GenerateFloor(s.state.FloorNum)
|
||||||
s.state.Phase = PhaseExploring
|
s.state.Phase = PhaseExploring
|
||||||
s.state.CombatTurn = 0
|
s.state.CombatTurn = 0
|
||||||
// Revive dead players at 30% HP
|
s.addLog(fmt.Sprintf("Descending to B%d...", s.state.FloorNum))
|
||||||
for _, p := range s.state.Players {
|
for _, p := range s.state.Players {
|
||||||
if p.IsDead() {
|
if p.IsDead() {
|
||||||
p.Revive(0.30)
|
p.Revive(0.30)
|
||||||
@@ -224,6 +269,7 @@ func (s *GameSession) grantBossRelic() {
|
|||||||
if !p.IsDead() {
|
if !p.IsDead() {
|
||||||
r := relics[rand.Intn(len(relics))]
|
r := relics[rand.Intn(len(relics))]
|
||||||
p.Relics = append(p.Relics, r)
|
p.Relics = append(p.Relics, r)
|
||||||
|
s.addLog(fmt.Sprintf("%s obtained relic: %s", p.Name, r.Name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,11 +284,11 @@ func (s *GameSession) resolveMonsterActions() {
|
|||||||
}
|
}
|
||||||
targetIdx, isAoE := combat.MonsterAI(m, s.state.Players, s.state.CombatTurn)
|
targetIdx, isAoE := combat.MonsterAI(m, s.state.Players, s.state.CombatTurn)
|
||||||
if isAoE {
|
if isAoE {
|
||||||
// Boss AoE: 0.5x damage to all
|
|
||||||
for _, p := range s.state.Players {
|
for _, p := range s.state.Players {
|
||||||
if !p.IsDead() {
|
if !p.IsDead() {
|
||||||
dmg := combat.CalcDamage(m.ATK, p.EffectiveDEF(), 0.5)
|
dmg := combat.CalcDamage(m.ATK, p.EffectiveDEF(), 0.5)
|
||||||
p.TakeDamage(dmg)
|
p.TakeDamage(dmg)
|
||||||
|
s.addLog(fmt.Sprintf("%s AoE hits %s for %d dmg", m.Name, p.Name, dmg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -251,13 +297,13 @@ func (s *GameSession) resolveMonsterActions() {
|
|||||||
if !p.IsDead() {
|
if !p.IsDead() {
|
||||||
dmg := combat.CalcDamage(m.ATK, p.EffectiveDEF(), 1.0)
|
dmg := combat.CalcDamage(m.ATK, p.EffectiveDEF(), 1.0)
|
||||||
p.TakeDamage(dmg)
|
p.TakeDamage(dmg)
|
||||||
|
s.addLog(fmt.Sprintf("%s attacks %s for %d dmg", m.Name, p.Name, dmg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.TickTaunt()
|
m.TickTaunt()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check party wipe
|
|
||||||
allPlayersDead := true
|
allPlayersDead := true
|
||||||
for _, p := range s.state.Players {
|
for _, p := range s.state.Players {
|
||||||
if !p.IsDead() {
|
if !p.IsDead() {
|
||||||
@@ -267,5 +313,7 @@ func (s *GameSession) resolveMonsterActions() {
|
|||||||
}
|
}
|
||||||
if allPlayersDead {
|
if allPlayersDead {
|
||||||
s.state.Phase = PhaseResult
|
s.state.Phase = PhaseResult
|
||||||
|
s.state.GameOver = true
|
||||||
|
s.addLog("Party wiped!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user