diff --git a/dungeon/theme.go b/dungeon/theme.go new file mode 100644 index 0000000..9ef3d30 --- /dev/null +++ b/dungeon/theme.go @@ -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] + } +} diff --git a/dungeon/theme_test.go b/dungeon/theme_test.go new file mode 100644 index 0000000..5edfbc2 --- /dev/null +++ b/dungeon/theme_test.go @@ -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) + } +} diff --git a/entity/monster.go b/entity/monster.go index 1ec0185..91cfe81 100644 --- a/entity/monster.go +++ b/entity/monster.go @@ -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 { diff --git a/game/event.go b/game/event.go index a7d59d2..42b3756 100644 --- a/game/event.go +++ b/game/event.go @@ -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) diff --git a/game/turn.go b/game/turn.go index 0390a32..4269ca5 100644 --- a/game/turn.go +++ b/game/turn.go @@ -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