feat: add floor themes with status effect modifiers
Add 4 floor themes (Swamp/Volcano/Glacier/Inferno) that boost status effect damage on matching floors. Realign boss patterns to match themes and add PatternFreeze for the Glacier boss. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
32
dungeon/theme.go
Normal file
32
dungeon/theme.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package dungeon
|
||||||
|
|
||||||
|
import "github.com/tolelom/catacombs/entity"
|
||||||
|
|
||||||
|
// ThemeModifier defines gameplay modifiers for a range of floors.
|
||||||
|
type ThemeModifier struct {
|
||||||
|
Name string
|
||||||
|
StatusBoost entity.StatusEffect // which status is boosted (-1 = all for Inferno)
|
||||||
|
DamageMult float64
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
var themeModifiers = []ThemeModifier{
|
||||||
|
{"Swamp", entity.StatusPoison, 1.5, "Toxic marshes amplify poison"},
|
||||||
|
{"Volcano", entity.StatusBurn, 1.5, "Volcanic heat intensifies burns"},
|
||||||
|
{"Glacier", entity.StatusFreeze, 1.5, "Glacial cold strengthens frost"},
|
||||||
|
{"Inferno", entity.StatusEffect(-1), 1.3, "Hellfire empowers all afflictions"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTheme returns the theme modifier for the given floor number.
|
||||||
|
func GetTheme(floor int) ThemeModifier {
|
||||||
|
switch {
|
||||||
|
case floor <= 5:
|
||||||
|
return themeModifiers[0]
|
||||||
|
case floor <= 10:
|
||||||
|
return themeModifiers[1]
|
||||||
|
case floor <= 15:
|
||||||
|
return themeModifiers[2]
|
||||||
|
default:
|
||||||
|
return themeModifiers[3]
|
||||||
|
}
|
||||||
|
}
|
||||||
32
dungeon/theme_test.go
Normal file
32
dungeon/theme_test.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package dungeon
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestGetTheme(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
floor int
|
||||||
|
name string
|
||||||
|
}{
|
||||||
|
{1, "Swamp"}, {5, "Swamp"},
|
||||||
|
{6, "Volcano"}, {10, "Volcano"},
|
||||||
|
{11, "Glacier"}, {15, "Glacier"},
|
||||||
|
{16, "Inferno"}, {20, "Inferno"},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
theme := GetTheme(tt.floor)
|
||||||
|
if theme.Name != tt.name {
|
||||||
|
t.Errorf("floor %d: expected %q, got %q", tt.floor, tt.name, theme.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThemeDamageMult(t *testing.T) {
|
||||||
|
theme := GetTheme(1) // Swamp
|
||||||
|
if theme.DamageMult != 1.5 {
|
||||||
|
t.Errorf("Swamp DamageMult: expected 1.5, got %f", theme.DamageMult)
|
||||||
|
}
|
||||||
|
theme = GetTheme(20) // Inferno
|
||||||
|
if theme.DamageMult != 1.3 {
|
||||||
|
t.Errorf("Inferno DamageMult: expected 1.3, got %f", theme.DamageMult)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ const (
|
|||||||
PatternPoison // applies poison
|
PatternPoison // applies poison
|
||||||
PatternBurn // applies burn to random player
|
PatternBurn // applies burn to random player
|
||||||
PatternHeal // heals self
|
PatternHeal // heals self
|
||||||
|
PatternFreeze // applies freeze to all players
|
||||||
)
|
)
|
||||||
|
|
||||||
type Monster struct {
|
type Monster struct {
|
||||||
|
|||||||
@@ -119,13 +119,13 @@ func (s *GameSession) spawnBoss() {
|
|||||||
boss := entity.NewMonster(mt, s.state.FloorNum, s.cfg.Combat.MonsterScaling)
|
boss := entity.NewMonster(mt, s.state.FloorNum, s.cfg.Combat.MonsterScaling)
|
||||||
switch mt {
|
switch mt {
|
||||||
case entity.MonsterBoss5:
|
case entity.MonsterBoss5:
|
||||||
boss.Pattern = entity.PatternAoE
|
boss.Pattern = entity.PatternPoison // Swamp theme
|
||||||
case entity.MonsterBoss10:
|
case entity.MonsterBoss10:
|
||||||
boss.Pattern = entity.PatternPoison
|
boss.Pattern = entity.PatternBurn // Volcano theme
|
||||||
case entity.MonsterBoss15:
|
case entity.MonsterBoss15:
|
||||||
boss.Pattern = entity.PatternBurn
|
boss.Pattern = entity.PatternFreeze // Glacier theme
|
||||||
case entity.MonsterBoss20:
|
case entity.MonsterBoss20:
|
||||||
boss.Pattern = entity.PatternHeal
|
boss.Pattern = entity.PatternHeal // Inferno theme (+ natural AoE every 3 turns)
|
||||||
}
|
}
|
||||||
if s.state.SoloMode {
|
if s.state.SoloMode {
|
||||||
boss.HP = int(float64(boss.HP) * s.cfg.Combat.SoloHPReduction)
|
boss.HP = int(float64(boss.HP) * s.cfg.Combat.SoloHPReduction)
|
||||||
|
|||||||
26
game/turn.go
26
game/turn.go
@@ -72,13 +72,30 @@ collecting:
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *GameSession) resolvePlayerActions() {
|
func (s *GameSession) resolvePlayerActions() {
|
||||||
// Tick status effects
|
// Tick status effects with floor theme damage bonus
|
||||||
|
theme := dungeon.GetTheme(s.state.FloorNum)
|
||||||
for _, p := range s.state.Players {
|
for _, p := range s.state.Players {
|
||||||
if !p.IsOut() {
|
if !p.IsOut() {
|
||||||
|
// Snapshot effects before tick to compute theme bonus
|
||||||
|
effectsBefore := make([]entity.ActiveEffect, len(p.Effects))
|
||||||
|
copy(effectsBefore, p.Effects)
|
||||||
|
|
||||||
msgs := p.TickEffects()
|
msgs := p.TickEffects()
|
||||||
for _, msg := range msgs {
|
for _, msg := range msgs {
|
||||||
s.addLog(msg)
|
s.addLog(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply theme damage bonus for matching status effects
|
||||||
|
for _, e := range effectsBefore {
|
||||||
|
if e.Value > 0 && (theme.StatusBoost == entity.StatusEffect(-1) || e.Type == theme.StatusBoost) {
|
||||||
|
bonus := int(float64(e.Value) * (theme.DamageMult - 1.0))
|
||||||
|
if bonus > 0 {
|
||||||
|
p.TakeDamage(bonus)
|
||||||
|
s.addLog(fmt.Sprintf(" (%s theme: +%d damage)", theme.Name, bonus))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if p.IsDead() {
|
if p.IsDead() {
|
||||||
s.addLog(fmt.Sprintf("☠ %s has fallen!", p.Name))
|
s.addLog(fmt.Sprintf("☠ %s has fallen!", p.Name))
|
||||||
}
|
}
|
||||||
@@ -409,6 +426,13 @@ func (s *GameSession) resolveMonsterActions() {
|
|||||||
s.addLog(fmt.Sprintf("%s burns %s!", m.Name, p.Name))
|
s.addLog(fmt.Sprintf("%s burns %s!", m.Name, p.Name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case entity.PatternFreeze:
|
||||||
|
for _, p := range s.state.Players {
|
||||||
|
if !p.IsOut() {
|
||||||
|
p.AddEffect(entity.ActiveEffect{Type: entity.StatusFreeze, Duration: 1, Value: 0})
|
||||||
|
s.addLog(fmt.Sprintf("%s freezes %s!", m.Name, p.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
case entity.PatternHeal:
|
case entity.PatternHeal:
|
||||||
healAmt := m.MaxHP / 10
|
healAmt := m.MaxHP / 10
|
||||||
m.HP += healAmt
|
m.HP += healAmt
|
||||||
|
|||||||
Reference in New Issue
Block a user