adventure: Tier 6 postgame "Mythic" dungeons (P1–P7 calibration)

Five post-game dungeons above the T5 ceiling, gated on both T5 bosses beaten
+ level 18. Opt-in endgame: deadly solo, clearable by a party with Pete + pets.

- P1 gating: postgameUnlocked (T5 clears + level floor), zonesForLevel excludes
  T6 unconditionally; wired into startZoneRun, !zone/!expedition, party accept,
  boredom picker, and the list dividers.
- P2 bestiary: 15 elites + 5 signature bosses (Layer-1 stat blocks).
- P3 zone defs + 4-region registries; ZoneLootEntry.BossOnly.
- P4 five zone graphs on a shared builder (44–52 rooms, no soft-lock; Ossuary
  secret Verse nodes).
- P5 loot: BossOnly enforced; signature items are real registry magic items;
  five Thom pity recipes off the per-zone crafting anchors.
- P6 narration/flavor (5 files), T6 achievements, Pete stays zone-parametric.
- P7 (in progress): sim can now reach gated T6 (SimRunner.SeedPostgameUnlock +
  IsPostgameZone). First calibration pass on millenia — hardened ossuary +
  drowned_star, softened first_hoard + unplace; last_meridian in band.

Fix: party members were refused from every T6 zone because expeditionCmdAccept
ran the level gate (which excludes T6) before the postgame check — the intended
party endgame was unreachable. Route T6 through postgameUnlocked. Regression
tests added.
This commit is contained in:
prosolis
2026-07-15 23:17:07 -07:00
parent 27c2b48007
commit d9541f07f1
41 changed files with 2487 additions and 29 deletions

View File

@@ -18,6 +18,11 @@ func TestRecipeRegistry_IngredientsExistInResources(t *testing.T) {
}
}
for _, r := range thomCraftRecipes {
// AlwaysKnown pity recipes consume zone-loot crafting anchors, not
// gather-resource materials — validated separately below.
if r.AlwaysKnown {
continue
}
for ing := range r.Ingredients {
if !known[ing] {
t.Errorf("recipe %s references unknown ingredient %q", r.ID, ing)
@@ -26,12 +31,59 @@ func TestRecipeRegistry_IngredientsExistInResources(t *testing.T) {
}
}
// TestPityRecipes_AnchorsAndOutputsResolve — the T6 signature-item pity
// recipes consume a zone-loot UniqueAlways crafting anchor and emit a real
// registry magic item. Validate both ends: every ingredient name matches a
// UniqueAlways zone-loot drop's inventory name, and every OutputSkillSource
// points at a magicItemRegistry ID.
func TestPityRecipes_AnchorsAndOutputsResolve(t *testing.T) {
anchorNames := map[string]bool{}
for _, z := range allZones() {
for _, e := range z.Loot {
if e.UniqueAlways {
anchorNames[titleCaseUnderscored(e.ItemID)] = true
}
}
}
pity := 0
for _, r := range thomCraftRecipes {
if !r.AlwaysKnown {
continue
}
pity++
for ing := range r.Ingredients {
if !anchorNames[ing] {
t.Errorf("pity recipe %s ingredient %q is not a zone-loot anchor name", r.ID, ing)
}
}
if r.OutputType != "magic_item" {
t.Errorf("pity recipe %s OutputType = %q, want magic_item", r.ID, r.OutputType)
}
id, ok := strings.CutPrefix(r.OutputSkillSource, "magic_item:")
if !ok {
t.Errorf("pity recipe %s OutputSkillSource %q missing magic_item: prefix", r.ID, r.OutputSkillSource)
continue
}
if _, ok := magicItemRegistry[id]; !ok {
t.Errorf("pity recipe %s output %q not in magicItemRegistry", r.ID, id)
}
}
if pity != 5 {
t.Errorf("expected 5 T6 pity recipes, found %d", pity)
}
}
// TestRecipeRegistry_OutputValuesMatchTier — outputs should land in §8.1
// rarity brackets given their tier (rare 100300, very rare 5001200,
// epic 5001200 in §8.1 — recipes here lean tier 34, so 200..1200 is the
// reasonable band).
func TestRecipeRegistry_OutputValuesInBand(t *testing.T) {
for _, r := range thomCraftRecipes {
// Pity recipes emit Legendary magic items priced well above the §8.1
// crafting band by design — excluded from this invariant.
if r.AlwaysKnown {
continue
}
if r.OutputValue < 200 || r.OutputValue > 1200 {
t.Errorf("recipe %s output value %d outside band [200,1200] (tier %d)",
r.ID, r.OutputValue, r.OutputTier)