Files
Catacombs/game/event.go
tolelom 1e155c62fb 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>
2026-03-25 15:22:17 +09:00

233 lines
5.7 KiB
Go

package game
import (
"fmt"
"math/rand"
"time"
"github.com/tolelom/catacombs/dungeon"
"github.com/tolelom/catacombs/entity"
)
func (s *GameSession) EnterRoom(roomIdx int) {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
for _, p := range s.state.Players {
if p.Fingerprint != "" {
s.lastActivity[p.Fingerprint] = now
}
}
s.state.Floor.CurrentRoom = roomIdx
dungeon.UpdateVisibility(s.state.Floor)
room := s.state.Floor.Rooms[roomIdx]
if room.Cleared {
return
}
switch room.Type {
case dungeon.RoomCombat:
s.spawnMonsters()
s.state.Phase = PhaseCombat
s.state.CombatTurn = 0
s.signalCombat()
case dungeon.RoomBoss:
s.spawnBoss()
s.state.Phase = PhaseCombat
s.state.CombatTurn = 0
s.signalCombat()
case dungeon.RoomShop:
s.generateShopItems()
s.state.Phase = PhaseShop
case dungeon.RoomTreasure:
s.grantTreasure()
room.Cleared = true
case dungeon.RoomEvent:
s.triggerEvent()
room.Cleared = true
case dungeon.RoomEmpty:
room.Cleared = true
}
}
func (s *GameSession) spawnMonsters() {
count := 1 + rand.Intn(5)
floor := s.state.FloorNum
s.state.Monsters = make([]*entity.Monster, count)
type floorRange struct {
mt entity.MonsterType
minFloor int
maxFloor int
}
ranges := []floorRange{
{entity.MonsterSlime, 1, 5},
{entity.MonsterSkeleton, 3, 10},
{entity.MonsterOrc, 6, 14},
{entity.MonsterDarkKnight, 12, 20},
}
var valid []entity.MonsterType
for _, r := range ranges {
if floor >= r.minFloor && floor <= r.maxFloor {
valid = append(valid, r.mt)
}
}
if len(valid) == 0 {
valid = []entity.MonsterType{entity.MonsterSlime}
}
for i := 0; i < count; i++ {
mt := valid[rand.Intn(len(valid))]
m := entity.NewMonster(mt, floor, s.cfg.Combat.MonsterScaling)
if s.state.SoloMode {
m.HP = int(float64(m.HP) * s.cfg.Combat.SoloHPReduction)
if m.HP < 1 {
m.HP = 1
}
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
}
// Reset skill uses for all players at combat start
for _, p := range s.state.Players {
p.SkillUses = s.cfg.Game.SkillUses
}
}
func (s *GameSession) spawnBoss() {
var mt entity.MonsterType
switch s.state.FloorNum {
case 5:
mt = entity.MonsterBoss5
case 10:
mt = entity.MonsterBoss10
case 15:
mt = entity.MonsterBoss15
case 20:
mt = entity.MonsterBoss20
default:
mt = entity.MonsterBoss5
}
boss := entity.NewMonster(mt, s.state.FloorNum, s.cfg.Combat.MonsterScaling)
switch mt {
case entity.MonsterBoss5:
boss.Pattern = entity.PatternPoison // Swamp theme
case entity.MonsterBoss10:
boss.Pattern = entity.PatternBurn // Volcano theme
case entity.MonsterBoss15:
boss.Pattern = entity.PatternFreeze // Glacier theme
case entity.MonsterBoss20:
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)
boss.MaxHP = boss.HP
boss.DEF = int(float64(boss.DEF) * s.cfg.Combat.SoloHPReduction)
}
s.state.Monsters = []*entity.Monster{boss}
// Reset skill uses for all players at combat start
for _, p := range s.state.Players {
p.SkillUses = s.cfg.Game.SkillUses
}
}
func (s *GameSession) grantTreasure() {
floor := s.state.FloorNum
for _, p := range s.state.Players {
if len(p.Inventory) >= s.cfg.Game.InventoryLimit {
s.addLog(fmt.Sprintf("%s's inventory is full!", p.Name))
continue
}
if rand.Float64() < 0.5 {
bonus := 3 + rand.Intn(6) + floor/3
item := entity.Item{
Name: weaponName(floor), Type: entity.ItemWeapon, Bonus: bonus,
}
p.Inventory = append(p.Inventory, item)
s.addLog(fmt.Sprintf("%s found %s (ATK+%d)", p.Name, item.Name, item.Bonus))
} else {
bonus := 2 + rand.Intn(4) + floor/4
item := entity.Item{
Name: armorName(floor), Type: entity.ItemArmor, Bonus: bonus,
}
p.Inventory = append(p.Inventory, item)
s.addLog(fmt.Sprintf("%s found %s (DEF+%d)", p.Name, item.Name, item.Bonus))
}
}
}
func (s *GameSession) generateShopItems() {
floor := s.state.FloorNum
// Weapon bonus scales: base 3-8 + floor/3
weaponBonus := 3 + rand.Intn(6) + floor/3
// Armor bonus scales: base 2-5 + floor/4
armorBonus := 2 + rand.Intn(4) + floor/4
// Prices scale with power
weaponPrice := 40 + weaponBonus*5
armorPrice := 30 + armorBonus*5
// Potion heals more on higher floors
potionHeal := 30 + floor
potionPrice := 20 + floor/2
s.state.ShopItems = []entity.Item{
{Name: "HP Potion", Type: entity.ItemConsumable, Bonus: potionHeal, Price: potionPrice},
{Name: weaponName(floor), Type: entity.ItemWeapon, Bonus: weaponBonus, Price: weaponPrice},
{Name: armorName(floor), Type: entity.ItemArmor, Bonus: armorBonus, Price: armorPrice},
}
}
func weaponName(floor int) string {
switch {
case floor >= 15:
return "Mythril Blade"
case floor >= 10:
return "Steel Sword"
case floor >= 5:
return "Bronze Sword"
default:
return "Iron Sword"
}
}
func armorName(floor int) string {
switch {
case floor >= 15:
return "Mythril Shield"
case floor >= 10:
return "Steel Shield"
case floor >= 5:
return "Bronze Shield"
default:
return "Iron Shield"
}
}
func (s *GameSession) triggerEvent() {
for _, p := range s.state.Players {
if p.IsDead() {
continue
}
if rand.Float64() < 0.5 {
baseDmg := 10 + s.state.FloorNum
dmg := baseDmg + rand.Intn(baseDmg/2+1)
p.TakeDamage(dmg)
s.addLog(fmt.Sprintf("Trap! %s takes %d damage", p.Name, dmg))
} else {
baseHeal := 15 + s.state.FloorNum
heal := baseHeal + rand.Intn(baseHeal/2+1)
before := p.HP
p.Heal(heal)
s.addLog(fmt.Sprintf("Blessing! %s heals %d HP", p.Name, p.HP-before))
}
}
}