feat: status effects (poison/burn), boss patterns, new relics
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -82,8 +82,8 @@ func AttemptFlee() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func MonsterAI(m *entity.Monster, players []*entity.Player, turnNumber int) (targetIdx int, isAoE bool) {
|
func MonsterAI(m *entity.Monster, players []*entity.Player, turnNumber int) (targetIdx int, isAoE bool) {
|
||||||
if m.IsBoss && turnNumber%3 == 0 {
|
if m.IsBoss && turnNumber > 0 && turnNumber%3 == 0 {
|
||||||
return -1, true
|
return -1, true // AoE every 3 turns for all bosses
|
||||||
}
|
}
|
||||||
if m.TauntTarget {
|
if m.TauntTarget {
|
||||||
for i, p := range players {
|
for i, p := range players {
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ const (
|
|||||||
RelicATKBoost
|
RelicATKBoost
|
||||||
RelicDEFBoost
|
RelicDEFBoost
|
||||||
RelicGoldBoost
|
RelicGoldBoost
|
||||||
|
RelicPoisonImmunity // immune to poison
|
||||||
|
RelicBurnResist // halve burn damage
|
||||||
|
RelicLifeSteal // heal 10% of damage dealt
|
||||||
)
|
)
|
||||||
|
|
||||||
type Relic struct {
|
type Relic struct {
|
||||||
|
|||||||
@@ -33,6 +33,16 @@ var monsterDefs = map[MonsterType]monsterBase{
|
|||||||
MonsterBoss20: {"Archlich", 600, 40, 20, 20, true},
|
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 {
|
type Monster struct {
|
||||||
Name string
|
Name string
|
||||||
Type MonsterType
|
Type MonsterType
|
||||||
@@ -41,6 +51,7 @@ type Monster struct {
|
|||||||
IsBoss bool
|
IsBoss bool
|
||||||
TauntTarget bool
|
TauntTarget bool
|
||||||
TauntTurns int
|
TauntTurns int
|
||||||
|
Pattern BossPattern
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMonster(mt MonsterType, floor int) *Monster {
|
func NewMonster(mt MonsterType, floor int) *Monster {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package entity
|
package entity
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
type Class int
|
type Class int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -24,6 +26,20 @@ var classBaseStats = map[Class]classStats{
|
|||||||
ClassRogue: {85, 15, 4},
|
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 {
|
type Player struct {
|
||||||
Name string
|
Name string
|
||||||
Fingerprint string
|
Fingerprint string
|
||||||
@@ -33,6 +49,7 @@ type Player struct {
|
|||||||
Gold int
|
Gold int
|
||||||
Inventory []Item
|
Inventory []Item
|
||||||
Relics []Relic
|
Relics []Relic
|
||||||
|
Effects []ActiveEffect
|
||||||
Dead bool
|
Dead bool
|
||||||
Fled bool
|
Fled bool
|
||||||
SkillUses int // remaining skill uses this combat
|
SkillUses int // remaining skill uses this combat
|
||||||
@@ -110,3 +127,59 @@ func (p *Player) EffectiveDEF() int {
|
|||||||
}
|
}
|
||||||
return def
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -114,6 +114,16 @@ func (s *GameSession) spawnBoss() {
|
|||||||
mt = entity.MonsterBoss5
|
mt = entity.MonsterBoss5
|
||||||
}
|
}
|
||||||
boss := entity.NewMonster(mt, s.state.FloorNum)
|
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 {
|
if s.state.SoloMode {
|
||||||
boss.HP = boss.HP / 2
|
boss.HP = boss.HP / 2
|
||||||
boss.MaxHP = boss.HP
|
boss.MaxHP = boss.HP
|
||||||
|
|||||||
@@ -220,6 +220,8 @@ func (s *GameSession) GetState() GameState {
|
|||||||
copy(cp.Inventory, p.Inventory)
|
copy(cp.Inventory, p.Inventory)
|
||||||
cp.Relics = make([]entity.Relic, len(p.Relics))
|
cp.Relics = make([]entity.Relic, len(p.Relics))
|
||||||
copy(cp.Relics, p.Relics)
|
copy(cp.Relics, p.Relics)
|
||||||
|
cp.Effects = make([]entity.ActiveEffect, len(p.Effects))
|
||||||
|
copy(cp.Effects, p.Effects)
|
||||||
players[i] = &cp
|
players[i] = &cp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
42
game/turn.go
42
game/turn.go
@@ -73,6 +73,19 @@ collecting:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *GameSession) resolvePlayerActions() {
|
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 intents []combat.AttackIntent
|
||||||
var intentOwners []string // track who owns each intent
|
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: "Power Amulet", Effect: entity.RelicATKBoost, Value: 3, Price: 120},
|
||||||
{Name: "Iron Ward", Effect: entity.RelicDEFBoost, Value: 2, Price: 100},
|
{Name: "Iron Ward", Effect: entity.RelicDEFBoost, Value: 2, Price: 100},
|
||||||
{Name: "Gold Charm", Effect: entity.RelicGoldBoost, Value: 5, Price: 150},
|
{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 {
|
for _, p := range s.state.Players {
|
||||||
if !p.IsOut() {
|
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 {
|
} else {
|
||||||
if targetIdx >= 0 && targetIdx < len(s.state.Players) {
|
if targetIdx >= 0 && targetIdx < len(s.state.Players) {
|
||||||
p := s.state.Players[targetIdx]
|
p := s.state.Players[targetIdx]
|
||||||
|
|||||||
@@ -248,6 +248,21 @@ func renderPartyPanel(players []*entity.Player, submittedActions map[string]stri
|
|||||||
hpBar := renderHPBar(p.HP, p.MaxHP, 16)
|
hpBar := renderHPBar(p.HP, p.MaxHP, 16)
|
||||||
sb.WriteString(fmt.Sprintf(" %s %d/%d\n", hpBar, p.HP, p.MaxHP))
|
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(fmt.Sprintf(" ATK:%-3d DEF:%-3d ", p.EffectiveATK(), p.EffectiveDEF()))
|
||||||
sb.WriteString(styleGold.Render(fmt.Sprintf("G:%d", p.Gold)))
|
sb.WriteString(styleGold.Render(fmt.Sprintf("G:%d", p.Gold)))
|
||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
|
|||||||
Reference in New Issue
Block a user