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:
2026-03-25 15:22:17 +09:00
parent 69ac6cd383
commit 1e155c62fb
5 changed files with 94 additions and 5 deletions

32
dungeon/theme.go Normal file
View 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
View 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)
}
}

View File

@@ -41,6 +41,7 @@ const (
PatternPoison // applies poison
PatternBurn // applies burn to random player
PatternHeal // heals self
PatternFreeze // applies freeze to all players
)
type Monster struct {

View File

@@ -119,13 +119,13 @@ func (s *GameSession) spawnBoss() {
boss := entity.NewMonster(mt, s.state.FloorNum, s.cfg.Combat.MonsterScaling)
switch mt {
case entity.MonsterBoss5:
boss.Pattern = entity.PatternAoE
boss.Pattern = entity.PatternPoison // Swamp theme
case entity.MonsterBoss10:
boss.Pattern = entity.PatternPoison
boss.Pattern = entity.PatternBurn // Volcano theme
case entity.MonsterBoss15:
boss.Pattern = entity.PatternBurn
boss.Pattern = entity.PatternFreeze // Glacier theme
case entity.MonsterBoss20:
boss.Pattern = entity.PatternHeal
boss.Pattern = entity.PatternHeal // Inferno theme (+ natural AoE every 3 turns)
}
if s.state.SoloMode {
boss.HP = int(float64(boss.HP) * s.cfg.Combat.SoloHPReduction)

View File

@@ -72,13 +72,30 @@ collecting:
}
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 {
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()
for _, msg := range msgs {
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() {
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))
}
}
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:
healAmt := m.MaxHP / 10
m.HP += healAmt