diff --git a/combat/combat.go b/combat/combat.go index 4ae2c40..78873dc 100644 --- a/combat/combat.go +++ b/combat/combat.go @@ -82,8 +82,8 @@ func AttemptFlee() bool { } func MonsterAI(m *entity.Monster, players []*entity.Player, turnNumber int) (targetIdx int, isAoE bool) { - if m.IsBoss && turnNumber%3 == 0 { - return -1, true + if m.IsBoss && turnNumber > 0 && turnNumber%3 == 0 { + return -1, true // AoE every 3 turns for all bosses } if m.TauntTarget { for i, p := range players { diff --git a/entity/item.go b/entity/item.go index 837a5a1..59aaa0d 100644 --- a/entity/item.go +++ b/entity/item.go @@ -22,6 +22,9 @@ const ( RelicATKBoost RelicDEFBoost RelicGoldBoost + RelicPoisonImmunity // immune to poison + RelicBurnResist // halve burn damage + RelicLifeSteal // heal 10% of damage dealt ) type Relic struct { diff --git a/entity/monster.go b/entity/monster.go index bf0b46f..d2155df 100644 --- a/entity/monster.go +++ b/entity/monster.go @@ -33,6 +33,16 @@ var monsterDefs = map[MonsterType]monsterBase{ MonsterBoss20: {"Archlich", 600, 40, 20, 20, true}, } +type BossPattern int + +const ( + PatternNone BossPattern = iota + PatternAoE // every 3 turns AoE + PatternPoison // applies poison + PatternBurn // applies burn to random player + PatternHeal // heals self +) + type Monster struct { Name string Type MonsterType @@ -41,6 +51,7 @@ type Monster struct { IsBoss bool TauntTarget bool TauntTurns int + Pattern BossPattern } func NewMonster(mt MonsterType, floor int) *Monster { diff --git a/entity/player.go b/entity/player.go index 575935d..939c7db 100644 --- a/entity/player.go +++ b/entity/player.go @@ -1,5 +1,7 @@ package entity +import "fmt" + type Class int const ( @@ -24,6 +26,20 @@ var classBaseStats = map[Class]classStats{ ClassRogue: {85, 15, 4}, } +type StatusEffect int + +const ( + StatusPoison StatusEffect = iota + StatusBurn + StatusFreeze +) + +type ActiveEffect struct { + Type StatusEffect + Duration int // remaining turns + Value int // damage per turn or effect strength +} + type Player struct { Name string Fingerprint string @@ -33,6 +49,7 @@ type Player struct { Gold int Inventory []Item Relics []Relic + Effects []ActiveEffect Dead bool Fled bool SkillUses int // remaining skill uses this combat @@ -110,3 +127,59 @@ func (p *Player) EffectiveDEF() int { } return def } + +func (p *Player) AddEffect(e ActiveEffect) { + // Check relic immunities + for _, r := range p.Relics { + if e.Type == StatusPoison && r.Effect == RelicPoisonImmunity { + return // immune + } + if e.Type == StatusBurn && r.Effect == RelicBurnResist { + e.Value = e.Value / 2 // halve burn damage + } + } + // Don't stack same type, refresh duration + for i, existing := range p.Effects { + if existing.Type == e.Type { + p.Effects[i] = e + return + } + } + p.Effects = append(p.Effects, e) +} + +func (p *Player) HasEffect(t StatusEffect) bool { + for _, e := range p.Effects { + if e.Type == t { + return true + } + } + return false +} + +func (p *Player) TickEffects() (damages []string) { + var remaining []ActiveEffect + for _, e := range p.Effects { + switch e.Type { + case StatusPoison: + p.HP -= e.Value + if p.HP <= 0 { + p.HP = 1 // Poison can't kill, leaves at 1 HP + } + damages = append(damages, fmt.Sprintf("%s takes %d poison damage", p.Name, e.Value)) + case StatusBurn: + p.HP -= e.Value + if p.HP <= 0 { + p.HP = 0 + p.Dead = true + } + damages = append(damages, fmt.Sprintf("%s takes %d burn damage", p.Name, e.Value)) + } + e.Duration-- + if e.Duration > 0 { + remaining = append(remaining, e) + } + } + p.Effects = remaining + return +} diff --git a/game/event.go b/game/event.go index 01b64df..8157354 100644 --- a/game/event.go +++ b/game/event.go @@ -114,6 +114,16 @@ func (s *GameSession) spawnBoss() { mt = entity.MonsterBoss5 } boss := entity.NewMonster(mt, s.state.FloorNum) + switch mt { + case entity.MonsterBoss5: + boss.Pattern = entity.PatternAoE + case entity.MonsterBoss10: + boss.Pattern = entity.PatternPoison + case entity.MonsterBoss15: + boss.Pattern = entity.PatternBurn + case entity.MonsterBoss20: + boss.Pattern = entity.PatternHeal + } if s.state.SoloMode { boss.HP = boss.HP / 2 boss.MaxHP = boss.HP diff --git a/game/session.go b/game/session.go index 3a55001..df28a4b 100644 --- a/game/session.go +++ b/game/session.go @@ -220,6 +220,8 @@ func (s *GameSession) GetState() GameState { copy(cp.Inventory, p.Inventory) cp.Relics = make([]entity.Relic, len(p.Relics)) copy(cp.Relics, p.Relics) + cp.Effects = make([]entity.ActiveEffect, len(p.Effects)) + copy(cp.Effects, p.Effects) players[i] = &cp } diff --git a/game/turn.go b/game/turn.go index 38a8f6c..8110b72 100644 --- a/game/turn.go +++ b/game/turn.go @@ -73,6 +73,19 @@ collecting: } func (s *GameSession) resolvePlayerActions() { + // Tick status effects + for _, p := range s.state.Players { + if !p.IsOut() { + msgs := p.TickEffects() + for _, msg := range msgs { + s.addLog(msg) + } + if p.IsDead() { + s.addLog(fmt.Sprintf("☠ %s has fallen!", p.Name)) + } + } + } + var intents []combat.AttackIntent var intentOwners []string // track who owns each intent @@ -299,6 +312,9 @@ func (s *GameSession) grantBossRelic() { {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}, + {Name: "Antidote Charm", Effect: entity.RelicPoisonImmunity, Value: 0, Price: 100}, + {Name: "Flame Guard", Effect: entity.RelicBurnResist, Value: 0, Price: 120}, + {Name: "Life Siphon", Effect: entity.RelicLifeSteal, Value: 10, Price: 150}, } for _, p := range s.state.Players { if !p.IsOut() { @@ -329,6 +345,32 @@ func (s *GameSession) resolveMonsterActions() { } } } + if m.IsBoss { + // Boss special pattern + switch m.Pattern { + case entity.PatternPoison: + for _, p := range s.state.Players { + if !p.IsOut() { + p.AddEffect(entity.ActiveEffect{Type: entity.StatusPoison, Duration: 3, Value: 5}) + s.addLog(fmt.Sprintf("%s poisons %s!", m.Name, p.Name)) + } + } + case entity.PatternBurn: + for _, p := range s.state.Players { + if !p.IsOut() { + p.AddEffect(entity.ActiveEffect{Type: entity.StatusBurn, Duration: 2, Value: 8}) + s.addLog(fmt.Sprintf("%s burns %s!", m.Name, p.Name)) + } + } + case entity.PatternHeal: + healAmt := m.MaxHP / 10 + m.HP += healAmt + if m.HP > m.MaxHP { + m.HP = m.MaxHP + } + s.addLog(fmt.Sprintf("%s regenerates %d HP!", m.Name, healAmt)) + } + } } else { if targetIdx >= 0 && targetIdx < len(s.state.Players) { p := s.state.Players[targetIdx] diff --git a/ui/game_view.go b/ui/game_view.go index 527fe26..848b34b 100644 --- a/ui/game_view.go +++ b/ui/game_view.go @@ -248,6 +248,21 @@ func renderPartyPanel(players []*entity.Player, submittedActions map[string]stri hpBar := renderHPBar(p.HP, p.MaxHP, 16) sb.WriteString(fmt.Sprintf(" %s %d/%d\n", hpBar, p.HP, p.MaxHP)) + if len(p.Effects) > 0 { + var effects []string + for _, e := range p.Effects { + switch e.Type { + case entity.StatusPoison: + effects = append(effects, styleHeal.Render(fmt.Sprintf("☠Poison(%dt)", e.Duration))) + case entity.StatusBurn: + effects = append(effects, styleDamage.Render(fmt.Sprintf("🔥Burn(%dt)", e.Duration))) + case entity.StatusFreeze: + effects = append(effects, styleFlee.Render(fmt.Sprintf("❄Freeze(%dt)", e.Duration))) + } + } + sb.WriteString(" " + strings.Join(effects, " ") + "\n") + } + sb.WriteString(fmt.Sprintf(" ATK:%-3d DEF:%-3d ", p.EffectiveATK(), p.EffectiveDEF())) sb.WriteString(styleGold.Render(fmt.Sprintf("G:%d", p.Gold))) sb.WriteString("\n")