mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Adv 2.0 D&D Phase 11 D5a: Tier 4-5 zones + bestiary
Underdark + Feywild Crossing (T4) and Dragon's Lair + Abyss Portal (T5)
registered. Zone hooks, atmosphere, factions, and boss descriptions
reuse the prewritten flavor in gogobee_dungeon_zones.md §5.
New bestiary entries:
Underdark: drow, drow_elite_warrior, drow_mage, mind_flayer,
hook_horror, roper, boss_ilvaras_xunyl
Feywild: redcap, will_o_wisp, quickling, night_hag, fomorian,
boss_thornmother (reuses Tier 2 green_hag)
Dragon's Lair: kobold, guard_drake, kobold_scale_sorcerer,
dragonborn_cultist, young_red_dragon, boss_infernax
Abyss Portal: quasit, vrock, hezrou, nalfeshnee, marilith,
boss_belaxath
Boss-entry pools (BossEntryInfernax, BossEntryBelaxath) and per-zone
RoomEntry pools for Underdark + Dragon's Lair already live in
twinbee_gm_flavor.go from a prior phase and are wired in
dnd_zone_narration.go — no narration switch changes here. Feywild and
Abyss room/boss/elite flavor lands in D5b.
Bumps TestBestiaryByCR cap from CR≤20 to CR≤30 to cover Infernax (CR
24, Ancient Red Dragon). New T4/T5 presence tests added.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -430,6 +430,227 @@ var _ = func() bool {
|
|||||||
return true
|
return true
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// ---- Tier 4-5 zone roster (Phase 11 D5a) -------------------------------------
|
||||||
|
//
|
||||||
|
// Underdark + Feywild Crossing (T4) and Dragon's Lair + Abyss Portal (T5)
|
||||||
|
// enemy entries. Stat blocks per gogobee_dungeon_zones.md §5. Same
|
||||||
|
// Attack-as-average-damage convention as prior tiers; AttackBonus = d20
|
||||||
|
// to-hit modifier; elites flagged via the zone roster, not here.
|
||||||
|
// Feywild reuses Tier 2's green_hag entry.
|
||||||
|
|
||||||
|
var _ = func() bool {
|
||||||
|
tier45 := map[string]DnDMonsterTemplate{
|
||||||
|
// ── Underdark ────────────────────────────────────────────────────
|
||||||
|
"drow": {
|
||||||
|
ID: "drow", Name: "Drow",
|
||||||
|
CR: 0.25, HP: 13, AC: 15, Attack: 5, AttackBonus: 4, Speed: 12,
|
||||||
|
BlockRate: 0.10,
|
||||||
|
Ability: &MonsterAbility{Name: "Drow Poison", Phase: "any", ProcChance: 0.30, Effect: "stun"},
|
||||||
|
XPValue: 50,
|
||||||
|
Notes: "Fey Ancestry (adv. vs charm); Sunlight Sensitivity; Hand Crossbow poison bolt.",
|
||||||
|
},
|
||||||
|
"drow_elite_warrior": {
|
||||||
|
ID: "drow_elite_warrior", Name: "Drow Elite Warrior",
|
||||||
|
CR: 5, HP: 71, AC: 18, Attack: 18, AttackBonus: 7, Speed: 12,
|
||||||
|
BlockRate: 0.20,
|
||||||
|
Ability: &MonsterAbility{Name: "Parry", Phase: "any", ProcChance: 0.35, Effect: "block"},
|
||||||
|
XPValue: 1800,
|
||||||
|
Notes: "Multiattack 3 hits; Parry reaction; Drow Poison (CON DC 13 unconscious).",
|
||||||
|
},
|
||||||
|
"drow_mage": {
|
||||||
|
ID: "drow_mage", Name: "Drow Mage",
|
||||||
|
CR: 7, HP: 45, AC: 12, Attack: 22, AttackBonus: 5, Speed: 12,
|
||||||
|
BlockRate: 0.05,
|
||||||
|
Ability: &MonsterAbility{Name: "Lightning Bolt", Phase: "decisive", ProcChance: 0.45, Effect: "aoe"},
|
||||||
|
XPValue: 2900,
|
||||||
|
Notes: "Spells: Lightning Bolt, Fly, Darkness, Faerie Fire. Magic Resistance.",
|
||||||
|
},
|
||||||
|
"mind_flayer": {
|
||||||
|
ID: "mind_flayer", Name: "Mind Flayer",
|
||||||
|
CR: 7, HP: 71, AC: 15, Attack: 26, AttackBonus: 7, Speed: 12,
|
||||||
|
BlockRate: 0.10,
|
||||||
|
Ability: &MonsterAbility{Name: "Mind Blast", Phase: "opening", ProcChance: 0.55, Effect: "stun"},
|
||||||
|
XPValue: 2900,
|
||||||
|
Notes: "Mind Blast AoE psychic INT DC 15 or Stunned; Extract Brain instakills stunned; telepathy.",
|
||||||
|
},
|
||||||
|
"hook_horror": {
|
||||||
|
ID: "hook_horror", Name: "Hook Horror",
|
||||||
|
CR: 3, HP: 75, AC: 15, Attack: 14, AttackBonus: 6, Speed: 10,
|
||||||
|
BlockRate: 0.10,
|
||||||
|
Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.30, Effect: "advantage"},
|
||||||
|
XPValue: 700,
|
||||||
|
Notes: "Echoes of Darkness blindsight 60 ft; grapple claws; pack tactics.",
|
||||||
|
},
|
||||||
|
"roper": {
|
||||||
|
ID: "roper", Name: "Roper",
|
||||||
|
CR: 5, HP: 93, AC: 20, Attack: 22, AttackBonus: 7, Speed: 4,
|
||||||
|
BlockRate: 0.20,
|
||||||
|
Ability: &MonsterAbility{Name: "Reel", Phase: "any", ProcChance: 0.50, Effect: "stun"},
|
||||||
|
XPValue: 1800,
|
||||||
|
Notes: "Elite. 6 tendrils grapple+restrain; Reel pulls 25 ft; False Appearance.",
|
||||||
|
},
|
||||||
|
"boss_ilvaras_xunyl": {
|
||||||
|
ID: "boss_ilvaras_xunyl", Name: "Ilvaras Xunyl, Drow High Priestess",
|
||||||
|
CR: 12, HP: 162, AC: 16, Attack: 38, AttackBonus: 9, Speed: 12,
|
||||||
|
BlockRate: 0.15,
|
||||||
|
Ability: &MonsterAbility{Name: "Divine Word", Phase: "decisive", ProcChance: 0.40, Effect: "execute"},
|
||||||
|
XPValue: 8400,
|
||||||
|
Notes: "Underdark boss. Legendary Actions; Lolth's Favour 1/combat; phase 2 below 35% HP — Lolth's Avatar overlay.",
|
||||||
|
},
|
||||||
|
// ── Feywild Crossing ─────────────────────────────────────────────
|
||||||
|
"redcap": {
|
||||||
|
ID: "redcap", Name: "Redcap",
|
||||||
|
CR: 3, HP: 45, AC: 13, Attack: 16, AttackBonus: 6, Speed: 14,
|
||||||
|
BlockRate: 0.10,
|
||||||
|
Ability: &MonsterAbility{Name: "Outsize Strength", Phase: "any", ProcChance: 0.40, Effect: "bonus_damage"},
|
||||||
|
XPValue: 700,
|
||||||
|
Notes: "Iron Boots add bludgeoning on hit; loses power without blood-soaked cap.",
|
||||||
|
},
|
||||||
|
"will_o_wisp": {
|
||||||
|
ID: "will_o_wisp", Name: "Will-o'-Wisp",
|
||||||
|
CR: 2, HP: 22, AC: 19, Attack: 9, AttackBonus: 4, Speed: 14,
|
||||||
|
BlockRate: 0.0,
|
||||||
|
Ability: &MonsterAbility{Name: "Consume Life", Phase: "any", ProcChance: 0.40, Effect: "lifesteal"},
|
||||||
|
XPValue: 450,
|
||||||
|
Notes: "Variable Illumination; Ephemeral (+13 vs OAs); incorporeal.",
|
||||||
|
},
|
||||||
|
"quickling": {
|
||||||
|
ID: "quickling", Name: "Quickling",
|
||||||
|
CR: 1, HP: 10, AC: 16, Attack: 8, AttackBonus: 5, Speed: 18,
|
||||||
|
BlockRate: 0.05,
|
||||||
|
Ability: &MonsterAbility{Name: "Blur", Phase: "any", ProcChance: 0.50, Effect: "evade"},
|
||||||
|
XPValue: 200,
|
||||||
|
Notes: "Attacks at disadvantage vs Quickling; 3 attacks/turn; Evasion.",
|
||||||
|
},
|
||||||
|
"night_hag": {
|
||||||
|
ID: "night_hag", Name: "Night Hag",
|
||||||
|
CR: 5, HP: 112, AC: 17, Attack: 21, AttackBonus: 7, Speed: 12,
|
||||||
|
BlockRate: 0.15,
|
||||||
|
Ability: &MonsterAbility{Name: "Etherealness", Phase: "any", ProcChance: 0.30, Effect: "evade"},
|
||||||
|
XPValue: 1800,
|
||||||
|
Notes: "Etherealness; Dream Haunting (no long rest benefit); Heartstone.",
|
||||||
|
},
|
||||||
|
"fomorian": {
|
||||||
|
ID: "fomorian", Name: "Fomorian",
|
||||||
|
CR: 8, HP: 149, AC: 14, Attack: 30, AttackBonus: 9, Speed: 12,
|
||||||
|
BlockRate: 0.10,
|
||||||
|
Ability: &MonsterAbility{Name: "Evil Eye", Phase: "any", ProcChance: 0.45, Effect: "stun"},
|
||||||
|
XPValue: 3900,
|
||||||
|
Notes: "Elite. Evil Eye WIS DC 14 (Frightened/Poisoned/Stunned — GM choice); stomp AoE.",
|
||||||
|
},
|
||||||
|
"boss_thornmother": {
|
||||||
|
ID: "boss_thornmother", Name: "The Thornmother",
|
||||||
|
CR: 11, HP: 187, AC: 17, Attack: 32, AttackBonus: 8, Speed: 12,
|
||||||
|
BlockRate: 0.15,
|
||||||
|
Ability: &MonsterAbility{Name: "Beguiling Bargain", Phase: "opening", ProcChance: 0.50, Effect: "stun"},
|
||||||
|
XPValue: 7200,
|
||||||
|
Notes: "Feywild Crossing boss. Coven Magic; Shapechange; phase 2 below 30% HP — True Form, +4d6 psychic, summons 2 Night Hags.",
|
||||||
|
},
|
||||||
|
// ── Dragon's Lair ────────────────────────────────────────────────
|
||||||
|
"kobold": {
|
||||||
|
ID: "kobold", Name: "Kobold",
|
||||||
|
CR: 0.125, HP: 5, AC: 12, Attack: 4, AttackBonus: 4, Speed: 12,
|
||||||
|
BlockRate: 0.05,
|
||||||
|
Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.40, Effect: "advantage"},
|
||||||
|
XPValue: 25,
|
||||||
|
Notes: "Sunlight Sensitivity; Trap expertise; pack tactics.",
|
||||||
|
},
|
||||||
|
"guard_drake": {
|
||||||
|
ID: "guard_drake", Name: "Guard Drake",
|
||||||
|
CR: 2, HP: 52, AC: 14, Attack: 12, AttackBonus: 5, Speed: 12,
|
||||||
|
BlockRate: 0.10,
|
||||||
|
Ability: &MonsterAbility{Name: "Draconic Loyalty", Phase: "any", ProcChance: 1.00, Effect: "fear_immune"},
|
||||||
|
XPValue: 450,
|
||||||
|
Notes: "Multiattack; immune to fear near dragons.",
|
||||||
|
},
|
||||||
|
"kobold_scale_sorcerer": {
|
||||||
|
ID: "kobold_scale_sorcerer", Name: "Kobold Scale Sorcerer",
|
||||||
|
CR: 1, HP: 27, AC: 15, Attack: 11, AttackBonus: 4, Speed: 12,
|
||||||
|
BlockRate: 0.05,
|
||||||
|
Ability: &MonsterAbility{Name: "Chromatic Orb", Phase: "decisive", ProcChance: 0.50, Effect: "bonus_damage"},
|
||||||
|
XPValue: 200,
|
||||||
|
Notes: "Spells: Chromatic Orb, Mage Armor, Shield. Draconic Sorcery.",
|
||||||
|
},
|
||||||
|
"dragonborn_cultist": {
|
||||||
|
ID: "dragonborn_cultist", Name: "Dragonborn Cultist",
|
||||||
|
CR: 4, HP: 71, AC: 16, Attack: 17, AttackBonus: 6, Speed: 12,
|
||||||
|
BlockRate: 0.10,
|
||||||
|
Ability: &MonsterAbility{Name: "Draconic Gift", Phase: "opening", ProcChance: 0.50, Effect: "aoe"},
|
||||||
|
XPValue: 1100,
|
||||||
|
Notes: "Breath weapon 2d6 (DC 13); Fanatical Devotion (immune to fear).",
|
||||||
|
},
|
||||||
|
"young_red_dragon": {
|
||||||
|
ID: "young_red_dragon", Name: "Young Red Dragon",
|
||||||
|
CR: 10, HP: 178, AC: 18, Attack: 36, AttackBonus: 10, Speed: 16,
|
||||||
|
BlockRate: 0.10,
|
||||||
|
Ability: &MonsterAbility{Name: "Fire Breath", Phase: "any", ProcChance: 0.30, Effect: "aoe_fire"},
|
||||||
|
XPValue: 5900,
|
||||||
|
Notes: "Elite. Fire Breath 16d6 DEX DC 21; Multiattack; Frightful Presence WIS DC 16.",
|
||||||
|
},
|
||||||
|
"boss_infernax": {
|
||||||
|
ID: "boss_infernax", Name: "Infernax the Undying",
|
||||||
|
CR: 24, HP: 546, AC: 22, Attack: 65, AttackBonus: 14, Speed: 18,
|
||||||
|
BlockRate: 0.15,
|
||||||
|
Ability: &MonsterAbility{Name: "Frightful Presence", Phase: "opening", ProcChance: 0.80, Effect: "stun"},
|
||||||
|
XPValue: 62000,
|
||||||
|
Notes: "Dragon's Lair boss. Ancient Red Dragon. Legendary Actions; Lair Actions; phase 2 below 50% HP — fire ignores resistance.",
|
||||||
|
},
|
||||||
|
// ── Abyss Portal ─────────────────────────────────────────────────
|
||||||
|
"quasit": {
|
||||||
|
ID: "quasit", Name: "Quasit",
|
||||||
|
CR: 1, HP: 7, AC: 13, Attack: 6, AttackBonus: 4, Speed: 14,
|
||||||
|
BlockRate: 0.0,
|
||||||
|
Ability: &MonsterAbility{Name: "Invisibility", Phase: "any", ProcChance: 0.40, Effect: "evade"},
|
||||||
|
XPValue: 200,
|
||||||
|
Notes: "Scare WIS DC 10 Frightened 1 min; poison claws.",
|
||||||
|
},
|
||||||
|
"vrock": {
|
||||||
|
ID: "vrock", Name: "Vrock",
|
||||||
|
CR: 6, HP: 104, AC: 15, Attack: 25, AttackBonus: 6, Speed: 12,
|
||||||
|
BlockRate: 0.10,
|
||||||
|
Ability: &MonsterAbility{Name: "Stunning Screech", Phase: "opening", ProcChance: 0.50, Effect: "stun"},
|
||||||
|
XPValue: 2300,
|
||||||
|
Notes: "Multiattack; Spores CON DC 14 (5d10 poison/24h); Screech CON DC 14 Stunned.",
|
||||||
|
},
|
||||||
|
"hezrou": {
|
||||||
|
ID: "hezrou", Name: "Hezrou",
|
||||||
|
CR: 8, HP: 136, AC: 16, Attack: 30, AttackBonus: 7, Speed: 13,
|
||||||
|
BlockRate: 0.15,
|
||||||
|
Ability: &MonsterAbility{Name: "Stench", Phase: "any", ProcChance: 0.50, Effect: "debuff"},
|
||||||
|
XPValue: 3900,
|
||||||
|
Notes: "Stench CON DC 14 Poisoned (disadvantage); multiattack.",
|
||||||
|
},
|
||||||
|
"nalfeshnee": {
|
||||||
|
ID: "nalfeshnee", Name: "Nalfeshnee",
|
||||||
|
CR: 13, HP: 184, AC: 18, Attack: 42, AttackBonus: 10, Speed: 12,
|
||||||
|
BlockRate: 0.15,
|
||||||
|
Ability: &MonsterAbility{Name: "Horror Nimbus", Phase: "opening", ProcChance: 0.55, Effect: "stun"},
|
||||||
|
XPValue: 10000,
|
||||||
|
Notes: "Multiattack 3 hits; Horror Nimbus AoE WIS DC 15 Frightened; Magic Resistance.",
|
||||||
|
},
|
||||||
|
"marilith": {
|
||||||
|
ID: "marilith", Name: "Marilith",
|
||||||
|
CR: 16, HP: 189, AC: 18, Attack: 50, AttackBonus: 11, Speed: 12,
|
||||||
|
BlockRate: 0.25,
|
||||||
|
Ability: &MonsterAbility{Name: "Reactive Parry", Phase: "any", ProcChance: 0.50, Effect: "block"},
|
||||||
|
XPValue: 15000,
|
||||||
|
Notes: "Elite. 7 attacks/turn; Parry reaction; Reactive (extra reaction); Magic Resistance.",
|
||||||
|
},
|
||||||
|
"boss_belaxath": {
|
||||||
|
ID: "boss_belaxath", Name: "Belaxath the Undivided",
|
||||||
|
CR: 19, HP: 262, AC: 19, Attack: 58, AttackBonus: 12, Speed: 14,
|
||||||
|
BlockRate: 0.20,
|
||||||
|
Ability: &MonsterAbility{Name: "Lightning Discharge", Phase: "decisive", ProcChance: 0.40, Effect: "aoe"},
|
||||||
|
XPValue: 22000,
|
||||||
|
Notes: "Abyss Portal boss. Balor. Fire Aura; Death Throes on death; Demonic Resilience; Legendary Resistance 3/combat; phase 2 below 40% HP — Huge size, advantage on attacks.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for id, m := range tier45 {
|
||||||
|
dndBestiary[id] = m
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}()
|
||||||
|
|
||||||
// dndBestiaryByCR returns templates whose CR is at or below the given cap.
|
// dndBestiaryByCR returns templates whose CR is at or below the given cap.
|
||||||
// Useful for procedurally selecting a monster appropriate to player level.
|
// Useful for procedurally selecting a monster appropriate to player level.
|
||||||
func dndBestiaryByCR(maxCR float32) []DnDMonsterTemplate {
|
func dndBestiaryByCR(maxCR float32) []DnDMonsterTemplate {
|
||||||
|
|||||||
@@ -35,9 +35,10 @@ func TestBestiaryByCR(t *testing.T) {
|
|||||||
t.Errorf("CR filter leaked: %s CR=%v", m.Name, m.CR)
|
t.Errorf("CR filter leaked: %s CR=%v", m.Name, m.CR)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
high := dndBestiaryByCR(20.0)
|
// Cap above CR 24 to cover Infernax (Ancient Red Dragon, T5 boss).
|
||||||
|
high := dndBestiaryByCR(30.0)
|
||||||
if len(high) != len(dndBestiary) {
|
if len(high) != len(dndBestiary) {
|
||||||
t.Errorf("CR≤20 filter returned %d, want all %d", len(high), len(dndBestiary))
|
t.Errorf("CR≤30 filter returned %d, want all %d", len(high), len(dndBestiary))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,10 @@ func init() {
|
|||||||
registerZone(zoneSunkenTemple())
|
registerZone(zoneSunkenTemple())
|
||||||
registerZone(zoneManorBlackspire())
|
registerZone(zoneManorBlackspire())
|
||||||
registerZone(zoneUnderforge())
|
registerZone(zoneUnderforge())
|
||||||
|
registerZone(zoneUnderdark())
|
||||||
|
registerZone(zoneFeywildCrossing())
|
||||||
|
registerZone(zoneDragonsLair())
|
||||||
|
registerZone(zoneAbyssPortal())
|
||||||
}
|
}
|
||||||
|
|
||||||
// getZone returns the definition by ID and ok=false if unknown.
|
// getZone returns the definition by ID and ok=false if unknown.
|
||||||
@@ -443,3 +447,194 @@ func zoneUnderforge() ZoneDefinition {
|
|||||||
FlavorFile: "zone_underforge_flavor.go",
|
FlavorFile: "zone_underforge_flavor.go",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Tier 4-5 zone factories (Phase 11 D5a) ---------------------------------
|
||||||
|
|
||||||
|
func zoneUnderdark() ZoneDefinition {
|
||||||
|
return ZoneDefinition{
|
||||||
|
ID: ZoneUnderdark,
|
||||||
|
Display: "The Underdark",
|
||||||
|
Tier: ZoneTierVeteran,
|
||||||
|
LevelMin: 8,
|
||||||
|
LevelMax: 12,
|
||||||
|
Faction: "Drow, Mind Flayers, Beholders (far), Ropers, Hook Horrors",
|
||||||
|
Atmosphere: "Absolute darkness, phosphorescent mushroom groves, vast underground seas, carved drow cities in the distance, things older than the surface world.",
|
||||||
|
Hook: "There is a world below the world. It has its own cities, its own wars, its own sky — which is stone, and has never once been kind. TwinBee speaks more quietly here. Something might be listening.",
|
||||||
|
MinRooms: 8,
|
||||||
|
MaxRooms: 10,
|
||||||
|
Enemies: []ZoneEnemy{
|
||||||
|
{BestiaryID: "drow", SpawnWeight: 7},
|
||||||
|
{BestiaryID: "drow_elite_warrior", SpawnWeight: 4},
|
||||||
|
{BestiaryID: "drow_mage", SpawnWeight: 3},
|
||||||
|
{BestiaryID: "mind_flayer", SpawnWeight: 2},
|
||||||
|
{BestiaryID: "hook_horror", SpawnWeight: 4},
|
||||||
|
{BestiaryID: "roper", SpawnWeight: 1, IsElite: true},
|
||||||
|
},
|
||||||
|
Boss: ZoneBoss{
|
||||||
|
BestiaryID: "boss_ilvaras_xunyl",
|
||||||
|
Name: "Ilvaras Xunyl, Drow High Priestess",
|
||||||
|
CR: 12,
|
||||||
|
HP: 162,
|
||||||
|
AC: 16,
|
||||||
|
PhaseTwoAt: 0.35,
|
||||||
|
Description: "She has killed four previous expeditions from the surface. She keeps their weapons as trophies. She has run out of wall space.",
|
||||||
|
Abilities: []string{
|
||||||
|
"Spells: Flame Strike, Dispel Magic, Divine Word (CHA DC 18), Insect Plague",
|
||||||
|
"Lolth's Favour: 1/combat auto-succeed a failed save",
|
||||||
|
"Summon Spiders (1/combat): 2d6 Giant Spiders fill the room",
|
||||||
|
"Legendary Actions (3): Melee (1 LA), Cast Cantrip (1 LA), Drain Life (3 LA — 4d10 necrotic)",
|
||||||
|
"Phase 2 (<35% HP): Lolth's Avatar overlay — +3 AC; spells cast as 1 higher slot",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Loot: []ZoneLootEntry{
|
||||||
|
{ItemID: "xunyls_diadem", DropChance: 0.05, Note: "+3 CHA; Drow Spells (Darkness, Faerie Fire 1/day each)"},
|
||||||
|
{ItemID: "arm_drow_chain_+2", DropChance: 0.08},
|
||||||
|
{ItemID: "wpn_dagger_of_venom", DropChance: 0.10},
|
||||||
|
{ItemID: "spider_silk", UniqueAlways: true, Note: "2d4 high-value crafting material"},
|
||||||
|
{ItemID: "coins_15d10x12", DropChance: 1.0, Note: "15d10 × 12 coins"},
|
||||||
|
},
|
||||||
|
FlavorFile: "zone_underdark_flavor.go",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func zoneFeywildCrossing() ZoneDefinition {
|
||||||
|
return ZoneDefinition{
|
||||||
|
ID: ZoneFeywildCrossing,
|
||||||
|
Display: "Feywild Crossing",
|
||||||
|
Tier: ZoneTierVeteran,
|
||||||
|
LevelMin: 8,
|
||||||
|
LevelMax: 12,
|
||||||
|
Faction: "Hags, Redcaps, Will-o-Wisps, Fomorians, Unseelie Fey",
|
||||||
|
Atmosphere: "Impossible beauty, treacherous whimsy, time distortion, rules that change without notice, bargains with terrible fine print.",
|
||||||
|
Hook: "The veil between worlds is thin here. Colors are too saturated. The mushrooms are too large. A small creature made of starlight just offered you a deal. TwinBee advises extreme caution regarding deals.",
|
||||||
|
MinRooms: 8,
|
||||||
|
MaxRooms: 10,
|
||||||
|
Enemies: []ZoneEnemy{
|
||||||
|
{BestiaryID: "redcap", SpawnWeight: 6},
|
||||||
|
{BestiaryID: "will_o_wisp", SpawnWeight: 5},
|
||||||
|
{BestiaryID: "quickling", SpawnWeight: 5},
|
||||||
|
{BestiaryID: "green_hag", SpawnWeight: 4},
|
||||||
|
{BestiaryID: "night_hag", SpawnWeight: 3},
|
||||||
|
{BestiaryID: "fomorian", SpawnWeight: 1, IsElite: true},
|
||||||
|
},
|
||||||
|
Boss: ZoneBoss{
|
||||||
|
BestiaryID: "boss_thornmother",
|
||||||
|
Name: "The Thornmother",
|
||||||
|
CR: 11,
|
||||||
|
HP: 187,
|
||||||
|
AC: 17,
|
||||||
|
PhaseTwoAt: 0.30,
|
||||||
|
Description: "She has three names. She will offer to tell you all three. She will not tell you what accepting means. The flowers around her throne are beautiful and they are not flowers.",
|
||||||
|
Abilities: []string{
|
||||||
|
"Coven Magic: extra spell slots scaled by GM Mood (Elated/Engaged TwinBee = stronger Thornmother)",
|
||||||
|
"Beguiling Bargain (1/combat): offers a deal — accept a debuff for a permanent minor buff",
|
||||||
|
"Thorned Grasp: Restrained + 4d6 piercing/turn, CON DC 16 to break free",
|
||||||
|
"Shapechange (1/combat): adopts a player's appearance; 50% miss vs her until DC 17 Investigation",
|
||||||
|
"Phase 2 (<30% HP): True Form — illusions drop; +4d6 psychic on attacks; coven summons 2 Night Hags",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Loot: []ZoneLootEntry{
|
||||||
|
{ItemID: "thornmothers_bargain", UniqueAlways: true, Note: "ring; +4 to one stat / -2 to another (player choice; permanent)"},
|
||||||
|
{ItemID: "wpn_rapier_of_puncturing_+2", DropChance: 0.08},
|
||||||
|
{ItemID: "arm_cloak_of_protection_+2", DropChance: 0.08},
|
||||||
|
{ItemID: "feywild_essence", UniqueAlways: true, Note: "1d4 legendary crafting material"},
|
||||||
|
},
|
||||||
|
FlavorFile: "zone_feywild_crossing_flavor.go",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func zoneDragonsLair() ZoneDefinition {
|
||||||
|
return ZoneDefinition{
|
||||||
|
ID: ZoneDragonsLair,
|
||||||
|
Display: "Dragon's Lair (Infernus Peak)",
|
||||||
|
Tier: ZoneTierLegendary,
|
||||||
|
LevelMin: 12,
|
||||||
|
LevelMax: 20,
|
||||||
|
Faction: "Kobolds, Drakes, Young Dragons, Wyrm",
|
||||||
|
Atmosphere: "Scorched stone, rivers of gold coins half-melted into the floor, kobold warrens as outer defenses, growing heat, the unmistakable smell of something ancient and enormous.",
|
||||||
|
Hook: "The mountain has not erupted in forty years. The locals say it is dormant. The locals are wrong about what lives in mountains. TwinBee has prepared an unusually long entry description for this one.",
|
||||||
|
MinRooms: 9,
|
||||||
|
MaxRooms: 10,
|
||||||
|
Enemies: []ZoneEnemy{
|
||||||
|
{BestiaryID: "kobold", SpawnWeight: 7},
|
||||||
|
{BestiaryID: "guard_drake", SpawnWeight: 5},
|
||||||
|
{BestiaryID: "kobold_scale_sorcerer", SpawnWeight: 4},
|
||||||
|
{BestiaryID: "dragonborn_cultist", SpawnWeight: 3},
|
||||||
|
{BestiaryID: "young_red_dragon", SpawnWeight: 1, IsElite: true},
|
||||||
|
},
|
||||||
|
Boss: ZoneBoss{
|
||||||
|
BestiaryID: "boss_infernax",
|
||||||
|
Name: "Infernax the Undying",
|
||||||
|
CR: 24,
|
||||||
|
HP: 546,
|
||||||
|
AC: 22,
|
||||||
|
PhaseTwoAt: 0.50,
|
||||||
|
Description: "Ancient Red Dragon. He remembers when the surface civilizations were just fires. He found the fires amusing. He finds their descendants less so.",
|
||||||
|
Abilities: []string{
|
||||||
|
"Multiattack: Bite + 2 Claws",
|
||||||
|
"Fire Breath (recharge 5–6): 90-ft cone, 26d6 fire, DEX DC 24 half",
|
||||||
|
"Frightful Presence: WIS DC 21 or Frightened 1 min",
|
||||||
|
"Legendary Resistance (3/combat): auto-succeed failed save",
|
||||||
|
"Legendary Actions (3): Detect (1), Tail Attack (1), Wing Attack (2 — AoE knockback DEX DC 22)",
|
||||||
|
"Lair Actions (init 20): Magma eruption (2d6 fire), Volcanic gases (CON DC 13 Poisoned), Tremor (DEX DC 15 Prone)",
|
||||||
|
"Phase 2 (<50% HP): Fire Breath recharge 4–6; fire damage ignores resistance",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Loot: []ZoneLootEntry{
|
||||||
|
{ItemID: "scale_of_infernax", UniqueAlways: true, Note: "legendary crafting anchor for legendary fire weapons"},
|
||||||
|
{ItemID: "arm_dragon_scale_mail_red", DropChance: 0.15},
|
||||||
|
{ItemID: "wpn_flame_tongue_+3", DropChance: 0.10},
|
||||||
|
{ItemID: "ring_of_fire_resistance", DropChance: 0.12},
|
||||||
|
{ItemID: "coins_50d10x100", DropChance: 1.0, Note: "Dragon Hoard: 50d10 × 100 coins"},
|
||||||
|
},
|
||||||
|
FlavorFile: "zone_dragons_lair_flavor.go",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func zoneAbyssPortal() ZoneDefinition {
|
||||||
|
return ZoneDefinition{
|
||||||
|
ID: ZoneAbyssPortal,
|
||||||
|
Display: "The Abyss Portal",
|
||||||
|
Tier: ZoneTierLegendary,
|
||||||
|
LevelMin: 15,
|
||||||
|
LevelMax: 20,
|
||||||
|
Faction: "Demons, Fiends, Corrupted Celestials",
|
||||||
|
Atmosphere: "Reality fractures, impossible geometry, constant low psychic pressure, the feeling of being watched by something that has no eyes.",
|
||||||
|
Hook: "Someone opened a door they should not have opened. The door is still open. Things are still coming through. TwinBee is not making jokes about this one.",
|
||||||
|
MinRooms: 9,
|
||||||
|
MaxRooms: 10,
|
||||||
|
Enemies: []ZoneEnemy{
|
||||||
|
{BestiaryID: "quasit", SpawnWeight: 6},
|
||||||
|
{BestiaryID: "vrock", SpawnWeight: 5},
|
||||||
|
{BestiaryID: "hezrou", SpawnWeight: 4},
|
||||||
|
{BestiaryID: "nalfeshnee", SpawnWeight: 2},
|
||||||
|
{BestiaryID: "marilith", SpawnWeight: 1, IsElite: true},
|
||||||
|
},
|
||||||
|
Boss: ZoneBoss{
|
||||||
|
BestiaryID: "boss_belaxath",
|
||||||
|
Name: "Belaxath the Undivided",
|
||||||
|
CR: 19,
|
||||||
|
HP: 262,
|
||||||
|
AC: 19,
|
||||||
|
PhaseTwoAt: 0.40,
|
||||||
|
Description: "Balor. The portal was not summoned. It was torn. Belaxath did the tearing from the other side, with purpose, over three decades. It is annoyed it took that long.",
|
||||||
|
Abilities: []string{
|
||||||
|
"Multiattack: Longsword + Whip",
|
||||||
|
"Fire Aura: 10d6 fire to all melee attackers each turn; weapons +3d6 fire",
|
||||||
|
"Lightning Discharge (recharge 5–6): 120-ft line, 12d6 lightning, DEX DC 20 half",
|
||||||
|
"Death Throes: on death, 30-ft radius, 20d6 fire, DEX DC 20 half — destroys non-legendary equipment on failed save",
|
||||||
|
"Magic Resistance: advantage on saves vs spells",
|
||||||
|
"Legendary Resistance (3/combat)",
|
||||||
|
"Demonic Resilience: resist cold/fire/lightning; immune poison; immune non-magical physical",
|
||||||
|
"Phase 2 (<40% HP): grows to Huge size; advantage on all attacks; Death Throes recharge 4–6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Loot: []ZoneLootEntry{
|
||||||
|
{ItemID: "shard_of_the_abyss", UniqueAlways: true, Note: "legendary quest item for portal-closing storyline"},
|
||||||
|
{ItemID: "wpn_demon_blade", DropChance: 0.05, Note: "+3 greatsword; +2d6 necrotic; immune to Charm/Frightened"},
|
||||||
|
{ItemID: "arm_demon_armor_+3", DropChance: 0.05},
|
||||||
|
{ItemID: "portal_fragment", UniqueAlways: true, Note: "legendary crafting — required for highest-tier recipes"},
|
||||||
|
},
|
||||||
|
FlavorFile: "zone_abyss_portal_flavor.go",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,6 +48,32 @@ func TestZoneRegistry_Tier3Present(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestZoneRegistry_Tier4Present(t *testing.T) {
|
||||||
|
if _, ok := getZone(ZoneUnderdark); !ok {
|
||||||
|
t.Fatal("The Underdark not registered")
|
||||||
|
}
|
||||||
|
if _, ok := getZone(ZoneFeywildCrossing); !ok {
|
||||||
|
t.Fatal("Feywild Crossing not registered")
|
||||||
|
}
|
||||||
|
t4 := zonesByTier(ZoneTierVeteran)
|
||||||
|
if len(t4) != 2 {
|
||||||
|
t.Fatalf("expected 2 Tier 4 zones, got %d", len(t4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZoneRegistry_Tier5Present(t *testing.T) {
|
||||||
|
if _, ok := getZone(ZoneDragonsLair); !ok {
|
||||||
|
t.Fatal("Dragon's Lair not registered")
|
||||||
|
}
|
||||||
|
if _, ok := getZone(ZoneAbyssPortal); !ok {
|
||||||
|
t.Fatal("The Abyss Portal not registered")
|
||||||
|
}
|
||||||
|
t5 := zonesByTier(ZoneTierLegendary)
|
||||||
|
if len(t5) != 2 {
|
||||||
|
t.Fatalf("expected 2 Tier 5 zones, got %d", len(t5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestZoneRegistry_BestiaryRefsResolve(t *testing.T) {
|
func TestZoneRegistry_BestiaryRefsResolve(t *testing.T) {
|
||||||
for _, z := range allZones() {
|
for _, z := range allZones() {
|
||||||
for _, e := range z.Enemies {
|
for _, e := range z.Enemies {
|
||||||
|
|||||||
Reference in New Issue
Block a user