feat: add secret rooms and mini-bosses on floors 4/9/14/19

Add RoomSecret (5% chance) and RoomMiniBoss room types. Add 4 mini-boss
monsters at 60% of boss stats (Guardian's Herald, Warden's Shadow,
Overlord's Lieutenant, Archlich's Harbinger) with IsMiniBoss flag and
boss pattern logic. Secret rooms grant double treasure. Mini-boss rooms
are placed on floors 4/9/14/19 at room index 1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 15:30:21 +09:00
parent e167165bbc
commit 7f29995833
8 changed files with 199 additions and 18 deletions

View File

@@ -13,6 +13,10 @@ const (
MonsterBoss10
MonsterBoss15
MonsterBoss20
MonsterMiniBoss5
MonsterMiniBoss10
MonsterMiniBoss15
MonsterMiniBoss20
)
type monsterBase struct {
@@ -31,6 +35,10 @@ var monsterDefs = map[MonsterType]monsterBase{
MonsterBoss10: {"Warden", 250, 22, 12, 10, true},
MonsterBoss15: {"Overlord", 400, 30, 16, 15, true},
MonsterBoss20: {"Archlich", 600, 40, 20, 20, true},
MonsterMiniBoss5: {"Guardian's Herald", 90, 9, 5, 4, false},
MonsterMiniBoss10: {"Warden's Shadow", 150, 13, 7, 9, false},
MonsterMiniBoss15: {"Overlord's Lieutenant", 240, 18, 10, 14, false},
MonsterMiniBoss20: {"Archlich's Harbinger", 360, 24, 12, 19, false},
}
type BossPattern int
@@ -50,6 +58,7 @@ type Monster struct {
HP, MaxHP int
ATK, DEF int
IsBoss bool
IsMiniBoss bool
IsElite bool
ElitePrefix ElitePrefixType
TauntTarget bool
@@ -59,21 +68,24 @@ type Monster struct {
func NewMonster(mt MonsterType, floor int, scaling float64) *Monster {
base := monsterDefs[mt]
isMiniBoss := mt == MonsterMiniBoss5 || mt == MonsterMiniBoss10 ||
mt == MonsterMiniBoss15 || mt == MonsterMiniBoss20
scale := 1.0
if !base.IsBoss && floor > base.MinFloor {
if !base.IsBoss && !isMiniBoss && floor > base.MinFloor {
scale = math.Pow(scaling, float64(floor-base.MinFloor))
}
hp := int(math.Round(float64(base.HP) * scale))
atk := int(math.Round(float64(base.ATK) * scale))
def := int(math.Round(float64(base.DEF) * scale))
return &Monster{
Name: base.Name,
Type: mt,
HP: hp,
MaxHP: hp,
ATK: atk,
DEF: def,
IsBoss: base.IsBoss,
Name: base.Name,
Type: mt,
HP: hp,
MaxHP: hp,
ATK: atk,
DEF: def,
IsBoss: base.IsBoss,
IsMiniBoss: isMiniBoss,
}
}

View File

@@ -49,6 +49,40 @@ func TestTickTaunt(t *testing.T) {
}
}
func TestMiniBossStats(t *testing.T) {
tests := []struct {
mt MonsterType
name string
wantHP, wantATK, wantDEF int
}{
{MonsterMiniBoss5, "Guardian's Herald", 90, 9, 5},
{MonsterMiniBoss10, "Warden's Shadow", 150, 13, 7},
{MonsterMiniBoss15, "Overlord's Lieutenant", 240, 18, 10},
{MonsterMiniBoss20, "Archlich's Harbinger", 360, 24, 12},
}
for _, tc := range tests {
m := NewMonster(tc.mt, tc.wantHP, 1.15) // floor doesn't matter, no scaling
if m.Name != tc.name {
t.Errorf("%v: name got %q, want %q", tc.mt, m.Name, tc.name)
}
if m.HP != tc.wantHP {
t.Errorf("%v: HP got %d, want %d", tc.mt, m.HP, tc.wantHP)
}
if m.ATK != tc.wantATK {
t.Errorf("%v: ATK got %d, want %d", tc.mt, m.ATK, tc.wantATK)
}
if m.DEF != tc.wantDEF {
t.Errorf("%v: DEF got %d, want %d", tc.mt, m.DEF, tc.wantDEF)
}
if !m.IsMiniBoss {
t.Errorf("%v: IsMiniBoss should be true", tc.mt)
}
if m.IsBoss {
t.Errorf("%v: IsBoss should be false for mini-bosses", tc.mt)
}
}
}
func TestMonsterAtMinFloor(t *testing.T) {
// Slime at floor 1 (minFloor=1) should have base stats
m := NewMonster(MonsterSlime, 1, 1.15)