mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Zone Infrastructure SUB-A: ZoneDefinition / ZoneTier / ZoneEnemy / ZoneBoss / ZoneLootEntry types and dndZoneRegistry. Tier 1 zones populated per gogobee_dungeon_zones.md §5: Goblin Warrens (T1, L1-3, boss Grol the Unbroken CR3) and Crypt of Valdris (T1, L1-3, boss Valdris the Unburied CR5, phase 2 at 50% HP). Helpers: getZone, zonesForLevel (2-tier-above ceiling), zonesByTier, allZones. Bestiary additions for Tier 1 enemy rosters: goblin_sneak, goblin_archer, hobgoblin_grunt, worg, goblin_shaman, hobgoblin_warchief, zombie, shadow, specter, wight, flameskull, plus the two zone bosses. Zombie AC bumped 8->10 to satisfy engine TestBestiaryAllWellFormed minimum. 7 new tests covering registry presence, bestiary-ref resolution, boss CR floor, level-tier gating, loot-chance bounds, room-count sanity, elite flag presence, and tier->level-range alignment. Full plugin suite green. Also marks SUB3 done in gogobee_subclass_system.md (all 15 subclasses through L15 shipped across SUB3a-d). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
123 lines
3.3 KiB
Go
123 lines
3.3 KiB
Go
package plugin
|
||
|
||
import "testing"
|
||
|
||
// Phase 11 D1a — zone registry tests. Validates that:
|
||
// 1. Tier 1 zones are registered.
|
||
// 2. All enemy/boss BestiaryID references resolve.
|
||
// 3. Level gating (zonesForLevel) respects the 2-tier-above ceiling.
|
||
// 4. Loot tables either have explicit drop chances in [0,1] or
|
||
// UniqueAlways set.
|
||
|
||
func TestZoneRegistry_Tier1Present(t *testing.T) {
|
||
if _, ok := getZone(ZoneGoblinWarrens); !ok {
|
||
t.Fatal("Goblin Warrens not registered")
|
||
}
|
||
if _, ok := getZone(ZoneCryptValdris); !ok {
|
||
t.Fatal("Crypt of Valdris not registered")
|
||
}
|
||
t1 := zonesByTier(ZoneTierBeginner)
|
||
if len(t1) != 2 {
|
||
t.Fatalf("expected 2 Tier 1 zones, got %d", len(t1))
|
||
}
|
||
}
|
||
|
||
func TestZoneRegistry_BestiaryRefsResolve(t *testing.T) {
|
||
for _, z := range allZones() {
|
||
for _, e := range z.Enemies {
|
||
if _, ok := dndBestiary[e.BestiaryID]; !ok {
|
||
t.Errorf("zone %s enemy %s missing from bestiary", z.ID, e.BestiaryID)
|
||
}
|
||
}
|
||
if _, ok := dndBestiary[z.Boss.BestiaryID]; !ok {
|
||
t.Errorf("zone %s boss %s missing from bestiary", z.ID, z.Boss.BestiaryID)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestZoneRegistry_BossCRMatchesZoneTier(t *testing.T) {
|
||
// Sanity: boss CR should be at least at the high end of tier
|
||
// CR range. Tier 1 = CR 1/8–1 enemies, but boss is CR 3–5.
|
||
for _, z := range allZones() {
|
||
if z.Boss.CR < 1 {
|
||
t.Errorf("zone %s boss CR %.1f too low", z.ID, z.Boss.CR)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestZonesForLevel_TierGate(t *testing.T) {
|
||
// L1 player (tier 1): can reach tier 1+2=3. With only T1 zones
|
||
// registered in D1a, returns both.
|
||
got := zonesForLevel(1)
|
||
if len(got) != 2 {
|
||
t.Fatalf("L1 should see 2 T1 zones, got %d", len(got))
|
||
}
|
||
// L20 player should also see all zones (no upper cutoff).
|
||
got = zonesForLevel(20)
|
||
if len(got) < 2 {
|
||
t.Fatalf("L20 should see all zones, got %d", len(got))
|
||
}
|
||
}
|
||
|
||
func TestZoneRegistry_LootDropChances(t *testing.T) {
|
||
for _, z := range allZones() {
|
||
for _, l := range z.Loot {
|
||
if l.UniqueAlways {
|
||
continue
|
||
}
|
||
if l.DropChance <= 0 || l.DropChance > 1 {
|
||
t.Errorf("zone %s loot %s drop chance %.2f out of range",
|
||
z.ID, l.ItemID, l.DropChance)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestZoneRegistry_RoomCountSane(t *testing.T) {
|
||
for _, z := range allZones() {
|
||
if z.MinRooms < 5 || z.MaxRooms > 10 || z.MinRooms > z.MaxRooms {
|
||
t.Errorf("zone %s rooms %d-%d outside design (5-10, min<=max)",
|
||
z.ID, z.MinRooms, z.MaxRooms)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestZoneRegistry_ElitesFlagged(t *testing.T) {
|
||
// Each Tier 1 zone roster should contain at least one elite (per
|
||
// design doc each zone lists ELITE entries).
|
||
for _, z := range allZones() {
|
||
anyElite := false
|
||
for _, e := range z.Enemies {
|
||
if e.IsElite {
|
||
anyElite = true
|
||
break
|
||
}
|
||
}
|
||
if !anyElite {
|
||
t.Errorf("zone %s has no elite enemy", z.ID)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestZoneRegistry_LevelRangeMatchesTier(t *testing.T) {
|
||
// Tier→expected level range per design doc §2.
|
||
expected := map[ZoneTier][2]int{
|
||
ZoneTierBeginner: {1, 3},
|
||
ZoneTierApprentice: {3, 5},
|
||
ZoneTierJourneyman: {5, 8},
|
||
ZoneTierVeteran: {8, 12},
|
||
ZoneTierLegendary: {12, 20},
|
||
}
|
||
for _, z := range allZones() {
|
||
want, ok := expected[z.Tier]
|
||
if !ok {
|
||
t.Errorf("zone %s has unknown tier %d", z.ID, z.Tier)
|
||
continue
|
||
}
|
||
if z.LevelMin < want[0] || z.LevelMax > want[1] {
|
||
t.Errorf("zone %s level %d-%d outside tier %d range %d-%d",
|
||
z.ID, z.LevelMin, z.LevelMax, z.Tier, want[0], want[1])
|
||
}
|
||
}
|
||
}
|