feat: add elite monsters with 5 prefix types
Elite monsters have ~20% spawn chance with Venomous, Burning, Freezing, Bleeding, or Vampiric prefixes. Each prefix scales HP/ATK and applies on-hit status effects (or life drain for Vampiric). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
47
entity/elite.go
Normal file
47
entity/elite.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package entity
|
||||
|
||||
import "math/rand"
|
||||
|
||||
type ElitePrefixType int
|
||||
|
||||
const (
|
||||
PrefixVenomous ElitePrefixType = iota // poison on hit
|
||||
PrefixBurning // burn on hit
|
||||
PrefixFreezing // freeze on hit
|
||||
PrefixBleeding // bleed on hit
|
||||
PrefixVampiric // heals self on hit
|
||||
)
|
||||
|
||||
type ElitePrefixDef struct {
|
||||
Name string
|
||||
HPMult float64
|
||||
ATKMult float64
|
||||
OnHit StatusEffect // -1 for vampiric (special)
|
||||
}
|
||||
|
||||
// ElitePrefixDefs is exported so game/turn.go can access for on-hit effects.
|
||||
var ElitePrefixDefs = map[ElitePrefixType]ElitePrefixDef{
|
||||
PrefixVenomous: {"Venomous", 1.3, 1.0, StatusPoison},
|
||||
PrefixBurning: {"Burning", 1.2, 1.1, StatusBurn},
|
||||
PrefixFreezing: {"Freezing", 1.2, 1.0, StatusFreeze},
|
||||
PrefixBleeding: {"Bleeding", 1.1, 1.2, StatusBleed},
|
||||
PrefixVampiric: {"Vampiric", 1.4, 1.1, StatusEffect(-1)},
|
||||
}
|
||||
|
||||
func (p ElitePrefixType) String() string {
|
||||
return ElitePrefixDefs[p].Name
|
||||
}
|
||||
|
||||
func RandomPrefix() ElitePrefixType {
|
||||
return ElitePrefixType(rand.Intn(5))
|
||||
}
|
||||
|
||||
func ApplyPrefix(m *Monster, prefix ElitePrefixType) {
|
||||
def := ElitePrefixDefs[prefix]
|
||||
m.IsElite = true
|
||||
m.ElitePrefix = prefix
|
||||
m.Name = def.Name + " " + m.Name
|
||||
m.HP = int(float64(m.HP) * def.HPMult)
|
||||
m.MaxHP = m.HP
|
||||
m.ATK = int(float64(m.ATK) * def.ATKMult)
|
||||
}
|
||||
94
entity/elite_test.go
Normal file
94
entity/elite_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package entity
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestApplyPrefix(t *testing.T) {
|
||||
m := &Monster{
|
||||
Name: "Slime",
|
||||
HP: 100,
|
||||
MaxHP: 100,
|
||||
ATK: 10,
|
||||
DEF: 5,
|
||||
}
|
||||
ApplyPrefix(m, PrefixVenomous)
|
||||
|
||||
if !m.IsElite {
|
||||
t.Fatal("expected IsElite to be true")
|
||||
}
|
||||
if m.ElitePrefix != PrefixVenomous {
|
||||
t.Fatalf("expected PrefixVenomous, got %d", m.ElitePrefix)
|
||||
}
|
||||
if m.Name != "Venomous Slime" {
|
||||
t.Fatalf("expected 'Venomous Slime', got %q", m.Name)
|
||||
}
|
||||
// HP should be multiplied by 1.3 => 130
|
||||
if m.HP != 130 {
|
||||
t.Fatalf("expected HP=130, got %d", m.HP)
|
||||
}
|
||||
if m.MaxHP != 130 {
|
||||
t.Fatalf("expected MaxHP=130, got %d", m.MaxHP)
|
||||
}
|
||||
// ATK mult is 1.0 for Venomous, so ATK stays 10
|
||||
if m.ATK != 10 {
|
||||
t.Fatalf("expected ATK=10, got %d", m.ATK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyPrefixVampiric(t *testing.T) {
|
||||
m := &Monster{
|
||||
Name: "Orc",
|
||||
HP: 100,
|
||||
MaxHP: 100,
|
||||
ATK: 20,
|
||||
DEF: 5,
|
||||
}
|
||||
ApplyPrefix(m, PrefixVampiric)
|
||||
|
||||
if !m.IsElite {
|
||||
t.Fatal("expected IsElite to be true")
|
||||
}
|
||||
if m.Name != "Vampiric Orc" {
|
||||
t.Fatalf("expected 'Vampiric Orc', got %q", m.Name)
|
||||
}
|
||||
// HP * 1.4 = 140
|
||||
if m.HP != 140 {
|
||||
t.Fatalf("expected HP=140, got %d", m.HP)
|
||||
}
|
||||
// ATK * 1.1 = 22
|
||||
if m.ATK != 22 {
|
||||
t.Fatalf("expected ATK=22, got %d", m.ATK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomPrefix(t *testing.T) {
|
||||
seen := make(map[ElitePrefixType]bool)
|
||||
for i := 0; i < 200; i++ {
|
||||
p := RandomPrefix()
|
||||
if p < 0 || p > 4 {
|
||||
t.Fatalf("RandomPrefix returned out-of-range value: %d", p)
|
||||
}
|
||||
seen[p] = true
|
||||
}
|
||||
// With 200 tries, all 5 prefixes should appear
|
||||
if len(seen) != 5 {
|
||||
t.Fatalf("expected all 5 prefixes to appear, got %d distinct values", len(seen))
|
||||
}
|
||||
}
|
||||
|
||||
func TestElitePrefixString(t *testing.T) {
|
||||
if PrefixVenomous.String() != "Venomous" {
|
||||
t.Fatalf("expected 'Venomous', got %q", PrefixVenomous.String())
|
||||
}
|
||||
if PrefixBurning.String() != "Burning" {
|
||||
t.Fatalf("expected 'Burning', got %q", PrefixBurning.String())
|
||||
}
|
||||
if PrefixFreezing.String() != "Freezing" {
|
||||
t.Fatalf("expected 'Freezing', got %q", PrefixFreezing.String())
|
||||
}
|
||||
if PrefixBleeding.String() != "Bleeding" {
|
||||
t.Fatalf("expected 'Bleeding', got %q", PrefixBleeding.String())
|
||||
}
|
||||
if PrefixVampiric.String() != "Vampiric" {
|
||||
t.Fatalf("expected 'Vampiric', got %q", PrefixVampiric.String())
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,8 @@ type Monster struct {
|
||||
HP, MaxHP int
|
||||
ATK, DEF int
|
||||
IsBoss bool
|
||||
IsElite bool
|
||||
ElitePrefix ElitePrefixType
|
||||
TauntTarget bool
|
||||
TauntTurns int
|
||||
Pattern BossPattern
|
||||
|
||||
@@ -90,6 +90,9 @@ func (s *GameSession) spawnMonsters() {
|
||||
m.MaxHP = m.HP
|
||||
m.DEF = int(float64(m.DEF) * s.cfg.Combat.SoloHPReduction)
|
||||
}
|
||||
if rand.Float64() < 0.20 {
|
||||
entity.ApplyPrefix(m, entity.RandomPrefix())
|
||||
}
|
||||
s.state.Monsters[i] = m
|
||||
}
|
||||
|
||||
|
||||
11
game/turn.go
11
game/turn.go
@@ -379,6 +379,17 @@ func (s *GameSession) resolveMonsterActions() {
|
||||
dmg := combat.CalcDamage(m.ATK, p.EffectiveDEF(), 1.0)
|
||||
p.TakeDamage(dmg)
|
||||
s.addLog(fmt.Sprintf("%s attacks %s for %d dmg", m.Name, p.Name, dmg))
|
||||
if m.IsElite {
|
||||
def := entity.ElitePrefixDefs[m.ElitePrefix]
|
||||
if def.OnHit >= 0 {
|
||||
p.AddEffect(entity.ActiveEffect{Type: def.OnHit, Duration: 2, Value: 3})
|
||||
s.addLog(fmt.Sprintf("%s's %s effect afflicts %s!", m.Name, def.Name, p.Name))
|
||||
} else if m.ElitePrefix == entity.PrefixVampiric {
|
||||
heal := dmg / 4
|
||||
m.HP = min(m.HP+heal, m.MaxHP)
|
||||
s.addLog(fmt.Sprintf("%s drains life from %s! (+%d HP)", m.Name, p.Name, heal))
|
||||
}
|
||||
}
|
||||
if p.IsDead() {
|
||||
s.addLog(fmt.Sprintf("☠ %s has fallen!", p.Name))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user