230 lines
5.2 KiB
Go
230 lines
5.2 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)
|
|
if s.state.SoloMode {
|
|
m.HP = m.HP / 2
|
|
if m.HP < 1 {
|
|
m.HP = 1
|
|
}
|
|
m.MaxHP = m.HP
|
|
m.DEF = m.DEF / 2
|
|
}
|
|
s.state.Monsters[i] = m
|
|
}
|
|
|
|
// Reset skill uses for all players at combat start
|
|
for _, p := range s.state.Players {
|
|
p.SkillUses = 3
|
|
}
|
|
}
|
|
|
|
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)
|
|
switch mt {
|
|
case entity.MonsterBoss5:
|
|
boss.Pattern = entity.PatternAoE
|
|
case entity.MonsterBoss10:
|
|
boss.Pattern = entity.PatternPoison
|
|
case entity.MonsterBoss15:
|
|
boss.Pattern = entity.PatternBurn
|
|
case entity.MonsterBoss20:
|
|
boss.Pattern = entity.PatternHeal
|
|
}
|
|
if s.state.SoloMode {
|
|
boss.HP = boss.HP / 2
|
|
boss.MaxHP = boss.HP
|
|
boss.DEF = boss.DEF / 2
|
|
}
|
|
s.state.Monsters = []*entity.Monster{boss}
|
|
|
|
// Reset skill uses for all players at combat start
|
|
for _, p := range s.state.Players {
|
|
p.SkillUses = 3
|
|
}
|
|
}
|
|
|
|
func (s *GameSession) grantTreasure() {
|
|
floor := s.state.FloorNum
|
|
for _, p := range s.state.Players {
|
|
if len(p.Inventory) >= 10 {
|
|
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))
|
|
}
|
|
}
|
|
}
|