diff --git a/entity/elite.go b/entity/elite.go new file mode 100644 index 0000000..13278f5 --- /dev/null +++ b/entity/elite.go @@ -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) +} diff --git a/entity/elite_test.go b/entity/elite_test.go new file mode 100644 index 0000000..ad61c1b --- /dev/null +++ b/entity/elite_test.go @@ -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()) + } +} diff --git a/entity/monster.go b/entity/monster.go index 962a2d1..1ec0185 100644 --- a/entity/monster.go +++ b/entity/monster.go @@ -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 diff --git a/game/event.go b/game/event.go index 2314fb9..a7d59d2 100644 --- a/game/event.go +++ b/game/event.go @@ -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 } diff --git a/game/turn.go b/game/turn.go index ada3622..51a0303 100644 --- a/game/turn.go +++ b/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)) }