gofmt: bring internal/ and cmd/ back to gofmt -l clean

Mechanical `gofmt -w ./internal ./cmd`. Mostly struct-field realignment that
had drifted, plus a few trailing-newline fixes. No behaviour change — gofmt is
semantics-preserving, and build/vet/test are green either side.

Split out from the code-review fixes that follow so those stay reviewable
instead of hiding inside a wall of realignment.
This commit is contained in:
prosolis
2026-07-10 07:18:07 -07:00
parent 08d3053368
commit 3369d7d8fe
108 changed files with 1248 additions and 1260 deletions

View File

@@ -3,17 +3,20 @@
// outcomes mirror what live players hit. // outcomes mirror what live players hit.
// //
// Single run: // Single run:
// expedition-sim [-class fighter] [-level 5] [-zone goblin_warrens] //
// [-bank 1000] [-cap 50] [-log] [-data DIR] // expedition-sim [-class fighter] [-level 5] [-zone goblin_warrens]
// [-bank 1000] [-cap 50] [-log] [-data DIR]
// //
// Party run (N3/P7 — the leader is -class; followers clone it unless named): // Party run (N3/P7 — the leader is -class; followers clone it unless named):
// expedition-sim -party 3 -level 15 -zone dragons_lair //
// expedition-sim -party 2 -class cleric -party-classes fighter -level 16 ... // expedition-sim -party 3 -level 15 -zone dragons_lair
// expedition-sim -party 2 -class cleric -party-classes fighter -level 16 ...
// //
// Matrix mode (cartesian sweep over classes × levels × zones × N runs, // Matrix mode (cartesian sweep over classes × levels × zones × N runs,
// one JSON object per stdout line, log suppressed by default): // one JSON object per stdout line, log suppressed by default):
// expedition-sim -matrix -classes fighter,mage -levels 5,10 \ //
// -zones goblin_warrens,wolf_den -runs 20 // expedition-sim -matrix -classes fighter,mage -levels 5,10 \
// -zones goblin_warrens,wolf_den -runs 20
package main package main
import ( import (
@@ -269,7 +272,6 @@ func matrixWorker(exe string, in <-chan matrixJob, out chan<- *plugin.SimResult,
} }
} }
func runOne(dataDir string, uid id.UserID, class plugin.DnDClass, level int, zone plugin.ZoneID, bank float64, cap, days int, followers []string) (*plugin.SimResult, error) { func runOne(dataDir string, uid id.UserID, class plugin.DnDClass, level int, zone plugin.ZoneID, bank float64, cap, days int, followers []string) (*plugin.SimResult, error) {
runner, err := plugin.NewSimRunner(dataDir) runner, err := plugin.NewSimRunner(dataDir)
if err != nil { if err != nil {

View File

@@ -129,7 +129,7 @@ var (
reSpaces = regexp.MustCompile(`\s+`) reSpaces = regexp.MustCompile(`\s+`)
reOrphanMustMake = regexp.MustCompile(`(?i)\s+must\s+(?:make|succeed on|attempt)([.,;])`) reOrphanMustMake = regexp.MustCompile(`(?i)\s+must\s+(?:make|succeed on|attempt)([.,;])`)
reOrphanTrailingAnd = regexp.MustCompile(`(?i),?\s+and\.`) reOrphanTrailingAnd = regexp.MustCompile(`(?i),?\s+and\.`)
descScrub = []*regexp.Regexp{ descScrub = []*regexp.Regexp{
// Range / distance jargon. // Range / distance jargon.
regexp.MustCompile(`(?i)\s*\bwithin range\b`), regexp.MustCompile(`(?i)\s*\bwithin range\b`),
regexp.MustCompile(`(?i)\s*\bwithin \d+ feet(?: of [a-z ]+?)?`), regexp.MustCompile(`(?i)\s*\bwithin \d+ feet(?: of [a-z ]+?)?`),

View File

@@ -93,11 +93,11 @@ func TestCleanDescRepairsOrphans(t *testing.T) {
// TestStripNameParenthetical — bestiary R21 / magic-item alias names. // TestStripNameParenthetical — bestiary R21 / magic-item alias names.
func TestStripNameParenthetical(t *testing.T) { func TestStripNameParenthetical(t *testing.T) {
cases := map[string]string{ cases := map[string]string{
"Giant Rat (Diseased)": "Giant Rat", "Giant Rat (Diseased)": "Giant Rat",
"Deep Gnome (Svirfneblin)": "Deep Gnome", "Deep Gnome (Svirfneblin)": "Deep Gnome",
"Stone of Good Luck (Luckstone)": "Stone of Good Luck", "Stone of Good Luck (Luckstone)": "Stone of Good Luck",
"Ordinary Name": "Ordinary Name", "Ordinary Name": "Ordinary Name",
"": "", "": "",
} }
for in, want := range cases { for in, want := range cases {
if got := stripNameParenthetical(in); got != want { if got := stripNameParenthetical(in); got != want {

View File

@@ -88,18 +88,18 @@ func must(err error) {
// open5eSpell is the subset of the Open5e v1 spell schema we consume. Unknown // open5eSpell is the subset of the Open5e v1 spell schema we consume. Unknown
// fields are ignored; the vendored JSON keeps the full payload. // fields are ignored; the vendored JSON keeps the full payload.
type open5eSpell struct { type open5eSpell struct {
Slug string `json:"slug"` Slug string `json:"slug"`
Name string `json:"name"` Name string `json:"name"`
Desc string `json:"desc"` Desc string `json:"desc"`
HigherLevel string `json:"higher_level"` HigherLevel string `json:"higher_level"`
Range string `json:"range"` Range string `json:"range"`
Material string `json:"material"` Material string `json:"material"`
CanBeCastAsRitual bool `json:"can_be_cast_as_ritual"` CanBeCastAsRitual bool `json:"can_be_cast_as_ritual"`
Duration string `json:"duration"` Duration string `json:"duration"`
RequiresConcentration bool `json:"requires_concentration"` RequiresConcentration bool `json:"requires_concentration"`
CastingTime string `json:"casting_time"` CastingTime string `json:"casting_time"`
LevelInt int `json:"level_int"` LevelInt int `json:"level_int"`
School string `json:"school"` School string `json:"school"`
// SpellLists is the structured class list, but the SRD dump leaves it // SpellLists is the structured class list, but the SRD dump leaves it
// incomplete (no paladin entries at all). DndClass is the free-text // incomplete (no paladin entries at all). DndClass is the free-text
// "Druid, Wizard" field and is the more complete source — mapClasses // "Druid, Wizard" field and is the more complete source — mapClasses

View File

@@ -21,10 +21,10 @@ import (
type AdventurePlugin struct { type AdventurePlugin struct {
Base Base
euro *EuroPlugin euro *EuroPlugin
xp *XPPlugin xp *XPPlugin
achievements *AchievementsPlugin achievements *AchievementsPlugin
mu sync.Mutex mu sync.Mutex
dmToPlayer map[id.RoomID]id.UserID dmToPlayer map[id.RoomID]id.UserID
pending sync.Map // userID string -> *advPendingInteraction pending sync.Map // userID string -> *advPendingInteraction
userLocks sync.Map // userID string -> *sync.Mutex userLocks sync.Map // userID string -> *sync.Mutex
@@ -37,8 +37,8 @@ type AdventurePlugin struct {
hospitalNudges sync.Map // userID string -> time.Time (when to send nudge) hospitalNudges sync.Map // userID string -> time.Time (when to send nudge)
craftResults sync.Map // userID string -> []CraftResult (pending craft results for narrative) craftResults sync.Map // userID string -> []CraftResult (pending craft results for narrative)
treasureUndo sync.Map // userID string -> *advTreasureUndoToken (10-min auto-swap reversal) treasureUndo sync.Map // userID string -> *advTreasureUndoToken (10-min auto-swap reversal)
morningHour int morningHour int
summaryHour int summaryHour int
// stopCh is closed by Stop() to signal all background tickers // stopCh is closed by Stop() to signal all background tickers
// (morning/summary/midnight/event/rival/robbie/hospital/mortgage/ // (morning/summary/midnight/event/rival/robbie/hospital/mortgage/
@@ -1558,4 +1558,3 @@ func (p *AdventurePlugin) handleBoostCmd(ctx MessageContext) error {
advSetBoost(true) advSetBoost(true)
return p.SendReply(ctx.RoomID, ctx.EventID, "⚡ Double XP/money boost **enabled**! All adventure XP and loot values are doubled.") return p.SendReply(ctx.RoomID, ctx.EventID, "⚡ Double XP/money boost **enabled**! All adventure XP and loot values are doubled.")
} }

View File

@@ -642,7 +642,7 @@ type AdvActionResult struct {
FlavorKey string FlavorKey string
EquipBroken []EquipmentSlot EquipBroken []EquipmentSlot
NearDeath bool NearDeath bool
StreakBonus int StreakBonus int
CombatLog *CombatResult CombatLog *CombatResult
// MasteryCrossings records equipment slots whose ActionsUsed crossed a // MasteryCrossings records equipment slots whose ActionsUsed crossed a
// mastery threshold (advMasteryThresholds) on this action. The caller // mastery threshold (advMasteryThresholds) on this action. The caller

View File

@@ -116,12 +116,12 @@ func loadAdvDailyActivity(date string) (map[id.UserID][]AdvDailyActivity, error)
} }
for rows.Next() { for rows.Next() {
var ( var (
uid, zoneID string uid, zoneID string
visitedJSON string visitedJSON string
totalRooms int totalRooms int
abandoned, bossDefeated int abandoned, bossDefeated int
completedAt *time.Time completedAt *time.Time
lastAction time.Time lastAction time.Time
) )
if err := rows.Scan(&uid, &zoneID, &visitedJSON, &totalRooms, if err := rows.Scan(&uid, &zoneID, &visitedJSON, &totalRooms,
&abandoned, &bossDefeated, &completedAt, &lastAction); err != nil { &abandoned, &bossDefeated, &completedAt, &lastAction); err != nil {

View File

@@ -16,20 +16,20 @@ import (
// ── Arena Run State ───────────────────────────────────────────────────────── // ── Arena Run State ─────────────────────────────────────────────────────────
type ArenaRun struct { type ArenaRun struct {
ID int64 ID int64
UserID id.UserID UserID id.UserID
RoomID id.RoomID RoomID id.RoomID
StartTier int StartTier int
Tier int Tier int
Round int Round int
Status string // "active", "awaiting", "completed", "dead", "cashed_out" Status string // "active", "awaiting", "completed", "dead", "cashed_out"
Earnings int64 // session total (multiplied euros accumulated across completed tiers) Earnings int64 // session total (multiplied euros accumulated across completed tiers)
TierEarnings int64 // current tier's raw earnings (reset each tier) TierEarnings int64 // current tier's raw earnings (reset each tier)
XPAccumulated int // session XP accumulator (raw, multiplied at payout) XPAccumulated int // session XP accumulator (raw, multiplied at payout)
RoundsSurvived int RoundsSurvived int
LastMonster string LastMonster string
StartedAt time.Time StartedAt time.Time
EndedAt *time.Time EndedAt *time.Time
} }
// ── Command Dispatch ──────────────────────────────────────────────────────── // ── Command Dispatch ────────────────────────────────────────────────────────
@@ -1015,8 +1015,8 @@ func loadArenaPersonalStats(userID id.UserID) *ArenaPersonalStats {
type ArenaGearSet struct { type ArenaGearSet struct {
Tier int Tier int
SetKey string // DB key: "bloodied", "ironclad", etc. SetKey string // DB key: "bloodied", "ironclad", etc.
SetName string // Display: "Bloodied", "Ironclad", etc. SetName string // Display: "Bloodied", "Ironclad", etc.
HelmetName string HelmetName string
Description string Description string
DropRate float64 DropRate float64

View File

@@ -138,12 +138,12 @@ func TestArenaBossOutcome_WinSurfacesStagedLogAndBossDeath(t *testing.T) {
victoryHeadline := "🏆 **" + monster.Name + "** falls (HP " + "60" + "→" + "42" + " / 60)." victoryHeadline := "🏆 **" + monster.Name + "** falls (HP " + "60" + "→" + "42" + " / 60)."
outcome := renderBossOutcome(BossOutcomeInputs{ outcome := renderBossOutcome(BossOutcomeInputs{
ZoneID: ZoneArena, ZoneID: ZoneArena,
RunID: "arena-stagedlog-test", RunID: "arena-stagedlog-test",
RoomIdx: 11, RoomIdx: 11,
Monster: monster, Monster: monster,
Result: result, Result: result,
PreHP: 60, PostHP: 42, MaxHP: 60, PreHP: 60, PostHP: 42, MaxHP: 60,
PhaseTwoAt: arenaBossPhaseTwoAt(1), PhaseTwoAt: arenaBossPhaseTwoAt(1),
Nat20s: 1, Nat20s: 1,
Nat1s: 0, Nat1s: 0,

View File

@@ -126,12 +126,12 @@ func renderArenaStatus(run *ArenaRun) string {
// ── Leaderboard ───────────────────────────────────────────────────────────── // ── Leaderboard ─────────────────────────────────────────────────────────────
type ArenaLeaderboardEntry struct { type ArenaLeaderboardEntry struct {
DisplayName string DisplayName string
TotalEarnings int64 TotalEarnings int64
HighestTier int HighestTier int
Tier5Completions int Tier5Completions int
TotalRuns int TotalRuns int
TotalDeaths int TotalDeaths int
} }
func renderArenaLeaderboard(season string, entries []ArenaLeaderboardEntry) string { func renderArenaLeaderboard(season string, entries []ArenaLeaderboardEntry) string {

View File

@@ -318,15 +318,15 @@ func renderBabysitSummary(char *AdventureCharacter, logs []babysitLogEntry) stri
// ── Babysit Log CRUD ──────────────────────────────────────────────────────── // ── Babysit Log CRUD ────────────────────────────────────────────────────────
type babysitLogEntry struct { type babysitLogEntry struct {
ID int64 ID int64
UserID id.UserID UserID id.UserID
LogDate string LogDate string
Activity string Activity string
Outcome string Outcome string
GoldEarned int GoldEarned int
XPGained int XPGained int
ItemsDropped string ItemsDropped string
RivalRefused string RivalRefused string
} }
func logBabysitActivity(userID id.UserID, activity, outcome string, gold, xp int, items string) { func logBabysitActivity(userID id.UserID, activity, outcome string, gold, xp int, items string) {

View File

@@ -66,11 +66,11 @@ func TestRunBabysitDailyTrickle_AccumulatesAndLevels(t *testing.T) {
defer func() { _ = recover() }() defer func() { _ = recover() }()
char := &AdventureCharacter{ char := &AdventureCharacter{
UserID: id.UserID("@accum:example"), UserID: id.UserID("@accum:example"),
PetType: "dog", PetType: "dog",
PetName: "Rex", PetName: "Rex",
PetLevel: 1, // needs 10 XP = 1000 centi-XP to L2 PetLevel: 1, // needs 10 XP = 1000 centi-XP to L2
PetXP: 900, PetXP: 900,
BabysitActive: true, BabysitActive: true,
} }
expires := time.Now().UTC().Add(48 * time.Hour) expires := time.Now().UTC().Add(48 * time.Hour)

View File

@@ -24,32 +24,32 @@ var allSlots = []EquipmentSlot{SlotWeapon, SlotArmor, SlotHelmet, SlotBoots, Slo
// ── Core Types ─────────────────────────────────────────────────────────────── // ── Core Types ───────────────────────────────────────────────────────────────
type AdventureCharacter struct { type AdventureCharacter struct {
UserID id.UserID UserID id.UserID
DisplayName string DisplayName string
CombatLevel int CombatLevel int
MiningSkill int MiningSkill int
ForagingSkill int ForagingSkill int
FishingSkill int // v2 FishingSkill int // v2
CombatXP int CombatXP int
MiningXP int MiningXP int
ForagingXP int ForagingXP int
FishingXP int // v2 FishingXP int // v2
Alive bool Alive bool
DeadUntil *time.Time DeadUntil *time.Time
ActionTakenToday bool ActionTakenToday bool
HolidayActionTaken bool HolidayActionTaken bool
CombatActionsUsed int CombatActionsUsed int
HarvestActionsUsed int HarvestActionsUsed int
ArenaWins int // v2 ArenaWins int // v2
ArenaLosses int // v2 ArenaLosses int // v2
InvasionScore int // v2 InvasionScore int // v2
Title string // v2 Title string // v2
CurrentStreak int CurrentStreak int
BestStreak int BestStreak int
LastActionDate string LastActionDate string
GrudgeLocation string GrudgeLocation string
CreatedAt time.Time CreatedAt time.Time
LastActiveAt time.Time LastActiveAt time.Time
DeathReprieveLast *time.Time DeathReprieveLast *time.Time
MasterworkDropsReceived int MasterworkDropsReceived int
RivalPool int RivalPool int
@@ -78,27 +78,27 @@ type AdventureCharacter struct {
HouseAutopay bool HouseAutopay bool
HouseCurrentRate float64 HouseCurrentRate float64
// Pets // Pets
PetType string PetType string
PetName string PetName string
PetXP int PetXP int
PetLevel int PetLevel int
PetArmorTier int PetArmorTier int
PetChasedAway bool PetChasedAway bool
PetReactivated bool PetReactivated bool
PetArrived bool PetArrived bool
MistyEncounterCount int MistyEncounterCount int
MistyDonatedCount int MistyDonatedCount int
ThomAnimalLineFired bool ThomAnimalLineFired bool
PetSupplyShopUnlocked bool PetSupplyShopUnlocked bool
PetLevel10Date string PetLevel10Date string
PetMorningDefense bool PetMorningDefense bool
AutoBabysit bool AutoBabysit bool
AutoBabysitFocus string // mining|fishing|foraging — preferred skill for auto-babysit; "" defaults to weakest AutoBabysitFocus string // mining|fishing|foraging — preferred skill for auto-babysit; "" defaults to weakest
TreasuresLocked bool // when true, treasure drops at cap are refused instead of auto-swapping TreasuresLocked bool // when true, treasure drops at cap are refused instead of auto-swapping
StreakDecayed bool StreakDecayed bool
CraftsSucceeded int CraftsSucceeded int
DeathSource string // "adventure" | "arena" | "" (legacy/unknown — treated as adventure) DeathSource string // "adventure" | "arena" | "" (legacy/unknown — treated as adventure)
DeathLocation string // human-readable location of last death; cleared on revive is not required DeathLocation string // human-readable location of last death; cleared on revive is not required
} }
type AdvEquipment struct { type AdvEquipment struct {

View File

@@ -17,26 +17,26 @@ import (
type ConsumableEffect string type ConsumableEffect string
const ( const (
EffectHeal ConsumableEffect = "heal" EffectHeal ConsumableEffect = "heal"
EffectDefBoost ConsumableEffect = "def_boost" EffectDefBoost ConsumableEffect = "def_boost"
EffectAtkBoost ConsumableEffect = "atk_boost" EffectAtkBoost ConsumableEffect = "atk_boost"
EffectWard ConsumableEffect = "ward" EffectWard ConsumableEffect = "ward"
EffectSpeedBoost ConsumableEffect = "speed_boost" EffectSpeedBoost ConsumableEffect = "speed_boost"
EffectCritBoost ConsumableEffect = "crit_boost" EffectCritBoost ConsumableEffect = "crit_boost"
EffectFlatDmg ConsumableEffect = "flat_dmg" EffectFlatDmg ConsumableEffect = "flat_dmg"
EffectSpore ConsumableEffect = "spore" EffectSpore ConsumableEffect = "spore"
EffectReflect ConsumableEffect = "reflect" EffectReflect ConsumableEffect = "reflect"
EffectAutoCrit ConsumableEffect = "auto_crit" EffectAutoCrit ConsumableEffect = "auto_crit"
) )
type ConsumableDef struct { type ConsumableDef struct {
Name string Name string
Effect ConsumableEffect Effect ConsumableEffect
Value float64 // meaning depends on effect Value float64 // meaning depends on effect
Tier int Tier int
Buyable bool Buyable bool
Price int64 Price int64
Slot string // "offensive" or "defensive" Slot string // "offensive" or "defensive"
} }
// ConsumableItem is an AdvItem that has consumable properties. // ConsumableItem is an AdvItem that has consumable properties.

View File

@@ -79,4 +79,3 @@ var blacksmithBrokenCondition = []string{
"_looks at the condition, looks at you, looks back at the condition_ You know it costs more when you let it get like this. Of course you know. You just didn't care. That's fine. I care enough for both of us. It'll cost you.", "_looks at the condition, looks at you, looks back at the condition_ You know it costs more when you let it get like this. Of course you know. You just didn't care. That's fine. I care enough for both of us. It'll cost you.",
"This could have been avoided with regular visits. _slides the cost estimate across the counter without breaking eye contact_", "This could have been avoided with regular visits. _slides the cost estimate across the counter without breaking eye contact_",
} }

View File

@@ -924,4 +924,4 @@ var DungeonExceptional = map[int][]string{
"in a dungeon. Write it down. " + "in a dungeon. Write it down. " +
"The dungeon will tell this story differently.", "The dungeon will tell this story differently.",
}, },
} }

View File

@@ -275,6 +275,7 @@ var PetCatDeflect = []string{
"Your cat is already somewhere else.\n\nIt blinks.\n\nNot at you.\n\nAt nothing.\n\n" + "Your cat is already somewhere else.\n\nIt blinks.\n\nNot at you.\n\nAt nothing.\n\n" +
"The way cats do.", "The way cats do.",
} }
// Fires randomly in the morning DM. Cat has left something. // Fires randomly in the morning DM. Cat has left something.
// Results in a defense boost for the day. // Results in a defense boost for the day.
// The cat is proud. The cat will never stop doing this. // The cat is proud. The cat will never stop doing this.

View File

@@ -97,12 +97,12 @@ func TestAdvMasteryRowSegment(t *testing.T) {
func TestAdvMasteryRollup(t *testing.T) { func TestAdvMasteryRollup(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
used map[EquipmentSlot]int used map[EquipmentSlot]int
wantCrossed int wantCrossed int
wantMaxed int wantMaxed int
wantBonus float64 wantBonus float64
wantAnyProgress bool wantAnyProgress bool
}{ }{
{"empty", nil, 0, 0, 0, false}, {"empty", nil, 0, 0, 0, false},
{"sub-50 only", map[EquipmentSlot]int{SlotWeapon: 30}, 0, 0, 0, true}, {"sub-50 only", map[EquipmentSlot]int{SlotWeapon: 30}, 0, 0, 0, true},
@@ -240,12 +240,12 @@ func TestAdvTreasureIrreplaceable(t *testing.T) {
func TestRenderCraftingTeaser_BracketBoundaries(t *testing.T) { func TestRenderCraftingTeaser_BracketBoundaries(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
foraging int foraging int
craftsSucceeded int craftsSucceeded int
wantEmpty bool wantEmpty bool
wantContainsAny []string // any of these (OR) wantContainsAny []string // any of these (OR)
wantNotContains []string wantNotContains []string
}{ }{
{"foraging 6 — out of pre-window", 6, 0, true, nil, nil}, {"foraging 6 — out of pre-window", 6, 0, true, nil, nil},
{"foraging 7 — pre-unlock with 3 levels left, plural", 7, 0, false, []string{"3 Foraging level"}, []string{"unlocks in 1 ", "unlocks in 0"}}, {"foraging 7 — pre-unlock with 3 levels left, plural", 7, 0, false, []string{"3 Foraging level"}, []string{"unlocks in 1 ", "unlocks in 0"}},
@@ -297,7 +297,7 @@ func TestCraftingReminderWeekday_Spread(t *testing.T) {
func TestCraftingReminderWeekday_StableWithinWeek(t *testing.T) { func TestCraftingReminderWeekday_StableWithinWeek(t *testing.T) {
uid := id.UserID("@stable:test") uid := id.UserID("@stable:test")
mon := time.Date(2026, 5, 4, 0, 0, 0, 0, time.UTC) // Monday mon := time.Date(2026, 5, 4, 0, 0, 0, 0, time.UTC) // Monday
sun := time.Date(2026, 5, 10, 23, 0, 0, 0, time.UTC) // Sunday same ISO week sun := time.Date(2026, 5, 10, 23, 0, 0, 0, time.UTC) // Sunday same ISO week
if craftingReminderWeekday(uid, mon) != craftingReminderWeekday(uid, sun) { if craftingReminderWeekday(uid, mon) != craftingReminderWeekday(uid, sun) {
t.Error("weekday should be stable within an ISO week") t.Error("weekday should be stable within an ISO week")

View File

@@ -207,7 +207,7 @@ func (p *AdventurePlugin) handleHospitalCmd(ctx MessageContext) error {
// Store pending interaction // Store pending interaction
p.pending.Store(string(ctx.Sender), &advPendingInteraction{ p.pending.Store(string(ctx.Sender), &advPendingInteraction{
Type: "hospital_pay", Type: "hospital_pay",
Data: &advPendingHospitalPay{ Data: &advPendingHospitalPay{
Cost: afterInsurance, Cost: afterInsurance,
Discounted: haggleOutcome == haggleWin, Discounted: haggleOutcome == haggleWin,
@@ -277,7 +277,7 @@ func (p *AdventurePlugin) resolveHospitalPay(ctx MessageContext, interaction *ad
gr := gamesRoom() gr := gamesRoom()
if gr != "" { if gr != "" {
name, _ := loadDisplayName(char.UserID) name, _ := loadDisplayName(char.UserID)
p.SendMessage(gr, fmt.Sprintf(hospitalDitchAnnounce, name)) p.SendMessage(gr, fmt.Sprintf(hospitalDitchAnnounce, name))
} }
return nil return nil
} }

View File

@@ -144,10 +144,10 @@ func TestHouseHPBonus(t *testing.T) {
expected float64 expected float64
}{ }{
{0, 0}, {0, 0},
{1, 0}, // Base house — no bonus {1, 0}, // Base house — no bonus
{2, 0.05}, // Livable {2, 0.05}, // Livable
{3, 0.12}, // Comfortable {3, 0.12}, // Comfortable
{4, 0.20}, // Established {4, 0.20}, // Established
} }
for _, tt := range tests { for _, tt := range tests {
char := &AdventureCharacter{HouseTier: tt.tier} char := &AdventureCharacter{HouseTier: tt.tier}

View File

@@ -15,8 +15,8 @@ import (
type MasterworkDef struct { type MasterworkDef struct {
Slot EquipmentSlot Slot EquipmentSlot
Activity AdvActivityType Activity AdvActivityType
SkillSource string // "mining", "fishing", "foraging" SkillSource string // "mining", "fishing", "foraging"
Tier int // 1-5, matches location tier Tier int // 1-5, matches location tier
Name string Name string
Description string // character sheet / trade listing Description string // character sheet / trade listing
DropRate float64 // per-tier: 0.05, 0.04, 0.03, 0.02, 0.005 DropRate float64 // per-tier: 0.05, 0.04, 0.03, 0.02, 0.005

View File

@@ -16,11 +16,11 @@ import (
// ── NPC Constants ────────────────────────────────────────────────────────── // ── NPC Constants ──────────────────────────────────────────────────────────
const ( const (
mistyCost = 100 mistyCost = 100
arinaCost = 5000 arinaCost = 5000
npcCooldownDays = 7 npcCooldownDays = 7
npcEncounterChance = 0.075 // 7.5% npcEncounterChance = 0.075 // 7.5%
npcBuffDuration = 7 * 24 * time.Hour npcBuffDuration = 7 * 24 * time.Hour
// Arena effect chances per round // Arena effect chances per round
mistyEffectChance = 0.20 // 20% mistyEffectChance = 0.20 // 20%
@@ -337,10 +337,10 @@ func (p *AdventurePlugin) resolveArina(ctx MessageContext, char *AdventureCharac
// Returns: extra text to append to round log, enemy HP modifier, player damage taken. // Returns: extra text to append to round log, enemy HP modifier, player damage taken.
type npcArenaResult struct { type npcArenaResult struct {
Text string Text string
EnemyDmg int // damage dealt to enemy EnemyDmg int // damage dealt to enemy
PlayerDmg int // damage dealt to player PlayerDmg int // damage dealt to player
SniperKill bool // enemy instant kill SniperKill bool // enemy instant kill
CondRepair int // equipment condition repair amount CondRepair int // equipment condition repair amount
} }
func npcCheckArenaEffects(char *AdventureCharacter, monsterName string) *npcArenaResult { func npcCheckArenaEffects(char *AdventureCharacter, monsterName string) *npcArenaResult {

View File

@@ -729,12 +729,12 @@ type AdvPlayerDaySummary struct {
ForagingSkill int ForagingSkill int
FishingSkill int FishingSkill int
Activity string Activity string
Location string Location string
Outcome string Outcome string
LootValue int64 LootValue int64
IsDead bool IsDead bool
DeadUntil string DeadUntil string
// DeadUntilHours is the integer hours-from-now until revival, used by // DeadUntilHours is the integer hours-from-now until revival, used by
// the standout-death template's {hours} placeholder. Computed when // the standout-death template's {hours} placeholder. Computed when
// the summary row is built; 0 if not dead or already past revival. // the summary row is built; 0 if not dead or already past revival.
@@ -744,11 +744,11 @@ type AdvPlayerDaySummary struct {
// {duration} placeholder for templates that want precision over the // {duration} placeholder for templates that want precision over the
// rounded {hours} count. // rounded {hours} count.
DeadUntilDuration string DeadUntilDuration string
IsResting bool IsResting bool
SummaryLine string SummaryLine string
HolidayActions int // 0 = not holiday or no action; 1 = took one; 2 = took both HolidayActions int // 0 = not holiday or no action; 1 = took one; 2 = took both
DeathSource string DeathSource string
DeathLocation string DeathLocation string
} }
// advPlayerHeadline renders the per-player headline for the daily report. // advPlayerHeadline renders the per-player headline for the daily report.
@@ -920,7 +920,6 @@ func renderAdvDailySummary(date string, tb *TwinBeeResult, tbRewards TwinBeeRewa
sb.WriteString("\n") sb.WriteString("\n")
} }
// Standout // Standout
if bestPlayer != nil && bestPlayer.LootValue > 0 { if bestPlayer != nil && bestPlayer.LootValue > 0 {
pool := SummaryStandoutGood pool := SummaryStandoutGood
@@ -984,8 +983,8 @@ func renderAdvLeaderboard(chars []AdvLeaderboardEntry) string {
// Sort by score // Sort by score
type entry struct { type entry struct {
Name string Name string
Score int Score int
Levels string Levels string
Streak int Streak int
} }

View File

@@ -281,17 +281,17 @@ func TestLuigiCategoryIntros_Coverage(t *testing.T) {
func TestLuigiFlavorPools_NonEmpty(t *testing.T) { func TestLuigiFlavorPools_NonEmpty(t *testing.T) {
pools := map[string][]string{ pools := map[string][]string{
"luigiGreetings": luigiGreetings, "luigiGreetings": luigiGreetings,
"luigiPurchaseConfirm": luigiPurchaseConfirm, "luigiPurchaseConfirm": luigiPurchaseConfirm,
"luigiTier5Confirm": luigiTier5Confirm, "luigiTier5Confirm": luigiTier5Confirm,
"luigiComboConfirm": luigiComboConfirm, "luigiComboConfirm": luigiComboConfirm,
"luigiInsufficientFunds": luigiInsufficientFunds, "luigiInsufficientFunds": luigiInsufficientFunds,
"luigiBrowseTimeout": luigiBrowseTimeout, "luigiBrowseTimeout": luigiBrowseTimeout,
"luigiMaxedOut": luigiMaxedOut, "luigiMaxedOut": luigiMaxedOut,
"luigiMasterworkAck": luigiMasterworkAck, "luigiMasterworkAck": luigiMasterworkAck,
"luigiShowAllComment": luigiShowAllComment, "luigiShowAllComment": luigiShowAllComment,
"luigiCommentary": luigiCommentary, "luigiCommentary": luigiCommentary,
"luigiCancellation": luigiCancellation, "luigiCancellation": luigiCancellation,
} }
for name, pool := range pools { for name, pool := range pools {
if len(pool) == 0 { if len(pool) == 0 {

View File

@@ -231,13 +231,13 @@ func TestCalculateAdvProbabilities_BetterGearReducesDeath(t *testing.T) {
func TestAdvEffectiveSkill(t *testing.T) { func TestAdvEffectiveSkill(t *testing.T) {
char := &AdventureCharacter{ char := &AdventureCharacter{
CombatLevel: 10, CombatLevel: 10,
MiningSkill: 15, MiningSkill: 15,
ForagingSkill: 20, ForagingSkill: 20,
} }
bonuses := &AdvBonusSummary{ bonuses := &AdvBonusSummary{
CombatBonus: 5, CombatBonus: 5,
MiningBonus: 3, MiningBonus: 3,
ForagingBonus: 0, ForagingBonus: 0,
} }

View File

@@ -322,11 +322,11 @@ func (p *AdventurePlugin) distributeTwinBeeRewards(result *TwinBeeResult) TwinBe
// ── TwinBee Gifts (Temporary Buffs) ────────────────────────────────────────── // ── TwinBee Gifts (Temporary Buffs) ──────────────────────────────────────────
type twinBeeGiftDef struct { type twinBeeGiftDef struct {
BuffType string BuffType string
BuffName string BuffName string
Modifier float64 Modifier float64
Duration time.Duration Duration time.Duration
Flavor string Flavor string
} }
var twinBeeGifts = []twinBeeGiftDef{ var twinBeeGifts = []twinBeeGiftDef{

View File

@@ -20,15 +20,15 @@ import (
// jikanAnime represents an anime entry from the Jikan API. // jikanAnime represents an anime entry from the Jikan API.
type jikanAnime struct { type jikanAnime struct {
MalID int `json:"mal_id"` MalID int `json:"mal_id"`
URL string `json:"url"` URL string `json:"url"`
Title string `json:"title"` Title string `json:"title"`
TitleEng string `json:"title_english"` TitleEng string `json:"title_english"`
Type string `json:"type"` Type string `json:"type"`
Episodes int `json:"episodes"` Episodes int `json:"episodes"`
Status string `json:"status"` Status string `json:"status"`
Score float64 `json:"score"` Score float64 `json:"score"`
Synopsis string `json:"synopsis"` Synopsis string `json:"synopsis"`
Aired struct { Aired struct {
String string `json:"string"` String string `json:"string"`
From string `json:"from"` From string `json:"from"`

View File

@@ -29,51 +29,51 @@ const (
thLinkmasterMinMsgs = 50 thLinkmasterMinMsgs = 50
// Temporal // Temporal
thNightOwlPct = 40 thNightOwlPct = 40
thNightOwlMinMsgs = 100 thNightOwlMinMsgs = 100
thEarlyBirdPct = 40 thEarlyBirdPct = 40
thEarlyBirdMinMsgs = 100 thEarlyBirdMinMsgs = 100
// Emotional (LLM-gated) // Emotional (LLM-gated)
thEmotionalMinClassified = 100 thEmotionalMinClassified = 100
thCheerleaderPosPct = 50 thCheerleaderPosPct = 50
thPhilosopherNeutPct = 40 thPhilosopherNeutPct = 40
thPhilosopherQPct = 10 thPhilosopherQPct = 10
thPhilosopherAvgWords = 8 thPhilosopherAvgWords = 8
thAgitatorNegPct = 30 thAgitatorNegPct = 30
thAgitatorMinMsgs = 200 thAgitatorMinMsgs = 200
thWildcardStdDev = 0.5 thWildcardStdDev = 0.5
thWildcardMinClassified = 150 thWildcardMinClassified = 150
thHypeMachineExclPct = 20 thHypeMachineExclPct = 20
thHypeMachinePosPct = 60 thHypeMachinePosPct = 60
// Economy // Economy
thBrokeSpiritedMaxBalance = 100.0 thBrokeSpiritedMaxBalance = 100.0
thDegenerateMinLosses = 10 thDegenerateMinLosses = 10
// Games // Games
thSharkWinRate = 55 thSharkWinRate = 55
thSharkMinGames = 15 thSharkMinGames = 15
thWordleMinPuzzles = 10 thWordleMinPuzzles = 10
thArenaChampMinTier = 4 thArenaChampMinTier = 4
thArenaChampWinRate = 50 thArenaChampWinRate = 50
thArenaCowardMinRuns = 3 thArenaCowardMinRuns = 3
thArenaCowardMaxAvgTier = 2 thArenaCowardMaxAvgTier = 2
thTriviaNerdMinCorrect = 10 thTriviaNerdMinCorrect = 10
// Adventure // Adventure
thAdvMinDays = 10 thAdvMinDays = 10
thAdvDiverseMinDays = 15 thAdvDiverseMinDays = 15
thAdvDiverseMinTypes = 3 thAdvDiverseMinTypes = 3
thAdvDiverseMaxShare = 40 thAdvDiverseMaxShare = 40
thGearheadMinMasterwork = 3 thGearheadMinMasterwork = 3
// Communication (vocabulary) // Communication (vocabulary)
thWordsmithMinFancyWords = 10 thWordsmithMinFancyWords = 10
// Social // Social
thPatronMinRepGiven = 5 thPatronMinRepGiven = 5
thPatronRatioMultiplier = 2 thPatronRatioMultiplier = 2
// Display // Display
maxDisplayArchetypes = 6 maxDisplayArchetypes = 6
@@ -83,13 +83,13 @@ const (
var archetypeFlavors = map[string]string{ var archetypeFlavors = map[string]string{
// Communication // Communication
"Novelist": "Writes in paragraphs. Has opinions. Probably re-reads their own messages.", "Novelist": "Writes in paragraphs. Has opinions. Probably re-reads their own messages.",
"Minimalist": "Says a lot with very little. You're never sure if they're fine or not.", "Minimalist": "Says a lot with very little. You're never sure if they're fine or not.",
"Inquisitor": "Always asking. Never satisfied with the first answer. Probably has follow-ups.", "Inquisitor": "Always asking. Never satisfied with the first answer. Probably has follow-ups.",
"Enthusiast": "Genuinely excited about things. All the things. Possibly all at once.", "Enthusiast": "Genuinely excited about things. All the things. Possibly all at once.",
"Chatterbox": "Has thoughts. Many thoughts. Shares them all. You wouldn't have it any other way.", "Chatterbox": "Has thoughts. Many thoughts. Shares them all. You wouldn't have it any other way.",
"Linkmaster": "The community's unofficial curator. Their tab count is not your business.", "Linkmaster": "The community's unofficial curator. Their tab count is not your business.",
"Wordsmith": "Uses words most people have to look up. The thesaurus fears them.", "Wordsmith": "Uses words most people have to look up. The thesaurus fears them.",
// Temporal // Temporal
"Night Owl": "Awake when they probably shouldn't be. Thriving despite all evidence.", "Night Owl": "Awake when they probably shouldn't be. Thriving despite all evidence.",
@@ -103,8 +103,8 @@ var archetypeFlavors = map[string]string{
"Hype Machine": "Arrived and immediately made everything louder. The room is better for it.", "Hype Machine": "Arrived and immediately made everything louder. The room is better for it.",
// Economy // Economy
"Whale": "Has money. Spends money. Has more money somehow. The math is unclear.", "Whale": "Has money. Spends money. Has more money somehow. The math is unclear.",
"Degenerate": "Knows exactly what they're doing. Does it anyway. Respects it.", "Degenerate": "Knows exactly what they're doing. Does it anyway. Respects it.",
"Broke But Spirited": "Down but not out. The pot fears them anyway.", "Broke But Spirited": "Down but not out. The pot fears them anyway.",
// Games // Games
@@ -221,16 +221,16 @@ type userData struct {
userID string userID string
// user_stats // user_stats
totalMsgs int totalMsgs int
totalWords int totalWords int
totalLinks int totalLinks int
totalImages int totalImages int
totalQuestions int totalQuestions int
totalExcl int totalExcl int
totalEmojis int totalEmojis int
nightMsgs int nightMsgs int
morningMsgs int morningMsgs int
fancyWords int fancyWords int
// sentiment_stats // sentiment_stats
sentPositive int sentPositive int
@@ -242,7 +242,7 @@ type userData struct {
sentVariance float64 sentVariance float64
// economy // economy
balance float64 balance float64
gamblingLosses int gamblingLosses int
recentGaming bool recentGaming bool
@@ -251,7 +251,7 @@ type userData struct {
hmPlayed, hmWon int hmPlayed, hmWon int
holdemPlayed int holdemPlayed int
holdemNetPositive bool holdemNetPositive bool
unoSoloPlayed, unoSoloWon int unoSoloPlayed, unoSoloWon int
unoMultiPlayed, unoMultiWon int unoMultiPlayed, unoMultiWon int
wordlePlayed int wordlePlayed int
triviaCorrect int triviaCorrect int
@@ -263,13 +263,13 @@ type userData struct {
arenaAvgCashTier float64 arenaAvgCashTier float64
// adventure // adventure
advDays int advDays int
advActivities map[string]int // activity_type -> count advActivities map[string]int // activity_type -> count
masterworkCount int masterworkCount int
// social // social
repGiven int repGiven int
repReceived int repReceived int
reactionsGiven int reactionsGiven int
} }

View File

@@ -14,327 +14,327 @@ package plugin
func buildTunedBestiarySRD() map[string]DnDMonsterTemplate { func buildTunedBestiarySRD() map[string]DnDMonsterTemplate {
return map[string]DnDMonsterTemplate{ return map[string]DnDMonsterTemplate{
"awakened_shrub": {ID: "awakened_shrub", Name: "Awakened Shrub", CR: 0, HP: 10, AC: 10, Attack: 1, AttackBonus: 1, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: False Appearance"}, "awakened_shrub": {ID: "awakened_shrub", Name: "Awakened Shrub", CR: 0, HP: 10, AC: 10, Attack: 1, AttackBonus: 1, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: False Appearance"},
"baboon": {ID: "baboon", Name: "Baboon", CR: 0, HP: 3, AC: 12, Attack: 1, AttackBonus: 1, Speed: 12, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Pack Tactics"}, "baboon": {ID: "baboon", Name: "Baboon", CR: 0, HP: 3, AC: 12, Attack: 1, AttackBonus: 1, Speed: 12, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Pack Tactics"},
"badger": {ID: "badger", Name: "Badger", CR: 0, HP: 3, AC: 10, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Smell"}, "badger": {ID: "badger", Name: "Badger", CR: 0, HP: 3, AC: 10, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Smell"},
"bat": {ID: "bat", Name: "Bat", CR: 0, HP: 1, AC: 12, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, Keen Hearing"}, "bat": {ID: "bat", Name: "Bat", CR: 0, HP: 1, AC: 12, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, Keen Hearing"},
"cat": {ID: "cat", Name: "Cat", CR: 0, HP: 2, AC: 12, Attack: 1, AttackBonus: 0, Speed: 16, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Smell"}, "cat": {ID: "cat", Name: "Cat", CR: 0, HP: 2, AC: 12, Attack: 1, AttackBonus: 0, Speed: 16, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Smell"},
"commoner": {ID: "commoner", Name: "Commoner", CR: 0, HP: 4, AC: 10, Attack: 1, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "commoner": {ID: "commoner", Name: "Commoner", CR: 0, HP: 4, AC: 10, Attack: 1, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"crab": {ID: "crab", Name: "Crab", CR: 0, HP: 2, AC: 11, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"}, "crab": {ID: "crab", Name: "Crab", CR: 0, HP: 2, AC: 11, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
"deer": {ID: "deer", Name: "Deer", CR: 0, HP: 4, AC: 13, Attack: 1, AttackBonus: 2, Speed: 18, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "deer": {ID: "deer", Name: "Deer", CR: 0, HP: 4, AC: 13, Attack: 1, AttackBonus: 2, Speed: 18, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"eagle": {ID: "eagle", Name: "Eagle", CR: 0, HP: 3, AC: 12, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Sight"}, "eagle": {ID: "eagle", Name: "Eagle", CR: 0, HP: 3, AC: 12, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Sight"},
"frog": {ID: "frog", Name: "Frog", CR: 0, HP: 1, AC: 11, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Amphibious, Standing Leap"}, "frog": {ID: "frog", Name: "Frog", CR: 0, HP: 1, AC: 11, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Amphibious, Standing Leap"},
"giant_fire_beetle": {ID: "giant_fire_beetle", Name: "Giant Fire Beetle", CR: 0, HP: 4, AC: 13, Attack: 1, AttackBonus: 1, Speed: 12, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Illumination"}, "giant_fire_beetle": {ID: "giant_fire_beetle", Name: "Giant Fire Beetle", CR: 0, HP: 4, AC: 13, Attack: 1, AttackBonus: 1, Speed: 12, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Illumination"},
"goat": {ID: "goat", Name: "Goat", CR: 0, HP: 4, AC: 10, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Sure-Footed"}, "goat": {ID: "goat", Name: "Goat", CR: 0, HP: 4, AC: 10, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Sure-Footed"},
"hawk": {ID: "hawk", Name: "Hawk", CR: 0, HP: 1, AC: 13, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Sight"}, "hawk": {ID: "hawk", Name: "Hawk", CR: 0, HP: 1, AC: 13, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Sight"},
"homunculus": {ID: "homunculus", Name: "Homunculus", CR: 0, HP: 5, AC: 13, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Telepathic Bond"}, "homunculus": {ID: "homunculus", Name: "Homunculus", CR: 0, HP: 5, AC: 13, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Telepathic Bond"},
"hyena": {ID: "hyena", Name: "Hyena", CR: 0, HP: 5, AC: 11, Attack: 1, AttackBonus: 2, Speed: 18, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Pack Tactics"}, "hyena": {ID: "hyena", Name: "Hyena", CR: 0, HP: 5, AC: 11, Attack: 1, AttackBonus: 2, Speed: 18, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Pack Tactics"},
"jackal": {ID: "jackal", Name: "Jackal", CR: 0, HP: 3, AC: 12, Attack: 1, AttackBonus: 1, Speed: 16, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics"}, "jackal": {ID: "jackal", Name: "Jackal", CR: 0, HP: 3, AC: 12, Attack: 1, AttackBonus: 1, Speed: 16, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics"},
"lemure": {ID: "lemure", Name: "Lemure", CR: 0, HP: 13, AC: 10, Attack: 1, AttackBonus: 3, Speed: 6, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Traits: Devil's Sight, Hellish Rejuvenation"}, "lemure": {ID: "lemure", Name: "Lemure", CR: 0, HP: 13, AC: 10, Attack: 1, AttackBonus: 3, Speed: 6, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Traits: Devil's Sight, Hellish Rejuvenation"},
"lizard": {ID: "lizard", Name: "Lizard", CR: 0, HP: 2, AC: 10, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "lizard": {ID: "lizard", Name: "Lizard", CR: 0, HP: 2, AC: 10, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"octopus": {ID: "octopus", Name: "Octopus", CR: 0, HP: 3, AC: 12, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Hold Breath, Underwater Camouflage, Water Breathing"}, "octopus": {ID: "octopus", Name: "Octopus", CR: 0, HP: 3, AC: 12, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Hold Breath, Underwater Camouflage, Water Breathing"},
"owl": {ID: "owl", Name: "Owl", CR: 0, HP: 1, AC: 11, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Flyby, Keen Hearing and Sight"}, "owl": {ID: "owl", Name: "Owl", CR: 0, HP: 1, AC: 11, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Flyby, Keen Hearing and Sight"},
"quipper": {ID: "quipper", Name: "Quipper", CR: 0, HP: 1, AC: 13, Attack: 1, AttackBonus: 0, Speed: 16, BlockRate: 0.05, XPValue: 10, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Blood Frenzy, Water Breathing"}, "quipper": {ID: "quipper", Name: "Quipper", CR: 0, HP: 1, AC: 13, Attack: 1, AttackBonus: 0, Speed: 16, BlockRate: 0.05, XPValue: 10, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Blood Frenzy, Water Breathing"},
"rat": {ID: "rat", Name: "Rat", CR: 0, HP: 1, AC: 10, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Smell"}, "rat": {ID: "rat", Name: "Rat", CR: 0, HP: 1, AC: 10, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Smell"},
"raven": {ID: "raven", Name: "Raven", CR: 0, HP: 1, AC: 12, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Mimicry"}, "raven": {ID: "raven", Name: "Raven", CR: 0, HP: 1, AC: 12, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Mimicry"},
"scorpion": {ID: "scorpion", Name: "Scorpion", CR: 0, HP: 1, AC: 11, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "scorpion": {ID: "scorpion", Name: "Scorpion", CR: 0, HP: 1, AC: 11, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"sea_horse": {ID: "sea_horse", Name: "Sea Horse", CR: 0, HP: 1, AC: 11, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Water Breathing"}, "sea_horse": {ID: "sea_horse", Name: "Sea Horse", CR: 0, HP: 1, AC: 11, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Water Breathing"},
"shrieker": {ID: "shrieker", Name: "Shrieker", CR: 0, HP: 13, AC: 10, Attack: 1, AttackBonus: 0, Speed: 10, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: False Appearance"}, "shrieker": {ID: "shrieker", Name: "Shrieker", CR: 0, HP: 13, AC: 10, Attack: 1, AttackBonus: 0, Speed: 10, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: False Appearance"},
"spider": {ID: "spider", Name: "Spider", CR: 0, HP: 1, AC: 12, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Spider Climb, Web Sense, Web Walker"}, "spider": {ID: "spider", Name: "Spider", CR: 0, HP: 1, AC: 12, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Spider Climb, Web Sense, Web Walker"},
"vulture": {ID: "vulture", Name: "Vulture", CR: 0, HP: 5, AC: 10, Attack: 1, AttackBonus: 2, Speed: 6, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Sight and Smell, Pack Tactics"}, "vulture": {ID: "vulture", Name: "Vulture", CR: 0, HP: 5, AC: 10, Attack: 1, AttackBonus: 2, Speed: 6, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Sight and Smell, Pack Tactics"},
"weasel": {ID: "weasel", Name: "Weasel", CR: 0, HP: 1, AC: 13, Attack: 1, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"}, "weasel": {ID: "weasel", Name: "Weasel", CR: 0, HP: 1, AC: 13, Attack: 1, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"},
"bandit": {ID: "bandit", Name: "Bandit", CR: 0.125, HP: 11, AC: 12, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "bandit": {ID: "bandit", Name: "Bandit", CR: 0.125, HP: 11, AC: 12, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"blood_hawk": {ID: "blood_hawk", Name: "Blood Hawk", CR: 0.125, HP: 7, AC: 12, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 25, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Sight, Pack Tactics"}, "blood_hawk": {ID: "blood_hawk", Name: "Blood Hawk", CR: 0.125, HP: 7, AC: 12, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 25, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Sight, Pack Tactics"},
"camel": {ID: "camel", Name: "Camel", CR: 0.125, HP: 15, AC: 10, Attack: 1, AttackBonus: 5, Speed: 18, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "camel": {ID: "camel", Name: "Camel", CR: 0.125, HP: 15, AC: 10, Attack: 1, AttackBonus: 5, Speed: 18, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"cultist": {ID: "cultist", Name: "Cultist", CR: 0.125, HP: 9, AC: 12, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Dark Devotion"}, "cultist": {ID: "cultist", Name: "Cultist", CR: 0.125, HP: 9, AC: 12, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Dark Devotion"},
"flying_snake": {ID: "flying_snake", Name: "Flying Snake", CR: 0.125, HP: 5, AC: 14, Attack: 1, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 25, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Flyby"}, "flying_snake": {ID: "flying_snake", Name: "Flying Snake", CR: 0.125, HP: 5, AC: 14, Attack: 1, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 25, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Flyby"},
"giant_crab": {ID: "giant_crab", Name: "Giant Crab", CR: 0.125, HP: 13, AC: 15, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0.1, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"}, "giant_crab": {ID: "giant_crab", Name: "Giant Crab", CR: 0.125, HP: 13, AC: 15, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0.1, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
"giant_rat": {ID: "giant_rat", Name: "Giant Rat", CR: 0.125, HP: 7, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 25, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Smell, Pack Tactics"}, "giant_rat": {ID: "giant_rat", Name: "Giant Rat", CR: 0.125, HP: 7, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 25, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Smell, Pack Tactics"},
"giant_rat_diseased": {ID: "giant_rat_diseased", Name: "Giant Rat", CR: 0.125, HP: 7, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "giant_rat_diseased": {ID: "giant_rat_diseased", Name: "Giant Rat", CR: 0.125, HP: 7, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"giant_weasel": {ID: "giant_weasel", Name: "Giant Weasel", CR: 0.125, HP: 9, AC: 13, Attack: 1, AttackBonus: 5, Speed: 16, BlockRate: 0.05, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"}, "giant_weasel": {ID: "giant_weasel", Name: "Giant Weasel", CR: 0.125, HP: 9, AC: 13, Attack: 1, AttackBonus: 5, Speed: 16, BlockRate: 0.05, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"},
"guard": {ID: "guard", Name: "Guard", CR: 0.125, HP: 11, AC: 16, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0.1, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "guard": {ID: "guard", Name: "Guard", CR: 0.125, HP: 11, AC: 16, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0.1, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"kobold": {ID: "kobold", Name: "Kobold", CR: 0.125, HP: 5, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 25, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Sunlight Sensitivity, Pack Tactics"}, "kobold": {ID: "kobold", Name: "Kobold", CR: 0.125, HP: 5, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 25, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Sunlight Sensitivity, Pack Tactics"},
"mastiff": {ID: "mastiff", Name: "Mastiff", CR: 0.125, HP: 5, AC: 12, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"}, "mastiff": {ID: "mastiff", Name: "Mastiff", CR: 0.125, HP: 5, AC: 12, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"},
"merfolk": {ID: "merfolk", Name: "Merfolk", CR: 0.125, HP: 11, AC: 11, Attack: 1, AttackBonus: 2, Speed: 6, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"}, "merfolk": {ID: "merfolk", Name: "Merfolk", CR: 0.125, HP: 11, AC: 11, Attack: 1, AttackBonus: 2, Speed: 6, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
"mule": {ID: "mule", Name: "Mule", CR: 0.125, HP: 11, AC: 10, Attack: 1, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Beast of Burden, Sure-Footed"}, "mule": {ID: "mule", Name: "Mule", CR: 0.125, HP: 11, AC: 10, Attack: 1, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Beast of Burden, Sure-Footed"},
"noble": {ID: "noble", Name: "Noble", CR: 0.125, HP: 9, AC: 15, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0.1, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "noble": {ID: "noble", Name: "Noble", CR: 0.125, HP: 9, AC: 15, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0.1, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"poisonous_snake": {ID: "poisonous_snake", Name: "Poisonous Snake", CR: 0.125, HP: 2, AC: 13, Attack: 1, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "poisonous_snake": {ID: "poisonous_snake", Name: "Poisonous Snake", CR: 0.125, HP: 2, AC: 13, Attack: 1, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"pony": {ID: "pony", Name: "Pony", CR: 0.125, HP: 11, AC: 10, Attack: 1, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "pony": {ID: "pony", Name: "Pony", CR: 0.125, HP: 11, AC: 10, Attack: 1, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"stirge": {ID: "stirge", Name: "Stirge", CR: 0.125, HP: 2, AC: 14, Attack: 1, AttackBonus: 5, Speed: 6, BlockRate: 0.05, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "stirge": {ID: "stirge", Name: "Stirge", CR: 0.125, HP: 2, AC: 14, Attack: 1, AttackBonus: 5, Speed: 6, BlockRate: 0.05, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"tribal_warrior": {ID: "tribal_warrior", Name: "Tribal Warrior", CR: 0.125, HP: 11, AC: 12, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 25, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Pack Tactics"}, "tribal_warrior": {ID: "tribal_warrior", Name: "Tribal Warrior", CR: 0.125, HP: 11, AC: 12, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 25, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Pack Tactics"},
"acolyte": {ID: "acolyte", Name: "Acolyte", CR: 0.25, HP: 9, AC: 10, Attack: 1, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Spellcasting"}, "acolyte": {ID: "acolyte", Name: "Acolyte", CR: 0.25, HP: 9, AC: 10, Attack: 1, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Spellcasting"},
"axe_beak": {ID: "axe_beak", Name: "Axe Beak", CR: 0.25, HP: 19, AC: 11, Attack: 1, AttackBonus: 4, Speed: 18, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "axe_beak": {ID: "axe_beak", Name: "Axe Beak", CR: 0.25, HP: 19, AC: 11, Attack: 1, AttackBonus: 4, Speed: 18, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"blink_dog": {ID: "blink_dog", Name: "Blink Dog", CR: 0.25, HP: 22, AC: 13, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"}, "blink_dog": {ID: "blink_dog", Name: "Blink Dog", CR: 0.25, HP: 22, AC: 13, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"},
"boar": {ID: "boar", Name: "Boar", CR: 0.25, HP: 11, AC: 11, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Relentless (Recharges after a Short or Long Rest)"}, "boar": {ID: "boar", Name: "Boar", CR: 0.25, HP: 11, AC: 11, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Relentless (Recharges after a Short or Long Rest)"},
"constrictor_snake": {ID: "constrictor_snake", Name: "Constrictor Snake", CR: 0.25, HP: 13, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "constrictor_snake": {ID: "constrictor_snake", Name: "Constrictor Snake", CR: 0.25, HP: 13, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"draft_horse": {ID: "draft_horse", Name: "Draft Horse", CR: 0.25, HP: 19, AC: 10, Attack: 1, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "draft_horse": {ID: "draft_horse", Name: "Draft Horse", CR: 0.25, HP: 19, AC: 10, Attack: 1, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"dretch": {ID: "dretch", Name: "Dretch", CR: 0.25, HP: 18, AC: 11, Attack: 1, AttackBonus: 2, Speed: 8, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dretch makes two attacks: one with its bite and one with its claws."}, "dretch": {ID: "dretch", Name: "Dretch", CR: 0.25, HP: 18, AC: 11, Attack: 1, AttackBonus: 2, Speed: 8, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dretch makes two attacks: one with its bite and one with its claws."},
"drow": {ID: "drow", Name: "Drow", CR: 0.25, HP: 13, AC: 15, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 50, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Fey Ancestry, Innate Spellcasting, Sunlight Sensitivity"}, "drow": {ID: "drow", Name: "Drow", CR: 0.25, HP: 13, AC: 15, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 50, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Fey Ancestry, Innate Spellcasting, Sunlight Sensitivity"},
"elk": {ID: "elk", Name: "Elk", CR: 0.25, HP: 13, AC: 10, Attack: 1, AttackBonus: 0, Speed: 18, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge"}, "elk": {ID: "elk", Name: "Elk", CR: 0.25, HP: 13, AC: 10, Attack: 1, AttackBonus: 0, Speed: 18, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge"},
"flying_sword": {ID: "flying_sword", Name: "Flying Sword", CR: 0.25, HP: 17, AC: 17, Attack: 1, AttackBonus: 3, Speed: 18, BlockRate: 0.1, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Antimagic Susceptibility, False Appearance"}, "flying_sword": {ID: "flying_sword", Name: "Flying Sword", CR: 0.25, HP: 17, AC: 17, Attack: 1, AttackBonus: 3, Speed: 18, BlockRate: 0.1, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Antimagic Susceptibility, False Appearance"},
"giant_badger": {ID: "giant_badger", Name: "Giant Badger", CR: 0.25, HP: 13, AC: 10, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The badger makes two attacks: one with its bite and one with its claws. Traits: Keen Smell"}, "giant_badger": {ID: "giant_badger", Name: "Giant Badger", CR: 0.25, HP: 13, AC: 10, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The badger makes two attacks: one with its bite and one with its claws. Traits: Keen Smell"},
"giant_bat": {ID: "giant_bat", Name: "Giant Bat", CR: 0.25, HP: 22, AC: 13, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, Keen Hearing"}, "giant_bat": {ID: "giant_bat", Name: "Giant Bat", CR: 0.25, HP: 22, AC: 13, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, Keen Hearing"},
"giant_centipede": {ID: "giant_centipede", Name: "Giant Centipede", CR: 0.25, HP: 4, AC: 13, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "giant_centipede": {ID: "giant_centipede", Name: "Giant Centipede", CR: 0.25, HP: 4, AC: 13, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"giant_frog": {ID: "giant_frog", Name: "Giant Frog", CR: 0.25, HP: 18, AC: 11, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Amphibious, Standing Leap"}, "giant_frog": {ID: "giant_frog", Name: "Giant Frog", CR: 0.25, HP: 18, AC: 11, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Amphibious, Standing Leap"},
"giant_lizard": {ID: "giant_lizard", Name: "Giant Lizard", CR: 0.25, HP: 19, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Variant: Hold Breath, Variant: Spider Climb"}, "giant_lizard": {ID: "giant_lizard", Name: "Giant Lizard", CR: 0.25, HP: 19, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Variant: Hold Breath, Variant: Spider Climb"},
"giant_owl": {ID: "giant_owl", Name: "Giant Owl", CR: 0.25, HP: 19, AC: 12, Attack: 1, AttackBonus: 3, Speed: 6, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Flyby, Keen Hearing and Sight"}, "giant_owl": {ID: "giant_owl", Name: "Giant Owl", CR: 0.25, HP: 19, AC: 12, Attack: 1, AttackBonus: 3, Speed: 6, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Flyby, Keen Hearing and Sight"},
"giant_poisonous_snake": {ID: "giant_poisonous_snake", Name: "Giant Poisonous Snake", CR: 0.25, HP: 11, AC: 14, Attack: 1, AttackBonus: 6, Speed: 12, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "giant_poisonous_snake": {ID: "giant_poisonous_snake", Name: "Giant Poisonous Snake", CR: 0.25, HP: 11, AC: 14, Attack: 1, AttackBonus: 6, Speed: 12, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"giant_wolf_spider": {ID: "giant_wolf_spider", Name: "Giant Wolf Spider", CR: 0.25, HP: 11, AC: 13, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Spider Climb, Web Sense, Web Walker"}, "giant_wolf_spider": {ID: "giant_wolf_spider", Name: "Giant Wolf Spider", CR: 0.25, HP: 11, AC: 13, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Spider Climb, Web Sense, Web Walker"},
"goblin": {ID: "goblin", Name: "Goblin", CR: 0.25, HP: 7, AC: 15, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 50, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Nimble Escape"}, "goblin": {ID: "goblin", Name: "Goblin", CR: 0.25, HP: 7, AC: 15, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 50, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Nimble Escape"},
"grimlock": {ID: "grimlock", Name: "Grimlock", CR: 0.25, HP: 11, AC: 11, Attack: 1, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Blind Senses, Keen Hearing and Smell, Stone Camouflage"}, "grimlock": {ID: "grimlock", Name: "Grimlock", CR: 0.25, HP: 11, AC: 11, Attack: 1, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Blind Senses, Keen Hearing and Smell, Stone Camouflage"},
"panther": {ID: "panther", Name: "Panther", CR: 0.25, HP: 13, AC: 12, Attack: 1, AttackBonus: 4, Speed: 18, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Keen Smell, Pounce"}, "panther": {ID: "panther", Name: "Panther", CR: 0.25, HP: 13, AC: 12, Attack: 1, AttackBonus: 4, Speed: 18, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Keen Smell, Pounce"},
"pseudodragon": {ID: "pseudodragon", Name: "Pseudodragon", CR: 0.25, HP: 7, AC: 13, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0.05, XPValue: 50, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Traits: Keen Senses, Magic Resistance, Limited Telepathy, Variant: Familiar"}, "pseudodragon": {ID: "pseudodragon", Name: "Pseudodragon", CR: 0.25, HP: 7, AC: 13, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0.05, XPValue: 50, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Traits: Keen Senses, Magic Resistance, Limited Telepathy, Variant: Familiar"},
"riding_horse": {ID: "riding_horse", Name: "Riding Horse", CR: 0.25, HP: 13, AC: 10, Attack: 1, AttackBonus: 5, Speed: 18, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "riding_horse": {ID: "riding_horse", Name: "Riding Horse", CR: 0.25, HP: 13, AC: 10, Attack: 1, AttackBonus: 5, Speed: 18, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"skeleton": {ID: "skeleton", Name: "Skeleton", CR: 0.25, HP: 13, AC: 13, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "skeleton": {ID: "skeleton", Name: "Skeleton", CR: 0.25, HP: 13, AC: 13, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"sprite": {ID: "sprite", Name: "Sprite", CR: 0.25, HP: 2, AC: 15, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0.1, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "sprite": {ID: "sprite", Name: "Sprite", CR: 0.25, HP: 2, AC: 15, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0.1, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"steam_mephit": {ID: "steam_mephit", Name: "Steam Mephit", CR: 0.25, HP: 21, AC: 10, Attack: 1, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, Innate Spellcasting (1/Day)"}, "steam_mephit": {ID: "steam_mephit", Name: "Steam Mephit", CR: 0.25, HP: 21, AC: 10, Attack: 1, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, Innate Spellcasting (1/Day)"},
"swarm_of_bats": {ID: "swarm_of_bats", Name: "Swarm of Bats", CR: 0.25, HP: 22, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, Keen Hearing, Swarm"}, "swarm_of_bats": {ID: "swarm_of_bats", Name: "Swarm of Bats", CR: 0.25, HP: 22, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, Keen Hearing, Swarm"},
"swarm_of_rats": {ID: "swarm_of_rats", Name: "Swarm of Rats", CR: 0.25, HP: 24, AC: 10, Attack: 1, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Smell, Swarm"}, "swarm_of_rats": {ID: "swarm_of_rats", Name: "Swarm of Rats", CR: 0.25, HP: 24, AC: 10, Attack: 1, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Smell, Swarm"},
"swarm_of_ravens": {ID: "swarm_of_ravens", Name: "Swarm of Ravens", CR: 0.25, HP: 24, AC: 12, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"}, "swarm_of_ravens": {ID: "swarm_of_ravens", Name: "Swarm of Ravens", CR: 0.25, HP: 24, AC: 12, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
"violet_fungus": {ID: "violet_fungus", Name: "Violet Fungus", CR: 0.25, HP: 18, AC: 10, Attack: 1, AttackBonus: 2, Speed: 6, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The fungus makes 1d4 Rotting Touch attacks. Traits: False Appearance"}, "violet_fungus": {ID: "violet_fungus", Name: "Violet Fungus", CR: 0.25, HP: 18, AC: 10, Attack: 1, AttackBonus: 2, Speed: 6, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The fungus makes 1d4 Rotting Touch attacks. Traits: False Appearance"},
"wolf": {ID: "wolf", Name: "Wolf", CR: 0.25, HP: 11, AC: 13, Attack: 1, AttackBonus: 4, Speed: 16, BlockRate: 0.05, XPValue: 50, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics"}, "wolf": {ID: "wolf", Name: "Wolf", CR: 0.25, HP: 11, AC: 13, Attack: 1, AttackBonus: 4, Speed: 16, BlockRate: 0.05, XPValue: 50, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics"},
"zombie": {ID: "zombie", Name: "Zombie", CR: 0.25, HP: 22, AC: 10, Attack: 1, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Undead Fortitude", Phase: "decisive", ProcChance: 0.5, Effect: "survive_at_1"}, Notes: "SRD-tuned baseline — Undead Fortitude (survive_at_1) wired from traits. Traits: Undead Fortitude"}, "zombie": {ID: "zombie", Name: "Zombie", CR: 0.25, HP: 22, AC: 10, Attack: 1, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Undead Fortitude", Phase: "decisive", ProcChance: 0.5, Effect: "survive_at_1"}, Notes: "SRD-tuned baseline — Undead Fortitude (survive_at_1) wired from traits. Traits: Undead Fortitude"},
"ape": {ID: "ape", Name: "Ape", CR: 0.5, HP: 19, AC: 12, Attack: 2, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The ape makes two fist attacks."}, "ape": {ID: "ape", Name: "Ape", CR: 0.5, HP: 19, AC: 12, Attack: 2, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The ape makes two fist attacks."},
"black_bear": {ID: "black_bear", Name: "Black Bear", CR: 0.5, HP: 19, AC: 11, Attack: 2, AttackBonus: 3, Speed: 16, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The bear makes two attacks: one with its bite and one with its claws. Traits: Keen Smell"}, "black_bear": {ID: "black_bear", Name: "Black Bear", CR: 0.5, HP: 19, AC: 11, Attack: 2, AttackBonus: 3, Speed: 16, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The bear makes two attacks: one with its bite and one with its claws. Traits: Keen Smell"},
"cockatrice": {ID: "cockatrice", Name: "Cockatrice", CR: 0.5, HP: 27, AC: 11, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "cockatrice": {ID: "cockatrice", Name: "Cockatrice", CR: 0.5, HP: 27, AC: 11, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"crocodile": {ID: "crocodile", Name: "Crocodile", CR: 0.5, HP: 19, AC: 12, Attack: 2, AttackBonus: 4, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Hold Breath"}, "crocodile": {ID: "crocodile", Name: "Crocodile", CR: 0.5, HP: 19, AC: 12, Attack: 2, AttackBonus: 4, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Hold Breath"},
"darkmantle": {ID: "darkmantle", Name: "Darkmantle", CR: 0.5, HP: 22, AC: 11, Attack: 2, AttackBonus: 5, Speed: 6, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, False Appearance"}, "darkmantle": {ID: "darkmantle", Name: "Darkmantle", CR: 0.5, HP: 22, AC: 11, Attack: 2, AttackBonus: 5, Speed: 6, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, False Appearance"},
"deep_gnome_svirfneblin": {ID: "deep_gnome_svirfneblin", Name: "Deep Gnome", CR: 0.5, HP: 16, AC: 15, Attack: 2, AttackBonus: 4, Speed: 8, BlockRate: 0.1, XPValue: 100, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Stone Camouflage, Gnome Cunning, Innate Spellcasting"}, "deep_gnome_svirfneblin": {ID: "deep_gnome_svirfneblin", Name: "Deep Gnome", CR: 0.5, HP: 16, AC: 15, Attack: 2, AttackBonus: 4, Speed: 8, BlockRate: 0.1, XPValue: 100, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Stone Camouflage, Gnome Cunning, Innate Spellcasting"},
"dust_mephit": {ID: "dust_mephit", Name: "Dust Mephit", CR: 0.5, HP: 17, AC: 12, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, Innate Spellcasting (1/Day)"}, "dust_mephit": {ID: "dust_mephit", Name: "Dust Mephit", CR: 0.5, HP: 17, AC: 12, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, Innate Spellcasting (1/Day)"},
"giant_goat": {ID: "giant_goat", Name: "Giant Goat", CR: 0.5, HP: 19, AC: 11, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Sure-Footed"}, "giant_goat": {ID: "giant_goat", Name: "Giant Goat", CR: 0.5, HP: 19, AC: 11, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Sure-Footed"},
"giant_sea_horse": {ID: "giant_sea_horse", Name: "Giant Sea Horse", CR: 0.5, HP: 16, AC: 13, Attack: 2, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Water Breathing"}, "giant_sea_horse": {ID: "giant_sea_horse", Name: "Giant Sea Horse", CR: 0.5, HP: 16, AC: 13, Attack: 2, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Water Breathing"},
"giant_wasp": {ID: "giant_wasp", Name: "Giant Wasp", CR: 0.5, HP: 13, AC: 12, Attack: 2, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "giant_wasp": {ID: "giant_wasp", Name: "Giant Wasp", CR: 0.5, HP: 13, AC: 12, Attack: 2, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"gnoll": {ID: "gnoll", Name: "Gnoll", CR: 0.5, HP: 22, AC: 15, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 100, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Rampage"}, "gnoll": {ID: "gnoll", Name: "Gnoll", CR: 0.5, HP: 22, AC: 15, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 100, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Rampage"},
"gray_ooze": {ID: "gray_ooze", Name: "Gray Ooze", CR: 0.5, HP: 22, AC: 10, Attack: 2, AttackBonus: 3, Speed: 6, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amorphous, Corrode Metal, False Appearance"}, "gray_ooze": {ID: "gray_ooze", Name: "Gray Ooze", CR: 0.5, HP: 22, AC: 10, Attack: 2, AttackBonus: 3, Speed: 6, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amorphous, Corrode Metal, False Appearance"},
"hobgoblin": {ID: "hobgoblin", Name: "Hobgoblin", CR: 0.5, HP: 11, AC: 18, Attack: 2, AttackBonus: 3, Speed: 12, BlockRate: 0.15, XPValue: 100, Ability: &MonsterAbility{Name: "Martial Advantage", Phase: "any", ProcChance: 0.35, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Martial Advantage (bonus_damage) wired from traits. Traits: Martial Advantage"}, "hobgoblin": {ID: "hobgoblin", Name: "Hobgoblin", CR: 0.5, HP: 11, AC: 18, Attack: 2, AttackBonus: 3, Speed: 12, BlockRate: 0.15, XPValue: 100, Ability: &MonsterAbility{Name: "Martial Advantage", Phase: "any", ProcChance: 0.35, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Martial Advantage (bonus_damage) wired from traits. Traits: Martial Advantage"},
"ice_mephit": {ID: "ice_mephit", Name: "Ice Mephit", CR: 0.5, HP: 21, AC: 11, Attack: 2, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, False Appearance, Innate Spellcasting (1/Day)"}, "ice_mephit": {ID: "ice_mephit", Name: "Ice Mephit", CR: 0.5, HP: 21, AC: 11, Attack: 2, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, False Appearance, Innate Spellcasting (1/Day)"},
"lizardfolk": {ID: "lizardfolk", Name: "Lizardfolk", CR: 0.5, HP: 22, AC: 15, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The lizardfolk makes two melee attacks, each one with a different weapon. Traits: Hold Breath"}, "lizardfolk": {ID: "lizardfolk", Name: "Lizardfolk", CR: 0.5, HP: 22, AC: 15, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The lizardfolk makes two melee attacks, each one with a different weapon. Traits: Hold Breath"},
"magma_mephit": {ID: "magma_mephit", Name: "Magma Mephit", CR: 0.5, HP: 22, AC: 11, Attack: 2, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, False Appearance, Innate Spellcasting (1/Day)"}, "magma_mephit": {ID: "magma_mephit", Name: "Magma Mephit", CR: 0.5, HP: 22, AC: 11, Attack: 2, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, False Appearance, Innate Spellcasting (1/Day)"},
"magmin": {ID: "magmin", Name: "Magmin", CR: 0.5, HP: 9, AC: 14, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 100, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, Ignited Illumination", FireAttacker: true}, "magmin": {ID: "magmin", Name: "Magmin", CR: 0.5, HP: 9, AC: 14, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 100, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, Ignited Illumination", FireAttacker: true},
"orc": {ID: "orc", Name: "Orc", CR: 0.5, HP: 15, AC: 13, Attack: 2, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 100, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Aggressive"}, "orc": {ID: "orc", Name: "Orc", CR: 0.5, HP: 15, AC: 13, Attack: 2, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 100, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Aggressive"},
"reef_shark": {ID: "reef_shark", Name: "Reef Shark", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Pack Tactics, Water Breathing"}, "reef_shark": {ID: "reef_shark", Name: "Reef Shark", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Pack Tactics, Water Breathing"},
"rust_monster": {ID: "rust_monster", Name: "Rust Monster", CR: 0.5, HP: 27, AC: 14, Attack: 2, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Iron Scent, Rust Metal"}, "rust_monster": {ID: "rust_monster", Name: "Rust Monster", CR: 0.5, HP: 27, AC: 14, Attack: 2, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Iron Scent, Rust Metal"},
"sahuagin": {ID: "sahuagin", Name: "Sahuagin", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Multiattack: The sahuagin makes two melee attacks: one with its bite and one with its claws or spear. Traits: Blood Frenzy, Limited Amphibiousness, Shark Telepathy"}, "sahuagin": {ID: "sahuagin", Name: "Sahuagin", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Multiattack: The sahuagin makes two melee attacks: one with its bite and one with its claws or spear. Traits: Blood Frenzy, Limited Amphibiousness, Shark Telepathy"},
"satyr": {ID: "satyr", Name: "Satyr", CR: 0.5, HP: 31, AC: 14, Attack: 2, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 100, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Traits: Magic Resistance"}, "satyr": {ID: "satyr", Name: "Satyr", CR: 0.5, HP: 31, AC: 14, Attack: 2, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 100, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Traits: Magic Resistance"},
"scout": {ID: "scout", Name: "Scout", CR: 0.5, HP: 16, AC: 13, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The scout makes two melee attacks or two ranged attacks. Traits: Keen Hearing and Sight"}, "scout": {ID: "scout", Name: "Scout", CR: 0.5, HP: 16, AC: 13, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The scout makes two melee attacks or two ranged attacks. Traits: Keen Hearing and Sight"},
"shadow": {ID: "shadow", Name: "Shadow", CR: 0.5, HP: 16, AC: 12, Attack: 2, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Surprise Attack", Phase: "opening", ProcChance: 0.8, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Surprise Attack (bonus_damage) wired from traits. Traits: Amorphous, Shadow Stealth, Sunlight Weakness"}, "shadow": {ID: "shadow", Name: "Shadow", CR: 0.5, HP: 16, AC: 12, Attack: 2, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Surprise Attack", Phase: "opening", ProcChance: 0.8, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Surprise Attack (bonus_damage) wired from traits. Traits: Amorphous, Shadow Stealth, Sunlight Weakness"},
"swarm_of_beetles": {ID: "swarm_of_beetles", Name: "Swarm of Beetles", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"}, "swarm_of_beetles": {ID: "swarm_of_beetles", Name: "Swarm of Beetles", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
"swarm_of_centipedes": {ID: "swarm_of_centipedes", Name: "Swarm of Centipedes", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"}, "swarm_of_centipedes": {ID: "swarm_of_centipedes", Name: "Swarm of Centipedes", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
"swarm_of_insects": {ID: "swarm_of_insects", Name: "Swarm of Insects", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"}, "swarm_of_insects": {ID: "swarm_of_insects", Name: "Swarm of Insects", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
"swarm_of_spiders": {ID: "swarm_of_spiders", Name: "Swarm of Spiders", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm, Spider Climb, Web Sense, Web Walker"}, "swarm_of_spiders": {ID: "swarm_of_spiders", Name: "Swarm of Spiders", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm, Spider Climb, Web Sense, Web Walker"},
"swarm_of_wasps": {ID: "swarm_of_wasps", Name: "Swarm of Wasps", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 6, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"}, "swarm_of_wasps": {ID: "swarm_of_wasps", Name: "Swarm of Wasps", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 6, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
"thug": {ID: "thug", Name: "Thug", CR: 0.5, HP: 32, AC: 11, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Multiattack: The thug makes two melee attacks. Traits: Pack Tactics"}, "thug": {ID: "thug", Name: "Thug", CR: 0.5, HP: 32, AC: 11, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Multiattack: The thug makes two melee attacks. Traits: Pack Tactics"},
"warhorse": {ID: "warhorse", Name: "Warhorse", CR: 0.5, HP: 19, AC: 11, Attack: 2, AttackBonus: 6, Speed: 18, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"}, "warhorse": {ID: "warhorse", Name: "Warhorse", CR: 0.5, HP: 19, AC: 11, Attack: 2, AttackBonus: 6, Speed: 18, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"},
"warhorse_skeleton": {ID: "warhorse_skeleton", Name: "Warhorse Skeleton", CR: 0.5, HP: 22, AC: 13, Attack: 2, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "warhorse_skeleton": {ID: "warhorse_skeleton", Name: "Warhorse Skeleton", CR: 0.5, HP: 22, AC: 13, Attack: 2, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"worg": {ID: "worg", Name: "Worg", CR: 0.5, HP: 26, AC: 13, Attack: 2, AttackBonus: 5, Speed: 18, BlockRate: 0.05, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"}, "worg": {ID: "worg", Name: "Worg", CR: 0.5, HP: 26, AC: 13, Attack: 2, AttackBonus: 5, Speed: 18, BlockRate: 0.05, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"},
"animated_armor": {ID: "animated_armor", Name: "Animated Armor", CR: 1, HP: 33, AC: 18, Attack: 2, AttackBonus: 4, Speed: 10, BlockRate: 0.15, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The armor makes two melee attacks. Traits: Antimagic Susceptibility, False Appearance"}, "animated_armor": {ID: "animated_armor", Name: "Animated Armor", CR: 1, HP: 33, AC: 18, Attack: 2, AttackBonus: 4, Speed: 10, BlockRate: 0.15, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The armor makes two melee attacks. Traits: Antimagic Susceptibility, False Appearance"},
"brass_dragon_wyrmling": {ID: "brass_dragon_wyrmling", Name: "Brass Dragon Wyrmling", CR: 1, HP: 16, AC: 16, Attack: 2, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat).", FireAttacker: true}, "brass_dragon_wyrmling": {ID: "brass_dragon_wyrmling", Name: "Brass Dragon Wyrmling", CR: 1, HP: 16, AC: 16, Attack: 2, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat).", FireAttacker: true},
"brown_bear": {ID: "brown_bear", Name: "Brown Bear", CR: 1, HP: 34, AC: 11, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The bear makes two attacks: one with its bite and one with its claws. Traits: Keen Smell"}, "brown_bear": {ID: "brown_bear", Name: "Brown Bear", CR: 1, HP: 34, AC: 11, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The bear makes two attacks: one with its bite and one with its claws. Traits: Keen Smell"},
"bugbear": {ID: "bugbear", Name: "Bugbear", CR: 1, HP: 27, AC: 16, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 200, Ability: &MonsterAbility{Name: "Surprise Attack", Phase: "opening", ProcChance: 0.8, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Surprise Attack (bonus_damage) wired from traits. Traits: Brute, Surprise Attack"}, "bugbear": {ID: "bugbear", Name: "Bugbear", CR: 1, HP: 27, AC: 16, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 200, Ability: &MonsterAbility{Name: "Surprise Attack", Phase: "opening", ProcChance: 0.8, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Surprise Attack (bonus_damage) wired from traits. Traits: Brute, Surprise Attack"},
"copper_dragon_wyrmling": {ID: "copper_dragon_wyrmling", Name: "Copper Dragon Wyrmling", CR: 1, HP: 22, AC: 16, Attack: 2, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "copper_dragon_wyrmling": {ID: "copper_dragon_wyrmling", Name: "Copper Dragon Wyrmling", CR: 1, HP: 22, AC: 16, Attack: 2, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"death_dog": {ID: "death_dog", Name: "Death Dog", CR: 1, HP: 39, AC: 12, Attack: 2, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dog makes two bite attacks. Traits: Two-Headed"}, "death_dog": {ID: "death_dog", Name: "Death Dog", CR: 1, HP: 39, AC: 12, Attack: 2, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dog makes two bite attacks. Traits: Two-Headed"},
"dire_wolf": {ID: "dire_wolf", Name: "Dire Wolf", CR: 1, HP: 37, AC: 14, Attack: 2, AttackBonus: 5, Speed: 18, BlockRate: 0.05, XPValue: 200, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics"}, "dire_wolf": {ID: "dire_wolf", Name: "Dire Wolf", CR: 1, HP: 37, AC: 14, Attack: 2, AttackBonus: 5, Speed: 18, BlockRate: 0.05, XPValue: 200, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics"},
"dryad": {ID: "dryad", Name: "Dryad", CR: 1, HP: 22, AC: 11, Attack: 2, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Innate Spellcasting, Magic Resistance, Speak with Beasts and Plants, Tree Stride"}, "dryad": {ID: "dryad", Name: "Dryad", CR: 1, HP: 22, AC: 11, Attack: 2, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Innate Spellcasting, Magic Resistance, Speak with Beasts and Plants, Tree Stride"},
"duergar": {ID: "duergar", Name: "Duergar", CR: 1, HP: 26, AC: 16, Attack: 2, AttackBonus: 4, Speed: 10, BlockRate: 0.1, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Duergar Resilience, Sunlight Sensitivity"}, "duergar": {ID: "duergar", Name: "Duergar", CR: 1, HP: 26, AC: 16, Attack: 2, AttackBonus: 4, Speed: 10, BlockRate: 0.1, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Duergar Resilience, Sunlight Sensitivity"},
"ghoul": {ID: "ghoul", Name: "Ghoul", CR: 1, HP: 22, AC: 12, Attack: 2, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "ghoul": {ID: "ghoul", Name: "Ghoul", CR: 1, HP: 22, AC: 12, Attack: 2, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"giant_eagle": {ID: "giant_eagle", Name: "Giant Eagle", CR: 1, HP: 26, AC: 13, Attack: 2, AttackBonus: 5, Speed: 6, BlockRate: 0.05, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The eagle makes two attacks: one with its beak and one with its talons. Traits: Keen Sight"}, "giant_eagle": {ID: "giant_eagle", Name: "Giant Eagle", CR: 1, HP: 26, AC: 13, Attack: 2, AttackBonus: 5, Speed: 6, BlockRate: 0.05, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The eagle makes two attacks: one with its beak and one with its talons. Traits: Keen Sight"},
"giant_hyena": {ID: "giant_hyena", Name: "Giant Hyena", CR: 1, HP: 45, AC: 12, Attack: 2, AttackBonus: 5, Speed: 18, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Rampage"}, "giant_hyena": {ID: "giant_hyena", Name: "Giant Hyena", CR: 1, HP: 45, AC: 12, Attack: 2, AttackBonus: 5, Speed: 18, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Rampage"},
"giant_octopus": {ID: "giant_octopus", Name: "Giant Octopus", CR: 1, HP: 52, AC: 11, Attack: 2, AttackBonus: 5, Speed: 6, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Hold Breath, Underwater Camouflage, Water Breathing"}, "giant_octopus": {ID: "giant_octopus", Name: "Giant Octopus", CR: 1, HP: 52, AC: 11, Attack: 2, AttackBonus: 5, Speed: 6, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Hold Breath, Underwater Camouflage, Water Breathing"},
"giant_spider": {ID: "giant_spider", Name: "Giant Spider", CR: 1, HP: 26, AC: 14, Attack: 2, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Spider Climb, Web Sense, Web Walker"}, "giant_spider": {ID: "giant_spider", Name: "Giant Spider", CR: 1, HP: 26, AC: 14, Attack: 2, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Spider Climb, Web Sense, Web Walker"},
"giant_toad": {ID: "giant_toad", Name: "Giant Toad", CR: 1, HP: 39, AC: 11, Attack: 2, AttackBonus: 4, Speed: 8, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Amphibious, Standing Leap"}, "giant_toad": {ID: "giant_toad", Name: "Giant Toad", CR: 1, HP: 39, AC: 11, Attack: 2, AttackBonus: 4, Speed: 8, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Amphibious, Standing Leap"},
"giant_vulture": {ID: "giant_vulture", Name: "Giant Vulture", CR: 1, HP: 22, AC: 10, Attack: 2, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Multiattack: The vulture makes two attacks: one with its beak and one with its talons. Traits: Keen Sight and Smell, Pack Tactics"}, "giant_vulture": {ID: "giant_vulture", Name: "Giant Vulture", CR: 1, HP: 22, AC: 10, Attack: 2, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Multiattack: The vulture makes two attacks: one with its beak and one with its talons. Traits: Keen Sight and Smell, Pack Tactics"},
"harpy": {ID: "harpy", Name: "Harpy", CR: 1, HP: 38, AC: 11, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The harpy makes two attacks: one with its claws and one with its club."}, "harpy": {ID: "harpy", Name: "Harpy", CR: 1, HP: 38, AC: 11, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The harpy makes two attacks: one with its claws and one with its club."},
"hippogriff": {ID: "hippogriff", Name: "Hippogriff", CR: 1, HP: 19, AC: 11, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The hippogriff makes two attacks: one with its beak and one with its claws. Traits: Keen Sight"}, "hippogriff": {ID: "hippogriff", Name: "Hippogriff", CR: 1, HP: 19, AC: 11, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The hippogriff makes two attacks: one with its beak and one with its claws. Traits: Keen Sight"},
"imp": {ID: "imp", Name: "Imp", CR: 1, HP: 10, AC: 13, Attack: 2, AttackBonus: 5, Speed: 8, BlockRate: 0.05, XPValue: 200, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Traits: Shapechanger, Devil's Sight, Magic Resistance, Variant: Familiar"}, "imp": {ID: "imp", Name: "Imp", CR: 1, HP: 10, AC: 13, Attack: 2, AttackBonus: 5, Speed: 8, BlockRate: 0.05, XPValue: 200, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Traits: Shapechanger, Devil's Sight, Magic Resistance, Variant: Familiar"},
"lion": {ID: "lion", Name: "Lion", CR: 1, HP: 26, AC: 12, Attack: 2, AttackBonus: 5, Speed: 18, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Keen Smell, Pack Tactics, Pounce, Running Leap"}, "lion": {ID: "lion", Name: "Lion", CR: 1, HP: 26, AC: 12, Attack: 2, AttackBonus: 5, Speed: 18, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Keen Smell, Pack Tactics, Pounce, Running Leap"},
"quasit": {ID: "quasit", Name: "Quasit", CR: 1, HP: 7, AC: 13, Attack: 2, AttackBonus: 4, Speed: 16, BlockRate: 0.05, XPValue: 200, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Traits: Shapechanger, Magic Resistance, Variant: Familiar"}, "quasit": {ID: "quasit", Name: "Quasit", CR: 1, HP: 7, AC: 13, Attack: 2, AttackBonus: 4, Speed: 16, BlockRate: 0.05, XPValue: 200, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Traits: Shapechanger, Magic Resistance, Variant: Familiar"},
"specter": {ID: "specter", Name: "Specter", CR: 1, HP: 22, AC: 12, Attack: 2, AttackBonus: 4, Speed: 18, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Incorporeal Movement, Sunlight Sensitivity"}, "specter": {ID: "specter", Name: "Specter", CR: 1, HP: 22, AC: 12, Attack: 2, AttackBonus: 4, Speed: 18, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Incorporeal Movement, Sunlight Sensitivity"},
"spy": {ID: "spy", Name: "Spy", CR: 1, HP: 27, AC: 12, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Martial Advantage", Phase: "any", ProcChance: 0.35, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Martial Advantage (bonus_damage) wired from traits. Multiattack: The spy makes two melee attacks. Traits: Cunning Action, Sneak Attack (1/Turn)"}, "spy": {ID: "spy", Name: "Spy", CR: 1, HP: 27, AC: 12, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Martial Advantage", Phase: "any", ProcChance: 0.35, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Martial Advantage (bonus_damage) wired from traits. Multiattack: The spy makes two melee attacks. Traits: Cunning Action, Sneak Attack (1/Turn)"},
"swarm_of_quippers": {ID: "swarm_of_quippers", Name: "Swarm of Quippers", CR: 1, HP: 28, AC: 13, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0.05, XPValue: 200, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Blood Frenzy, Swarm, Water Breathing"}, "swarm_of_quippers": {ID: "swarm_of_quippers", Name: "Swarm of Quippers", CR: 1, HP: 28, AC: 13, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0.05, XPValue: 200, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Blood Frenzy, Swarm, Water Breathing"},
"tiger": {ID: "tiger", Name: "Tiger", CR: 1, HP: 37, AC: 12, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Keen Smell, Pounce"}, "tiger": {ID: "tiger", Name: "Tiger", CR: 1, HP: 37, AC: 12, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Keen Smell, Pounce"},
"ankheg": {ID: "ankheg", Name: "Ankheg", CR: 2, HP: 39, AC: 14, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "ankheg": {ID: "ankheg", Name: "Ankheg", CR: 2, HP: 39, AC: 14, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"awakened_tree": {ID: "awakened_tree", Name: "Awakened Tree", CR: 2, HP: 59, AC: 13, Attack: 4, AttackBonus: 6, Speed: 8, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: False Appearance"}, "awakened_tree": {ID: "awakened_tree", Name: "Awakened Tree", CR: 2, HP: 59, AC: 13, Attack: 4, AttackBonus: 6, Speed: 8, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: False Appearance"},
"azer": {ID: "azer", Name: "Azer", CR: 2, HP: 39, AC: 17, Attack: 4, AttackBonus: 5, Speed: 12, BlockRate: 0.1, XPValue: 450, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Traits: Heated Body, Heated Weapons, Illumination"}, "azer": {ID: "azer", Name: "Azer", CR: 2, HP: 39, AC: 17, Attack: 4, AttackBonus: 5, Speed: 12, BlockRate: 0.1, XPValue: 450, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Traits: Heated Body, Heated Weapons, Illumination"},
"bandit_captain": {ID: "bandit_captain", Name: "Bandit Captain", CR: 2, HP: 65, AC: 15, Attack: 4, AttackBonus: 5, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The captain makes three melee attacks: two with its scimitar and one with its dagger."}, "bandit_captain": {ID: "bandit_captain", Name: "Bandit Captain", CR: 2, HP: 65, AC: 15, Attack: 4, AttackBonus: 5, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The captain makes three melee attacks: two with its scimitar and one with its dagger."},
"berserker": {ID: "berserker", Name: "Berserker", CR: 2, HP: 67, AC: 13, Attack: 4, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Reckless"}, "berserker": {ID: "berserker", Name: "Berserker", CR: 2, HP: 67, AC: 13, Attack: 4, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Reckless"},
"black_dragon_wyrmling": {ID: "black_dragon_wyrmling", Name: "Black Dragon Wyrmling", CR: 2, HP: 33, AC: 17, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"}, "black_dragon_wyrmling": {ID: "black_dragon_wyrmling", Name: "Black Dragon Wyrmling", CR: 2, HP: 33, AC: 17, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
"bronze_dragon_wyrmling": {ID: "bronze_dragon_wyrmling", Name: "Bronze Dragon Wyrmling", CR: 2, HP: 32, AC: 17, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"}, "bronze_dragon_wyrmling": {ID: "bronze_dragon_wyrmling", Name: "Bronze Dragon Wyrmling", CR: 2, HP: 32, AC: 17, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
"centaur": {ID: "centaur", Name: "Centaur", CR: 2, HP: 45, AC: 12, Attack: 4, AttackBonus: 6, Speed: 18, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Multiattack: The centaur makes two attacks: one with its pike and one with its hooves or two with its longbow. Traits: Charge"}, "centaur": {ID: "centaur", Name: "Centaur", CR: 2, HP: 45, AC: 12, Attack: 4, AttackBonus: 6, Speed: 18, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Multiattack: The centaur makes two attacks: one with its pike and one with its hooves or two with its longbow. Traits: Charge"},
"cult_fanatic": {ID: "cult_fanatic", Name: "Cult Fanatic", CR: 2, HP: 22, AC: 13, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The fanatic makes two melee attacks. Traits: Dark Devotion, Spellcasting"}, "cult_fanatic": {ID: "cult_fanatic", Name: "Cult Fanatic", CR: 2, HP: 22, AC: 13, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The fanatic makes two melee attacks. Traits: Dark Devotion, Spellcasting"},
"druid": {ID: "druid", Name: "Druid", CR: 2, HP: 27, AC: 11, Attack: 4, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Spellcasting"}, "druid": {ID: "druid", Name: "Druid", CR: 2, HP: 27, AC: 11, Attack: 4, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Spellcasting"},
"ettercap": {ID: "ettercap", Name: "Ettercap", CR: 2, HP: 44, AC: 13, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The ettercap makes two attacks: one with its bite and one with its claws. Traits: Spider Climb, Web Sense, Web Walker"}, "ettercap": {ID: "ettercap", Name: "Ettercap", CR: 2, HP: 44, AC: 13, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The ettercap makes two attacks: one with its bite and one with its claws. Traits: Spider Climb, Web Sense, Web Walker"},
"gargoyle": {ID: "gargoyle", Name: "Gargoyle", CR: 2, HP: 52, AC: 15, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The gargoyle makes two attacks: one with its bite and one with its claws. Traits: False Appearance"}, "gargoyle": {ID: "gargoyle", Name: "Gargoyle", CR: 2, HP: 52, AC: 15, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The gargoyle makes two attacks: one with its bite and one with its claws. Traits: False Appearance"},
"gelatinous_cube": {ID: "gelatinous_cube", Name: "Gelatinous Cube", CR: 2, HP: 84, AC: 10, Attack: 4, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Ooze Cube, Transparent"}, "gelatinous_cube": {ID: "gelatinous_cube", Name: "Gelatinous Cube", CR: 2, HP: 84, AC: 10, Attack: 4, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Ooze Cube, Transparent"},
"ghast": {ID: "ghast", Name: "Ghast", CR: 2, HP: 36, AC: 13, Attack: 4, AttackBonus: 3, Speed: 12, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Traits: Stench, Turn Defiance"}, "ghast": {ID: "ghast", Name: "Ghast", CR: 2, HP: 36, AC: 13, Attack: 4, AttackBonus: 3, Speed: 12, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Traits: Stench, Turn Defiance"},
"giant_boar": {ID: "giant_boar", Name: "Giant Boar", CR: 2, HP: 42, AC: 12, Attack: 4, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Relentless (Recharges after a Short or Long Rest)"}, "giant_boar": {ID: "giant_boar", Name: "Giant Boar", CR: 2, HP: 42, AC: 12, Attack: 4, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Relentless (Recharges after a Short or Long Rest)"},
"giant_constrictor_snake": {ID: "giant_constrictor_snake", Name: "Giant Constrictor Snake", CR: 2, HP: 60, AC: 12, Attack: 4, AttackBonus: 6, Speed: 12, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "giant_constrictor_snake": {ID: "giant_constrictor_snake", Name: "Giant Constrictor Snake", CR: 2, HP: 60, AC: 12, Attack: 4, AttackBonus: 6, Speed: 12, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"giant_elk": {ID: "giant_elk", Name: "Giant Elk", CR: 2, HP: 42, AC: 15, Attack: 4, AttackBonus: 6, Speed: 18, BlockRate: 0.1, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge"}, "giant_elk": {ID: "giant_elk", Name: "Giant Elk", CR: 2, HP: 42, AC: 15, Attack: 4, AttackBonus: 6, Speed: 18, BlockRate: 0.1, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge"},
"gibbering_mouther": {ID: "gibbering_mouther", Name: "Gibbering Mouther", CR: 2, HP: 67, AC: 10, Attack: 4, AttackBonus: 2, Speed: 6, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The gibbering mouther makes one bite attack and, if it can, uses its Blinding Spittle. Traits: Aberrant Ground, Gibbering"}, "gibbering_mouther": {ID: "gibbering_mouther", Name: "Gibbering Mouther", CR: 2, HP: 67, AC: 10, Attack: 4, AttackBonus: 2, Speed: 6, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The gibbering mouther makes one bite attack and, if it can, uses its Blinding Spittle. Traits: Aberrant Ground, Gibbering"},
"green_dragon_wyrmling": {ID: "green_dragon_wyrmling", Name: "Green Dragon Wyrmling", CR: 2, HP: 38, AC: 17, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"}, "green_dragon_wyrmling": {ID: "green_dragon_wyrmling", Name: "Green Dragon Wyrmling", CR: 2, HP: 38, AC: 17, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
"grick": {ID: "grick", Name: "Grick", CR: 2, HP: 27, AC: 14, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The grick makes one attack with its tentacles. Traits: Stone Camouflage"}, "grick": {ID: "grick", Name: "Grick", CR: 2, HP: 27, AC: 14, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The grick makes one attack with its tentacles. Traits: Stone Camouflage"},
"griffon": {ID: "griffon", Name: "Griffon", CR: 2, HP: 59, AC: 12, Attack: 4, AttackBonus: 6, Speed: 12, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The griffon makes two attacks: one with its beak and one with its claws. Traits: Keen Sight"}, "griffon": {ID: "griffon", Name: "Griffon", CR: 2, HP: 59, AC: 12, Attack: 4, AttackBonus: 6, Speed: 12, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The griffon makes two attacks: one with its beak and one with its claws. Traits: Keen Sight"},
"hunter_shark": {ID: "hunter_shark", Name: "Hunter Shark", CR: 2, HP: 45, AC: 12, Attack: 4, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Blood Frenzy, Water Breathing"}, "hunter_shark": {ID: "hunter_shark", Name: "Hunter Shark", CR: 2, HP: 45, AC: 12, Attack: 4, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Blood Frenzy, Water Breathing"},
"merrow": {ID: "merrow", Name: "Merrow", CR: 2, HP: 45, AC: 13, Attack: 4, AttackBonus: 6, Speed: 6, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The merrow makes two attacks: one with its bite and one with its claws or harpoon. Traits: Amphibious"}, "merrow": {ID: "merrow", Name: "Merrow", CR: 2, HP: 45, AC: 13, Attack: 4, AttackBonus: 6, Speed: 6, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The merrow makes two attacks: one with its bite and one with its claws or harpoon. Traits: Amphibious"},
"mimic": {ID: "mimic", Name: "Mimic", CR: 2, HP: 58, AC: 12, Attack: 4, AttackBonus: 5, Speed: 6, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Shapechanger, Adhesive (Object Form Only), False Appearance (Object Form Only), Grappler"}, "mimic": {ID: "mimic", Name: "Mimic", CR: 2, HP: 58, AC: 12, Attack: 4, AttackBonus: 5, Speed: 6, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Shapechanger, Adhesive (Object Form Only), False Appearance (Object Form Only), Grappler"},
"minotaur_skeleton": {ID: "minotaur_skeleton", Name: "Minotaur Skeleton", CR: 2, HP: 67, AC: 12, Attack: 4, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge"}, "minotaur_skeleton": {ID: "minotaur_skeleton", Name: "Minotaur Skeleton", CR: 2, HP: 67, AC: 12, Attack: 4, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge"},
"ochre_jelly": {ID: "ochre_jelly", Name: "Ochre Jelly", CR: 2, HP: 45, AC: 10, Attack: 4, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amorphous, Spider Climb"}, "ochre_jelly": {ID: "ochre_jelly", Name: "Ochre Jelly", CR: 2, HP: 45, AC: 10, Attack: 4, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amorphous, Spider Climb"},
"ogre": {ID: "ogre", Name: "Ogre", CR: 2, HP: 59, AC: 11, Attack: 4, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "ogre": {ID: "ogre", Name: "Ogre", CR: 2, HP: 59, AC: 11, Attack: 4, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"ogre_zombie": {ID: "ogre_zombie", Name: "Ogre Zombie", CR: 2, HP: 85, AC: 10, Attack: 4, AttackBonus: 6, Speed: 12, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Undead Fortitude", Phase: "decisive", ProcChance: 0.5, Effect: "survive_at_1"}, Notes: "SRD-tuned baseline — Undead Fortitude (survive_at_1) wired from traits. Traits: Undead Fortitude"}, "ogre_zombie": {ID: "ogre_zombie", Name: "Ogre Zombie", CR: 2, HP: 85, AC: 10, Attack: 4, AttackBonus: 6, Speed: 12, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Undead Fortitude", Phase: "decisive", ProcChance: 0.5, Effect: "survive_at_1"}, Notes: "SRD-tuned baseline — Undead Fortitude (survive_at_1) wired from traits. Traits: Undead Fortitude"},
"pegasus": {ID: "pegasus", Name: "Pegasus", CR: 2, HP: 59, AC: 12, Attack: 4, AttackBonus: 6, Speed: 18, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "pegasus": {ID: "pegasus", Name: "Pegasus", CR: 2, HP: 59, AC: 12, Attack: 4, AttackBonus: 6, Speed: 18, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"plesiosaurus": {ID: "plesiosaurus", Name: "Plesiosaurus", CR: 2, HP: 68, AC: 13, Attack: 4, AttackBonus: 6, Speed: 8, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Hold Breath"}, "plesiosaurus": {ID: "plesiosaurus", Name: "Plesiosaurus", CR: 2, HP: 68, AC: 13, Attack: 4, AttackBonus: 6, Speed: 8, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Hold Breath"},
"polar_bear": {ID: "polar_bear", Name: "Polar Bear", CR: 2, HP: 42, AC: 12, Attack: 4, AttackBonus: 7, Speed: 16, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The bear makes two attacks: one with its bite and one with its claws. Traits: Keen Smell"}, "polar_bear": {ID: "polar_bear", Name: "Polar Bear", CR: 2, HP: 42, AC: 12, Attack: 4, AttackBonus: 7, Speed: 16, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The bear makes two attacks: one with its bite and one with its claws. Traits: Keen Smell"},
"priest": {ID: "priest", Name: "Priest", CR: 2, HP: 27, AC: 13, Attack: 4, AttackBonus: 2, Speed: 10, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Divine Eminence, Spellcasting"}, "priest": {ID: "priest", Name: "Priest", CR: 2, HP: 27, AC: 13, Attack: 4, AttackBonus: 2, Speed: 10, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Divine Eminence, Spellcasting"},
"rhinoceros": {ID: "rhinoceros", Name: "Rhinoceros", CR: 2, HP: 45, AC: 11, Attack: 4, AttackBonus: 7, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge"}, "rhinoceros": {ID: "rhinoceros", Name: "Rhinoceros", CR: 2, HP: 45, AC: 11, Attack: 4, AttackBonus: 7, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge"},
"rug_of_smothering": {ID: "rug_of_smothering", Name: "Rug of Smothering", CR: 2, HP: 33, AC: 12, Attack: 4, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Antimagic Susceptibility, Damage Transfer, False Appearance"}, "rug_of_smothering": {ID: "rug_of_smothering", Name: "Rug of Smothering", CR: 2, HP: 33, AC: 12, Attack: 4, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Antimagic Susceptibility, Damage Transfer, False Appearance"},
"saber_toothed_tiger": {ID: "saber_toothed_tiger", Name: "Saber-Toothed Tiger", CR: 2, HP: 52, AC: 12, Attack: 4, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Keen Smell, Pounce"}, "saber_toothed_tiger": {ID: "saber_toothed_tiger", Name: "Saber-Toothed Tiger", CR: 2, HP: 52, AC: 12, Attack: 4, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Keen Smell, Pounce"},
"sea_hag": {ID: "sea_hag", Name: "Sea Hag", CR: 2, HP: 52, AC: 14, Attack: 4, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Amphibious, Horrific Appearance, Hag Coven, Shared Spellcasting (Coven Only), Hag Eye (Coven Only)"}, "sea_hag": {ID: "sea_hag", Name: "Sea Hag", CR: 2, HP: 52, AC: 14, Attack: 4, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Amphibious, Horrific Appearance, Hag Coven, Shared Spellcasting (Coven Only), Hag Eye (Coven Only)"},
"silver_dragon_wyrmling": {ID: "silver_dragon_wyrmling", Name: "Silver Dragon Wyrmling", CR: 2, HP: 45, AC: 17, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "silver_dragon_wyrmling": {ID: "silver_dragon_wyrmling", Name: "Silver Dragon Wyrmling", CR: 2, HP: 45, AC: 17, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"swarm_of_poisonous_snakes": {ID: "swarm_of_poisonous_snakes", Name: "Swarm of Poisonous Snakes", CR: 2, HP: 36, AC: 14, Attack: 4, AttackBonus: 6, Speed: 12, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"}, "swarm_of_poisonous_snakes": {ID: "swarm_of_poisonous_snakes", Name: "Swarm of Poisonous Snakes", CR: 2, HP: 36, AC: 14, Attack: 4, AttackBonus: 6, Speed: 12, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
"wererat": {ID: "wererat", Name: "Wererat", CR: 2, HP: 33, AC: 12, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Shapechanger, Keen Smell"}, "wererat": {ID: "wererat", Name: "Wererat", CR: 2, HP: 33, AC: 12, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Shapechanger, Keen Smell"},
"white_dragon_wyrmling": {ID: "white_dragon_wyrmling", Name: "White Dragon Wyrmling", CR: 2, HP: 32, AC: 16, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "white_dragon_wyrmling": {ID: "white_dragon_wyrmling", Name: "White Dragon Wyrmling", CR: 2, HP: 32, AC: 16, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"will_o_wisp": {ID: "will_o_wisp", Name: "Will-o'-Wisp", CR: 2, HP: 22, AC: 19, Attack: 4, AttackBonus: 4, Speed: 18, BlockRate: 0.15, XPValue: 450, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Consume Life, Ephemeral, Incorporeal Movement, Variable Illumination"}, "will_o_wisp": {ID: "will_o_wisp", Name: "Will-o'-Wisp", CR: 2, HP: 22, AC: 19, Attack: 4, AttackBonus: 4, Speed: 18, BlockRate: 0.15, XPValue: 450, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Consume Life, Ephemeral, Incorporeal Movement, Variable Illumination"},
"basilisk": {ID: "basilisk", Name: "Basilisk", CR: 3, HP: 52, AC: 12, Attack: 6, AttackBonus: 5, Speed: 8, BlockRate: 0, XPValue: 700, Ability: &MonsterAbility{Name: "Frightful Presence", Phase: "opening", ProcChance: 0.4, Effect: "stun"}, Notes: "SRD-tuned baseline — Frightful Presence (stun) wired from traits. Traits: Petrifying Gaze"}, "basilisk": {ID: "basilisk", Name: "Basilisk", CR: 3, HP: 52, AC: 12, Attack: 6, AttackBonus: 5, Speed: 8, BlockRate: 0, XPValue: 700, Ability: &MonsterAbility{Name: "Frightful Presence", Phase: "opening", ProcChance: 0.4, Effect: "stun"}, Notes: "SRD-tuned baseline — Frightful Presence (stun) wired from traits. Traits: Petrifying Gaze"},
"bearded_devil": {ID: "bearded_devil", Name: "Bearded Devil", CR: 3, HP: 52, AC: 13, Attack: 6, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes two attacks: one with its beard and one with its glaive. Traits: Devil's Sight, Magic Resistance, Steadfast"}, "bearded_devil": {ID: "bearded_devil", Name: "Bearded Devil", CR: 3, HP: 52, AC: 13, Attack: 6, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes two attacks: one with its beard and one with its glaive. Traits: Devil's Sight, Magic Resistance, Steadfast"},
"blue_dragon_wyrmling": {ID: "blue_dragon_wyrmling", Name: "Blue Dragon Wyrmling", CR: 3, HP: 52, AC: 17, Attack: 6, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."}, "blue_dragon_wyrmling": {ID: "blue_dragon_wyrmling", Name: "Blue Dragon Wyrmling", CR: 3, HP: 52, AC: 17, Attack: 6, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
"doppelganger": {ID: "doppelganger", Name: "Doppelganger", CR: 3, HP: 52, AC: 14, Attack: 6, AttackBonus: 6, Speed: 12, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Surprise Attack", Phase: "opening", ProcChance: 0.8, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Surprise Attack (bonus_damage) wired from traits. Multiattack: The doppelganger makes two melee attacks. Traits: Shapechanger, Ambusher, Surprise Attack"}, "doppelganger": {ID: "doppelganger", Name: "Doppelganger", CR: 3, HP: 52, AC: 14, Attack: 6, AttackBonus: 6, Speed: 12, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Surprise Attack", Phase: "opening", ProcChance: 0.8, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Surprise Attack (bonus_damage) wired from traits. Multiattack: The doppelganger makes two melee attacks. Traits: Shapechanger, Ambusher, Surprise Attack"},
"giant_scorpion": {ID: "giant_scorpion", Name: "Giant Scorpion", CR: 3, HP: 52, AC: 15, Attack: 6, AttackBonus: 4, Speed: 16, BlockRate: 0.1, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The scorpion makes three attacks: two with its claws and one with its sting."}, "giant_scorpion": {ID: "giant_scorpion", Name: "Giant Scorpion", CR: 3, HP: 52, AC: 15, Attack: 6, AttackBonus: 4, Speed: 16, BlockRate: 0.1, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The scorpion makes three attacks: two with its claws and one with its sting."},
"gold_dragon_wyrmling": {ID: "gold_dragon_wyrmling", Name: "Gold Dragon Wyrmling", CR: 3, HP: 60, AC: 17, Attack: 6, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious", FireAttacker: true}, "gold_dragon_wyrmling": {ID: "gold_dragon_wyrmling", Name: "Gold Dragon Wyrmling", CR: 3, HP: 60, AC: 17, Attack: 6, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious", FireAttacker: true},
"green_hag": {ID: "green_hag", Name: "Green Hag", CR: 3, HP: 82, AC: 17, Attack: 6, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 700, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Amphibious, Innate Spellcasting, Mimicry, Hag Coven, Shared Spellcasting (Coven Only), Hag Eye (Coven Only)"}, "green_hag": {ID: "green_hag", Name: "Green Hag", CR: 3, HP: 82, AC: 17, Attack: 6, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 700, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Amphibious, Innate Spellcasting, Mimicry, Hag Coven, Shared Spellcasting (Coven Only), Hag Eye (Coven Only)"},
"hell_hound": {ID: "hell_hound", Name: "Hell Hound", CR: 3, HP: 45, AC: 15, Attack: 6, AttackBonus: 0, Speed: 18, BlockRate: 0.1, XPValue: 700, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics", FireAttacker: true}, "hell_hound": {ID: "hell_hound", Name: "Hell Hound", CR: 3, HP: 45, AC: 15, Attack: 6, AttackBonus: 0, Speed: 18, BlockRate: 0.1, XPValue: 700, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics", FireAttacker: true},
"killer_whale": {ID: "killer_whale", Name: "Killer Whale", CR: 3, HP: 90, AC: 12, Attack: 6, AttackBonus: 0, Speed: 18, BlockRate: 0, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, Hold Breath, Keen Hearing"}, "killer_whale": {ID: "killer_whale", Name: "Killer Whale", CR: 3, HP: 90, AC: 12, Attack: 6, AttackBonus: 0, Speed: 18, BlockRate: 0, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, Hold Breath, Keen Hearing"},
"knight": {ID: "knight", Name: "Knight", CR: 3, HP: 52, AC: 18, Attack: 6, AttackBonus: 5, Speed: 12, BlockRate: 0.15, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The knight makes two melee attacks. Traits: Brave"}, "knight": {ID: "knight", Name: "Knight", CR: 3, HP: 52, AC: 18, Attack: 6, AttackBonus: 5, Speed: 12, BlockRate: 0.15, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The knight makes two melee attacks. Traits: Brave"},
"manticore": {ID: "manticore", Name: "Manticore", CR: 3, HP: 68, AC: 14, Attack: 6, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The manticore makes three attacks: one with its bite and two with its claws or three with its tail spikes. Traits: Tail Spike Regrowth"}, "manticore": {ID: "manticore", Name: "Manticore", CR: 3, HP: 68, AC: 14, Attack: 6, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The manticore makes three attacks: one with its bite and two with its claws or three with its tail spikes. Traits: Tail Spike Regrowth"},
"minotaur": {ID: "minotaur", Name: "Minotaur", CR: 3, HP: 76, AC: 14, Attack: 6, AttackBonus: 6, Speed: 16, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Charge, Labyrinthine Recall, Reckless"}, "minotaur": {ID: "minotaur", Name: "Minotaur", CR: 3, HP: 76, AC: 14, Attack: 6, AttackBonus: 6, Speed: 16, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Charge, Labyrinthine Recall, Reckless"},
"mummy": {ID: "mummy", Name: "Mummy", CR: 3, HP: 58, AC: 11, Attack: 6, AttackBonus: 5, Speed: 8, BlockRate: 0, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The mummy can use its Dreadful Glare and makes one attack with its rotting fist."}, "mummy": {ID: "mummy", Name: "Mummy", CR: 3, HP: 58, AC: 11, Attack: 6, AttackBonus: 5, Speed: 8, BlockRate: 0, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The mummy can use its Dreadful Glare and makes one attack with its rotting fist."},
"nightmare": {ID: "nightmare", Name: "Nightmare", CR: 3, HP: 68, AC: 13, Attack: 6, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Confer Fire Resistance, Illumination"}, "nightmare": {ID: "nightmare", Name: "Nightmare", CR: 3, HP: 68, AC: 13, Attack: 6, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Confer Fire Resistance, Illumination"},
"owlbear": {ID: "owlbear", Name: "Owlbear", CR: 3, HP: 59, AC: 13, Attack: 6, AttackBonus: 7, Speed: 16, BlockRate: 0.05, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The owlbear makes two attacks: one with its beak and one with its claws. Traits: Keen Sight and Smell"}, "owlbear": {ID: "owlbear", Name: "Owlbear", CR: 3, HP: 59, AC: 13, Attack: 6, AttackBonus: 7, Speed: 16, BlockRate: 0.05, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The owlbear makes two attacks: one with its beak and one with its claws. Traits: Keen Sight and Smell"},
"phase_spider": {ID: "phase_spider", Name: "Phase Spider", CR: 3, HP: 32, AC: 13, Attack: 6, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Ethereal Jaunt, Spider Climb, Web Walker"}, "phase_spider": {ID: "phase_spider", Name: "Phase Spider", CR: 3, HP: 32, AC: 13, Attack: 6, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Ethereal Jaunt, Spider Climb, Web Walker"},
"veteran": {ID: "veteran", Name: "Veteran", CR: 3, HP: 58, AC: 17, Attack: 6, AttackBonus: 5, Speed: 12, BlockRate: 0.1, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The veteran makes two longsword attacks."}, "veteran": {ID: "veteran", Name: "Veteran", CR: 3, HP: 58, AC: 17, Attack: 6, AttackBonus: 5, Speed: 12, BlockRate: 0.1, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The veteran makes two longsword attacks."},
"werewolf": {ID: "werewolf", Name: "Werewolf", CR: 3, HP: 58, AC: 11, Attack: 6, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 700, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Shapechanger, Keen Hearing and Smell"}, "werewolf": {ID: "werewolf", Name: "Werewolf", CR: 3, HP: 58, AC: 11, Attack: 6, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 700, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Shapechanger, Keen Hearing and Smell"},
"wight": {ID: "wight", Name: "Wight", CR: 3, HP: 45, AC: 14, Attack: 6, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The wight makes two longsword attacks or two longbow attacks. Traits: Sunlight Sensitivity"}, "wight": {ID: "wight", Name: "Wight", CR: 3, HP: 45, AC: 14, Attack: 6, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The wight makes two longsword attacks or two longbow attacks. Traits: Sunlight Sensitivity"},
"winter_wolf": {ID: "winter_wolf", Name: "Winter Wolf", CR: 3, HP: 75, AC: 13, Attack: 6, AttackBonus: 0, Speed: 18, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics, Snow Camouflage"}, "winter_wolf": {ID: "winter_wolf", Name: "Winter Wolf", CR: 3, HP: 75, AC: 13, Attack: 6, AttackBonus: 0, Speed: 18, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics, Snow Camouflage"},
"black_pudding": {ID: "black_pudding", Name: "Black Pudding", CR: 4, HP: 85, AC: 10, Attack: 7, AttackBonus: 5, Speed: 8, BlockRate: 0, XPValue: 1100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amorphous, Corrosive Form, Spider Climb"}, "black_pudding": {ID: "black_pudding", Name: "Black Pudding", CR: 4, HP: 85, AC: 10, Attack: 7, AttackBonus: 5, Speed: 8, BlockRate: 0, XPValue: 1100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amorphous, Corrosive Form, Spider Climb"},
"chuul": {ID: "chuul", Name: "Chuul", CR: 4, HP: 93, AC: 16, Attack: 7, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 1100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The chuul makes two pincer attacks. Traits: Amphibious, Sense Magic"}, "chuul": {ID: "chuul", Name: "Chuul", CR: 4, HP: 93, AC: 16, Attack: 7, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 1100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The chuul makes two pincer attacks. Traits: Amphibious, Sense Magic"},
"couatl": {ID: "couatl", Name: "Couatl", CR: 4, HP: 97, AC: 19, Attack: 7, AttackBonus: 6, Speed: 12, BlockRate: 0.15, XPValue: 1100, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Innate Spellcasting, Magic Weapons, Shielded Mind"}, "couatl": {ID: "couatl", Name: "Couatl", CR: 4, HP: 97, AC: 19, Attack: 7, AttackBonus: 6, Speed: 12, BlockRate: 0.15, XPValue: 1100, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Innate Spellcasting, Magic Weapons, Shielded Mind"},
"elephant": {ID: "elephant", Name: "Elephant", CR: 4, HP: 76, AC: 12, Attack: 7, AttackBonus: 8, Speed: 16, BlockRate: 0, XPValue: 1100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"}, "elephant": {ID: "elephant", Name: "Elephant", CR: 4, HP: 76, AC: 12, Attack: 7, AttackBonus: 8, Speed: 16, BlockRate: 0, XPValue: 1100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"},
"ettin": {ID: "ettin", Name: "Ettin", CR: 4, HP: 85, AC: 12, Attack: 7, AttackBonus: 7, Speed: 16, BlockRate: 0, XPValue: 1100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The ettin makes two attacks: one with its battleaxe and one with its morningstar. Traits: Two Heads, Wakeful"}, "ettin": {ID: "ettin", Name: "Ettin", CR: 4, HP: 85, AC: 12, Attack: 7, AttackBonus: 7, Speed: 16, BlockRate: 0, XPValue: 1100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The ettin makes two attacks: one with its battleaxe and one with its morningstar. Traits: Two Heads, Wakeful"},
"ghost": {ID: "ghost", Name: "Ghost", CR: 4, HP: 45, AC: 11, Attack: 7, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 1100, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Ethereal Sight, Incorporeal Movement"}, "ghost": {ID: "ghost", Name: "Ghost", CR: 4, HP: 45, AC: 11, Attack: 7, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 1100, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Ethereal Sight, Incorporeal Movement"},
"lamia": {ID: "lamia", Name: "Lamia", CR: 4, HP: 97, AC: 13, Attack: 7, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 1100, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The lamia makes two attacks: one with its claws and one with its dagger or Intoxicating Touch. Traits: Innate Spellcasting"}, "lamia": {ID: "lamia", Name: "Lamia", CR: 4, HP: 97, AC: 13, Attack: 7, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 1100, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The lamia makes two attacks: one with its claws and one with its dagger or Intoxicating Touch. Traits: Innate Spellcasting"},
"red_dragon_wyrmling": {ID: "red_dragon_wyrmling", Name: "Red Dragon Wyrmling", CR: 4, HP: 75, AC: 17, Attack: 7, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 1100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat).", FireAttacker: true}, "red_dragon_wyrmling": {ID: "red_dragon_wyrmling", Name: "Red Dragon Wyrmling", CR: 4, HP: 75, AC: 17, Attack: 7, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 1100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat).", FireAttacker: true},
"succubusincubus": {ID: "succubusincubus", Name: "Succubus/Incubus", CR: 4, HP: 66, AC: 15, Attack: 7, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 1100, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Telepathic Bond, Shapechanger"}, "succubusincubus": {ID: "succubusincubus", Name: "Succubus/Incubus", CR: 4, HP: 66, AC: 15, Attack: 7, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 1100, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Telepathic Bond, Shapechanger"},
"wereboar": {ID: "wereboar", Name: "Wereboar", CR: 4, HP: 78, AC: 10, Attack: 7, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 1100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Shapechanger, Charge (Boar or Hybrid Form Only), Relentless (Recharges after a Short or Long Rest)"}, "wereboar": {ID: "wereboar", Name: "Wereboar", CR: 4, HP: 78, AC: 10, Attack: 7, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 1100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Shapechanger, Charge (Boar or Hybrid Form Only), Relentless (Recharges after a Short or Long Rest)"},
"weretiger": {ID: "weretiger", Name: "Weretiger", CR: 4, HP: 120, AC: 12, Attack: 7, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 1100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Shapechanger, Keen Hearing and Smell, Pounce (Tiger or Hybrid Form Only)"}, "weretiger": {ID: "weretiger", Name: "Weretiger", CR: 4, HP: 120, AC: 12, Attack: 7, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 1100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Shapechanger, Keen Hearing and Smell, Pounce (Tiger or Hybrid Form Only)"},
"air_elemental": {ID: "air_elemental", Name: "Air Elemental", CR: 5, HP: 90, AC: 15, Attack: 8, AttackBonus: 8, Speed: 18, BlockRate: 0.1, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The elemental makes two slam attacks. Traits: Air Form"}, "air_elemental": {ID: "air_elemental", Name: "Air Elemental", CR: 5, HP: 90, AC: 15, Attack: 8, AttackBonus: 8, Speed: 18, BlockRate: 0.1, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The elemental makes two slam attacks. Traits: Air Form"},
"barbed_devil": {ID: "barbed_devil", Name: "Barbed Devil", CR: 5, HP: 110, AC: 15, Attack: 8, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes three melee attacks: one with its tail and two with its claws. Traits: Barbed Hide, Devil's Sight, Magic Resistance"}, "barbed_devil": {ID: "barbed_devil", Name: "Barbed Devil", CR: 5, HP: 110, AC: 15, Attack: 8, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes three melee attacks: one with its tail and two with its claws. Traits: Barbed Hide, Devil's Sight, Magic Resistance"},
"bulette": {ID: "bulette", Name: "Bulette", CR: 5, HP: 94, AC: 17, Attack: 8, AttackBonus: 7, Speed: 16, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Standing Leap"}, "bulette": {ID: "bulette", Name: "Bulette", CR: 5, HP: 94, AC: 17, Attack: 8, AttackBonus: 7, Speed: 16, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Standing Leap"},
"earth_elemental": {ID: "earth_elemental", Name: "Earth Elemental", CR: 5, HP: 126, AC: 17, Attack: 8, AttackBonus: 8, Speed: 12, BlockRate: 0.1, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The elemental makes two slam attacks. Traits: Earth Glide, Siege Monster"}, "earth_elemental": {ID: "earth_elemental", Name: "Earth Elemental", CR: 5, HP: 126, AC: 17, Attack: 8, AttackBonus: 8, Speed: 12, BlockRate: 0.1, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The elemental makes two slam attacks. Traits: Earth Glide, Siege Monster"},
"fire_elemental": {ID: "fire_elemental", Name: "Fire Elemental", CR: 5, HP: 102, AC: 13, Attack: 8, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 1800, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Multiattack: The elemental makes two touch attacks. Traits: Fire Form, Illumination, Water Susceptibility", FireAttacker: true}, "fire_elemental": {ID: "fire_elemental", Name: "Fire Elemental", CR: 5, HP: 102, AC: 13, Attack: 8, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 1800, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Multiattack: The elemental makes two touch attacks. Traits: Fire Form, Illumination, Water Susceptibility", FireAttacker: true},
"flesh_golem": {ID: "flesh_golem", Name: "Flesh Golem", CR: 5, HP: 93, AC: 10, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0, XPValue: 1800, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The golem makes two slam attacks. Traits: Berserk, Aversion of Fire, Immutable Form, Lightning Absorption, Magic Resistance, Magic Weapons"}, "flesh_golem": {ID: "flesh_golem", Name: "Flesh Golem", CR: 5, HP: 93, AC: 10, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0, XPValue: 1800, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The golem makes two slam attacks. Traits: Berserk, Aversion of Fire, Immutable Form, Lightning Absorption, Magic Resistance, Magic Weapons"},
"giant_crocodile": {ID: "giant_crocodile", Name: "Giant Crocodile", CR: 5, HP: 85, AC: 14, Attack: 8, AttackBonus: 8, Speed: 12, BlockRate: 0.05, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The crocodile makes two attacks: one with its bite and one with its tail. Traits: Hold Breath"}, "giant_crocodile": {ID: "giant_crocodile", Name: "Giant Crocodile", CR: 5, HP: 85, AC: 14, Attack: 8, AttackBonus: 8, Speed: 12, BlockRate: 0.05, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The crocodile makes two attacks: one with its bite and one with its tail. Traits: Hold Breath"},
"giant_shark": {ID: "giant_shark", Name: "Giant Shark", CR: 5, HP: 126, AC: 13, Attack: 8, AttackBonus: 9, Speed: 18, BlockRate: 0.05, XPValue: 1800, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Blood Frenzy, Water Breathing"}, "giant_shark": {ID: "giant_shark", Name: "Giant Shark", CR: 5, HP: 126, AC: 13, Attack: 8, AttackBonus: 9, Speed: 18, BlockRate: 0.05, XPValue: 1800, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Blood Frenzy, Water Breathing"},
"gladiator": {ID: "gladiator", Name: "Gladiator", CR: 5, HP: 112, AC: 16, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Multiattack: The gladiator makes three melee attacks or two ranged attacks. Traits: Brave, Brute"}, "gladiator": {ID: "gladiator", Name: "Gladiator", CR: 5, HP: 112, AC: 16, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Multiattack: The gladiator makes three melee attacks or two ranged attacks. Traits: Brave, Brute"},
"gorgon": {ID: "gorgon", Name: "Gorgon", CR: 5, HP: 114, AC: 19, Attack: 8, AttackBonus: 8, Speed: 16, BlockRate: 0.15, XPValue: 1800, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"}, "gorgon": {ID: "gorgon", Name: "Gorgon", CR: 5, HP: 114, AC: 19, Attack: 8, AttackBonus: 8, Speed: 16, BlockRate: 0.15, XPValue: 1800, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"},
"half_red_dragon_veteran": {ID: "half_red_dragon_veteran", Name: "Half-Red Dragon Veteran", CR: 5, HP: 65, AC: 18, Attack: 8, AttackBonus: 0, Speed: 12, BlockRate: 0.15, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The veteran makes two longsword attacks.", FireAttacker: true}, "half_red_dragon_veteran": {ID: "half_red_dragon_veteran", Name: "Half-Red Dragon Veteran", CR: 5, HP: 65, AC: 18, Attack: 8, AttackBonus: 0, Speed: 12, BlockRate: 0.15, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The veteran makes two longsword attacks.", FireAttacker: true},
"hill_giant": {ID: "hill_giant", Name: "Hill Giant", CR: 5, HP: 105, AC: 13, Attack: 8, AttackBonus: 8, Speed: 16, BlockRate: 0.05, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The giant makes two greatclub attacks."}, "hill_giant": {ID: "hill_giant", Name: "Hill Giant", CR: 5, HP: 105, AC: 13, Attack: 8, AttackBonus: 8, Speed: 16, BlockRate: 0.05, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The giant makes two greatclub attacks."},
"night_hag": {ID: "night_hag", Name: "Night Hag", CR: 5, HP: 112, AC: 17, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Innate Spellcasting, Magic Resistance, Night Hag Items, Hag Coven, Shared Spellcasting (Coven Only), Hag Eye (Coven Only)"}, "night_hag": {ID: "night_hag", Name: "Night Hag", CR: 5, HP: 112, AC: 17, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Innate Spellcasting, Magic Resistance, Night Hag Items, Hag Coven, Shared Spellcasting (Coven Only), Hag Eye (Coven Only)"},
"otyugh": {ID: "otyugh", Name: "Otyugh", CR: 5, HP: 114, AC: 14, Attack: 8, AttackBonus: 6, Speed: 12, BlockRate: 0.05, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The otyugh makes three attacks: one with its bite and two with its tentacles. Traits: Limited Telepathy"}, "otyugh": {ID: "otyugh", Name: "Otyugh", CR: 5, HP: 114, AC: 14, Attack: 8, AttackBonus: 6, Speed: 12, BlockRate: 0.05, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The otyugh makes three attacks: one with its bite and two with its tentacles. Traits: Limited Telepathy"},
"roper": {ID: "roper", Name: "Roper", CR: 5, HP: 93, AC: 20, Attack: 8, AttackBonus: 7, Speed: 6, BlockRate: 0.15, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The roper makes four attacks with its tendrils, uses Reel, and makes one attack with its bite. Traits: False Appearance, Grasping Tendrils, Spider Climb"}, "roper": {ID: "roper", Name: "Roper", CR: 5, HP: 93, AC: 20, Attack: 8, AttackBonus: 7, Speed: 6, BlockRate: 0.15, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The roper makes four attacks with its tendrils, uses Reel, and makes one attack with its bite. Traits: False Appearance, Grasping Tendrils, Spider Climb"},
"salamander": {ID: "salamander", Name: "Salamander", CR: 5, HP: 90, AC: 15, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Multiattack: The salamander makes two attacks: one with its spear and one with its tail. Traits: Heated Body, Heated Weapons"}, "salamander": {ID: "salamander", Name: "Salamander", CR: 5, HP: 90, AC: 15, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Multiattack: The salamander makes two attacks: one with its spear and one with its tail. Traits: Heated Body, Heated Weapons"},
"shambling_mound": {ID: "shambling_mound", Name: "Shambling Mound", CR: 5, HP: 136, AC: 15, Attack: 8, AttackBonus: 7, Speed: 8, BlockRate: 0.1, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The shambling mound makes two slam attacks. Traits: Lightning Absorption"}, "shambling_mound": {ID: "shambling_mound", Name: "Shambling Mound", CR: 5, HP: 136, AC: 15, Attack: 8, AttackBonus: 7, Speed: 8, BlockRate: 0.1, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The shambling mound makes two slam attacks. Traits: Lightning Absorption"},
"triceratops": {ID: "triceratops", Name: "Triceratops", CR: 5, HP: 95, AC: 13, Attack: 8, AttackBonus: 9, Speed: 18, BlockRate: 0.05, XPValue: 1800, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"}, "triceratops": {ID: "triceratops", Name: "Triceratops", CR: 5, HP: 95, AC: 13, Attack: 8, AttackBonus: 9, Speed: 18, BlockRate: 0.05, XPValue: 1800, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"},
"troll": {ID: "troll", Name: "Troll", CR: 5, HP: 84, AC: 15, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Multiattack: The troll makes three attacks: one with its bite and two with its claws. Traits: Keen Smell, Regeneration, Variant: Loathsome Limbs"}, "troll": {ID: "troll", Name: "Troll", CR: 5, HP: 84, AC: 15, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Multiattack: The troll makes three attacks: one with its bite and two with its claws. Traits: Keen Smell, Regeneration, Variant: Loathsome Limbs"},
"unicorn": {ID: "unicorn", Name: "Unicorn", CR: 5, HP: 67, AC: 12, Attack: 8, AttackBonus: 7, Speed: 18, BlockRate: 0, XPValue: 1800, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The unicorn makes two attacks: one with its hooves and one with its horn. Traits: Charge, Innate Spellcasting, Magic Resistance, Magic Weapons"}, "unicorn": {ID: "unicorn", Name: "Unicorn", CR: 5, HP: 67, AC: 12, Attack: 8, AttackBonus: 7, Speed: 18, BlockRate: 0, XPValue: 1800, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The unicorn makes two attacks: one with its hooves and one with its horn. Traits: Charge, Innate Spellcasting, Magic Resistance, Magic Weapons"},
"vampire_spawn": {ID: "vampire_spawn", Name: "Vampire Spawn", CR: 5, HP: 82, AC: 15, Attack: 8, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Multiattack: The vampire makes two attacks, only one of which can be a bite attack. Traits: Regeneration, Spider Climb, Vampire Weaknesses"}, "vampire_spawn": {ID: "vampire_spawn", Name: "Vampire Spawn", CR: 5, HP: 82, AC: 15, Attack: 8, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Multiattack: The vampire makes two attacks, only one of which can be a bite attack. Traits: Regeneration, Spider Climb, Vampire Weaknesses"},
"water_elemental": {ID: "water_elemental", Name: "Water Elemental", CR: 5, HP: 114, AC: 14, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.05, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The elemental makes two slam attacks. Traits: Water Form, Freeze"}, "water_elemental": {ID: "water_elemental", Name: "Water Elemental", CR: 5, HP: 114, AC: 14, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.05, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The elemental makes two slam attacks. Traits: Water Form, Freeze"},
"werebear": {ID: "werebear", Name: "Werebear", CR: 5, HP: 135, AC: 10, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0, XPValue: 1800, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Multiattack: In bear form, the werebear makes two claw attacks. Traits: Shapechanger, Keen Smell"}, "werebear": {ID: "werebear", Name: "Werebear", CR: 5, HP: 135, AC: 10, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0, XPValue: 1800, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Multiattack: In bear form, the werebear makes two claw attacks. Traits: Shapechanger, Keen Smell"},
"wraith": {ID: "wraith", Name: "Wraith", CR: 5, HP: 67, AC: 13, Attack: 8, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 1800, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Incorporeal Movement, Sunlight Sensitivity"}, "wraith": {ID: "wraith", Name: "Wraith", CR: 5, HP: 67, AC: 13, Attack: 8, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 1800, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Incorporeal Movement, Sunlight Sensitivity"},
"xorn": {ID: "xorn", Name: "Xorn", CR: 5, HP: 73, AC: 19, Attack: 8, AttackBonus: 6, Speed: 8, BlockRate: 0.15, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The xorn makes three claw attacks and one bite attack. Traits: Earth Glide, Stone Camouflage, Treasure Sense"}, "xorn": {ID: "xorn", Name: "Xorn", CR: 5, HP: 73, AC: 19, Attack: 8, AttackBonus: 6, Speed: 8, BlockRate: 0.15, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The xorn makes three claw attacks and one bite attack. Traits: Earth Glide, Stone Camouflage, Treasure Sense"},
"chimera": {ID: "chimera", Name: "Chimera", CR: 6, HP: 114, AC: 14, Attack: 10, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The chimera makes three attacks: one with its bite, one with its horns, and one with its claws.", FireAttacker: true}, "chimera": {ID: "chimera", Name: "Chimera", CR: 6, HP: 114, AC: 14, Attack: 10, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The chimera makes three attacks: one with its bite, one with its horns, and one with its claws.", FireAttacker: true},
"drider": {ID: "drider", Name: "Drider", CR: 6, HP: 123, AC: 19, Attack: 10, AttackBonus: 6, Speed: 12, BlockRate: 0.15, XPValue: 2300, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The drider makes three attacks, either with its longsword or its longbow. Traits: Fey Ancestry, Innate Spellcasting, Spider Climb, Sunlight Sensitivity, Web Walker"}, "drider": {ID: "drider", Name: "Drider", CR: 6, HP: 123, AC: 19, Attack: 10, AttackBonus: 6, Speed: 12, BlockRate: 0.15, XPValue: 2300, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The drider makes three attacks, either with its longsword or its longbow. Traits: Fey Ancestry, Innate Spellcasting, Spider Climb, Sunlight Sensitivity, Web Walker"},
"invisible_stalker": {ID: "invisible_stalker", Name: "Invisible Stalker", CR: 6, HP: 104, AC: 14, Attack: 10, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The stalker makes two slam attacks. Traits: Invisibility, Faultless Tracker"}, "invisible_stalker": {ID: "invisible_stalker", Name: "Invisible Stalker", CR: 6, HP: 104, AC: 14, Attack: 10, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The stalker makes two slam attacks. Traits: Invisibility, Faultless Tracker"},
"mage": {ID: "mage", Name: "Mage", CR: 6, HP: 40, AC: 12, Attack: 10, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 2300, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Spellcasting"}, "mage": {ID: "mage", Name: "Mage", CR: 6, HP: 40, AC: 12, Attack: 10, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 2300, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Spellcasting"},
"mammoth": {ID: "mammoth", Name: "Mammoth", CR: 6, HP: 126, AC: 13, Attack: 10, AttackBonus: 10, Speed: 16, BlockRate: 0.05, XPValue: 2300, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"}, "mammoth": {ID: "mammoth", Name: "Mammoth", CR: 6, HP: 126, AC: 13, Attack: 10, AttackBonus: 10, Speed: 16, BlockRate: 0.05, XPValue: 2300, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"},
"medusa": {ID: "medusa", Name: "Medusa", CR: 6, HP: 127, AC: 15, Attack: 10, AttackBonus: 5, Speed: 12, BlockRate: 0.1, XPValue: 2300, Ability: &MonsterAbility{Name: "Frightful Presence", Phase: "opening", ProcChance: 0.4, Effect: "stun"}, Notes: "SRD-tuned baseline — Frightful Presence (stun) wired from traits. Multiattack: The medusa makes either three melee attacks - one with its snake hair and two with its shortsword - or two ranged attacks with its longbow. Traits: Petrifying Gaze"}, "medusa": {ID: "medusa", Name: "Medusa", CR: 6, HP: 127, AC: 15, Attack: 10, AttackBonus: 5, Speed: 12, BlockRate: 0.1, XPValue: 2300, Ability: &MonsterAbility{Name: "Frightful Presence", Phase: "opening", ProcChance: 0.4, Effect: "stun"}, Notes: "SRD-tuned baseline — Frightful Presence (stun) wired from traits. Multiattack: The medusa makes either three melee attacks - one with its snake hair and two with its shortsword - or two ranged attacks with its longbow. Traits: Petrifying Gaze"},
"vrock": {ID: "vrock", Name: "Vrock", CR: 6, HP: 104, AC: 15, Attack: 10, AttackBonus: 6, Speed: 16, BlockRate: 0.1, XPValue: 2300, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The vrock makes two attacks: one with its beak and one with its talons. Traits: Magic Resistance"}, "vrock": {ID: "vrock", Name: "Vrock", CR: 6, HP: 104, AC: 15, Attack: 10, AttackBonus: 6, Speed: 16, BlockRate: 0.1, XPValue: 2300, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The vrock makes two attacks: one with its beak and one with its talons. Traits: Magic Resistance"},
"wyvern": {ID: "wyvern", Name: "Wyvern", CR: 6, HP: 110, AC: 13, Attack: 10, AttackBonus: 7, Speed: 8, BlockRate: 0.05, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The wyvern makes two attacks: one with its bite and one with its stinger."}, "wyvern": {ID: "wyvern", Name: "Wyvern", CR: 6, HP: 110, AC: 13, Attack: 10, AttackBonus: 7, Speed: 8, BlockRate: 0.05, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The wyvern makes two attacks: one with its bite and one with its stinger."},
"young_brass_dragon": {ID: "young_brass_dragon", Name: "Young Brass Dragon", CR: 6, HP: 110, AC: 17, Attack: 10, AttackBonus: 0, Speed: 16, BlockRate: 0.1, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws.", FireAttacker: true}, "young_brass_dragon": {ID: "young_brass_dragon", Name: "Young Brass Dragon", CR: 6, HP: 110, AC: 17, Attack: 10, AttackBonus: 0, Speed: 16, BlockRate: 0.1, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws.", FireAttacker: true},
"young_white_dragon": {ID: "young_white_dragon", Name: "Young White Dragon", CR: 6, HP: 133, AC: 17, Attack: 10, AttackBonus: 0, Speed: 16, BlockRate: 0.1, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Ice Walk"}, "young_white_dragon": {ID: "young_white_dragon", Name: "Young White Dragon", CR: 6, HP: 133, AC: 17, Attack: 10, AttackBonus: 0, Speed: 16, BlockRate: 0.1, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Ice Walk"},
"giant_ape": {ID: "giant_ape", Name: "Giant Ape", CR: 7, HP: 157, AC: 12, Attack: 12, AttackBonus: 9, Speed: 16, BlockRate: 0, XPValue: 2900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The ape makes two fist attacks."}, "giant_ape": {ID: "giant_ape", Name: "Giant Ape", CR: 7, HP: 157, AC: 12, Attack: 12, AttackBonus: 9, Speed: 16, BlockRate: 0, XPValue: 2900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The ape makes two fist attacks."},
"oni": {ID: "oni", Name: "Oni", CR: 7, HP: 110, AC: 16, Attack: 12, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 2900, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The oni makes two attacks, either with its claws or its glaive. Traits: Innate Spellcasting, Magic Weapons, Regeneration"}, "oni": {ID: "oni", Name: "Oni", CR: 7, HP: 110, AC: 16, Attack: 12, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 2900, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The oni makes two attacks, either with its claws or its glaive. Traits: Innate Spellcasting, Magic Weapons, Regeneration"},
"shield_guardian": {ID: "shield_guardian", Name: "Shield Guardian", CR: 7, HP: 142, AC: 17, Attack: 12, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 2900, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Multiattack: The guardian makes two fist attacks. Traits: Bound, Regeneration, Spell Storing"}, "shield_guardian": {ID: "shield_guardian", Name: "Shield Guardian", CR: 7, HP: 142, AC: 17, Attack: 12, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 2900, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Multiattack: The guardian makes two fist attacks. Traits: Bound, Regeneration, Spell Storing"},
"stone_giant": {ID: "stone_giant", Name: "Stone Giant", CR: 7, HP: 126, AC: 17, Attack: 12, AttackBonus: 9, Speed: 16, BlockRate: 0.1, XPValue: 2900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The giant makes two greatclub attacks. Traits: Stone Camouflage"}, "stone_giant": {ID: "stone_giant", Name: "Stone Giant", CR: 7, HP: 126, AC: 17, Attack: 12, AttackBonus: 9, Speed: 16, BlockRate: 0.1, XPValue: 2900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The giant makes two greatclub attacks. Traits: Stone Camouflage"},
"young_black_dragon": {ID: "young_black_dragon", Name: "Young Black Dragon", CR: 7, HP: 127, AC: 18, Attack: 12, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 2900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Amphibious"}, "young_black_dragon": {ID: "young_black_dragon", Name: "Young Black Dragon", CR: 7, HP: 127, AC: 18, Attack: 12, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 2900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Amphibious"},
"young_copper_dragon": {ID: "young_copper_dragon", Name: "Young Copper Dragon", CR: 7, HP: 119, AC: 17, Attack: 12, AttackBonus: 0, Speed: 16, BlockRate: 0.1, XPValue: 2900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws."}, "young_copper_dragon": {ID: "young_copper_dragon", Name: "Young Copper Dragon", CR: 7, HP: 119, AC: 17, Attack: 12, AttackBonus: 0, Speed: 16, BlockRate: 0.1, XPValue: 2900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws."},
"assassin": {ID: "assassin", Name: "Assassin", CR: 8, HP: 78, AC: 15, Attack: 13, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 3900, Ability: &MonsterAbility{Name: "Surprise Attack", Phase: "opening", ProcChance: 0.8, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Surprise Attack (bonus_damage) wired from traits. Multiattack: The assassin makes two shortsword attacks. Traits: Assassinate, Evasion, Sneak Attack (1/Turn)"}, "assassin": {ID: "assassin", Name: "Assassin", CR: 8, HP: 78, AC: 15, Attack: 13, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 3900, Ability: &MonsterAbility{Name: "Surprise Attack", Phase: "opening", ProcChance: 0.8, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Surprise Attack (bonus_damage) wired from traits. Multiattack: The assassin makes two shortsword attacks. Traits: Assassinate, Evasion, Sneak Attack (1/Turn)"},
"cloaker": {ID: "cloaker", Name: "Cloaker", CR: 8, HP: 78, AC: 14, Attack: 13, AttackBonus: 6, Speed: 6, BlockRate: 0.05, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The cloaker makes two attacks: one with its bite and one with its tail. Traits: Damage Transfer, False Appearance, Light Sensitivity"}, "cloaker": {ID: "cloaker", Name: "Cloaker", CR: 8, HP: 78, AC: 14, Attack: 13, AttackBonus: 6, Speed: 6, BlockRate: 0.05, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The cloaker makes two attacks: one with its bite and one with its tail. Traits: Damage Transfer, False Appearance, Light Sensitivity"},
"frost_giant": {ID: "frost_giant", Name: "Frost Giant", CR: 8, HP: 138, AC: 15, Attack: 13, AttackBonus: 9, Speed: 16, BlockRate: 0.1, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The giant makes two greataxe attacks."}, "frost_giant": {ID: "frost_giant", Name: "Frost Giant", CR: 8, HP: 138, AC: 15, Attack: 13, AttackBonus: 9, Speed: 16, BlockRate: 0.1, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The giant makes two greataxe attacks."},
"hezrou": {ID: "hezrou", Name: "Hezrou", CR: 8, HP: 136, AC: 16, Attack: 13, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 3900, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The hezrou makes three attacks: one with its bite and two with its claws. Traits: Magic Resistance, Stench"}, "hezrou": {ID: "hezrou", Name: "Hezrou", CR: 8, HP: 136, AC: 16, Attack: 13, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 3900, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The hezrou makes three attacks: one with its bite and two with its claws. Traits: Magic Resistance, Stench"},
"hydra": {ID: "hydra", Name: "Hydra", CR: 8, HP: 172, AC: 15, Attack: 13, AttackBonus: 8, Speed: 12, BlockRate: 0.1, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The hydra makes as many bite attacks as it has heads. Traits: Hold Breath, Multiple Heads, Reactive Heads, Wakeful"}, "hydra": {ID: "hydra", Name: "Hydra", CR: 8, HP: 172, AC: 15, Attack: 13, AttackBonus: 8, Speed: 12, BlockRate: 0.1, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The hydra makes as many bite attacks as it has heads. Traits: Hold Breath, Multiple Heads, Reactive Heads, Wakeful"},
"spirit_naga": {ID: "spirit_naga", Name: "Spirit Naga", CR: 8, HP: 75, AC: 15, Attack: 13, AttackBonus: 7, Speed: 16, BlockRate: 0.1, XPValue: 3900, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Rejuvenation, Spellcasting"}, "spirit_naga": {ID: "spirit_naga", Name: "Spirit Naga", CR: 8, HP: 75, AC: 15, Attack: 13, AttackBonus: 7, Speed: 16, BlockRate: 0.1, XPValue: 3900, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Rejuvenation, Spellcasting"},
"tyrannosaurus_rex": {ID: "tyrannosaurus_rex", Name: "Tyrannosaurus Rex", CR: 8, HP: 136, AC: 13, Attack: 13, AttackBonus: 10, Speed: 18, BlockRate: 0.05, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The tyrannosaurus makes two attacks: one with its bite and one with its tail."}, "tyrannosaurus_rex": {ID: "tyrannosaurus_rex", Name: "Tyrannosaurus Rex", CR: 8, HP: 136, AC: 13, Attack: 13, AttackBonus: 10, Speed: 18, BlockRate: 0.05, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The tyrannosaurus makes two attacks: one with its bite and one with its tail."},
"young_bronze_dragon": {ID: "young_bronze_dragon", Name: "Young Bronze Dragon", CR: 8, HP: 142, AC: 18, Attack: 13, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Amphibious"}, "young_bronze_dragon": {ID: "young_bronze_dragon", Name: "Young Bronze Dragon", CR: 8, HP: 142, AC: 18, Attack: 13, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Amphibious"},
"young_green_dragon": {ID: "young_green_dragon", Name: "Young Green Dragon", CR: 8, HP: 136, AC: 18, Attack: 13, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Amphibious"}, "young_green_dragon": {ID: "young_green_dragon", Name: "Young Green Dragon", CR: 8, HP: 136, AC: 18, Attack: 13, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Amphibious"},
"bone_devil": {ID: "bone_devil", Name: "Bone Devil", CR: 9, HP: 142, AC: 19, Attack: 14, AttackBonus: 8, Speed: 16, BlockRate: 0.15, XPValue: 5000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes three attacks: two with its claws and one with its sting. Traits: Devil's Sight, Magic Resistance"}, "bone_devil": {ID: "bone_devil", Name: "Bone Devil", CR: 9, HP: 142, AC: 19, Attack: 14, AttackBonus: 8, Speed: 16, BlockRate: 0.15, XPValue: 5000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes three attacks: two with its claws and one with its sting. Traits: Devil's Sight, Magic Resistance"},
"clay_golem": {ID: "clay_golem", Name: "Clay Golem", CR: 9, HP: 133, AC: 14, Attack: 14, AttackBonus: 8, Speed: 8, BlockRate: 0.05, XPValue: 5000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The golem makes two slam attacks. Traits: Acid Absorption, Berserk, Immutable Form, Magic Resistance, Magic Weapons"}, "clay_golem": {ID: "clay_golem", Name: "Clay Golem", CR: 9, HP: 133, AC: 14, Attack: 14, AttackBonus: 8, Speed: 8, BlockRate: 0.05, XPValue: 5000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The golem makes two slam attacks. Traits: Acid Absorption, Berserk, Immutable Form, Magic Resistance, Magic Weapons"},
"cloud_giant": {ID: "cloud_giant", Name: "Cloud Giant", CR: 9, HP: 200, AC: 14, Attack: 14, AttackBonus: 12, Speed: 16, BlockRate: 0.05, XPValue: 5000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The giant makes two morningstar attacks. Traits: Keen Smell, Innate Spellcasting"}, "cloud_giant": {ID: "cloud_giant", Name: "Cloud Giant", CR: 9, HP: 200, AC: 14, Attack: 14, AttackBonus: 12, Speed: 16, BlockRate: 0.05, XPValue: 5000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The giant makes two morningstar attacks. Traits: Keen Smell, Innate Spellcasting"},
"fire_giant": {ID: "fire_giant", Name: "Fire Giant", CR: 9, HP: 162, AC: 18, Attack: 14, AttackBonus: 11, Speed: 12, BlockRate: 0.15, XPValue: 5000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The giant makes two greatsword attacks."}, "fire_giant": {ID: "fire_giant", Name: "Fire Giant", CR: 9, HP: 162, AC: 18, Attack: 14, AttackBonus: 11, Speed: 12, BlockRate: 0.15, XPValue: 5000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The giant makes two greatsword attacks."},
"glabrezu": {ID: "glabrezu", Name: "Glabrezu", CR: 9, HP: 157, AC: 17, Attack: 14, AttackBonus: 9, Speed: 16, BlockRate: 0.1, XPValue: 5000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The glabrezu makes four attacks: two with its pincers and two with its fists. Traits: Innate Spellcasting, Magic Resistance"}, "glabrezu": {ID: "glabrezu", Name: "Glabrezu", CR: 9, HP: 157, AC: 17, Attack: 14, AttackBonus: 9, Speed: 16, BlockRate: 0.1, XPValue: 5000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The glabrezu makes four attacks: two with its pincers and two with its fists. Traits: Innate Spellcasting, Magic Resistance"},
"treant": {ID: "treant", Name: "Treant", CR: 9, HP: 138, AC: 16, Attack: 14, AttackBonus: 10, Speed: 12, BlockRate: 0.1, XPValue: 5000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The treant makes two slam attacks. Traits: False Appearance, Siege Monster"}, "treant": {ID: "treant", Name: "Treant", CR: 9, HP: 138, AC: 16, Attack: 14, AttackBonus: 10, Speed: 12, BlockRate: 0.1, XPValue: 5000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The treant makes two slam attacks. Traits: False Appearance, Siege Monster"},
"young_blue_dragon": {ID: "young_blue_dragon", Name: "Young Blue Dragon", CR: 9, HP: 152, AC: 18, Attack: 14, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 5000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws."}, "young_blue_dragon": {ID: "young_blue_dragon", Name: "Young Blue Dragon", CR: 9, HP: 152, AC: 18, Attack: 14, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 5000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws."},
"young_silver_dragon": {ID: "young_silver_dragon", Name: "Young Silver Dragon", CR: 9, HP: 168, AC: 18, Attack: 14, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 5000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws."}, "young_silver_dragon": {ID: "young_silver_dragon", Name: "Young Silver Dragon", CR: 9, HP: 168, AC: 18, Attack: 14, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 5000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws."},
"aboleth": {ID: "aboleth", Name: "Aboleth", CR: 10, HP: 135, AC: 17, Attack: 16, AttackBonus: 9, Speed: 6, BlockRate: 0.1, XPValue: 5900, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Multiattack: The aboleth makes three tentacle attacks. Traits: Amphibious, Mucous Cloud, Probing Telepathy"}, "aboleth": {ID: "aboleth", Name: "Aboleth", CR: 10, HP: 135, AC: 17, Attack: 16, AttackBonus: 9, Speed: 6, BlockRate: 0.1, XPValue: 5900, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Multiattack: The aboleth makes three tentacle attacks. Traits: Amphibious, Mucous Cloud, Probing Telepathy"},
"deva": {ID: "deva", Name: "Deva", CR: 10, HP: 136, AC: 17, Attack: 16, AttackBonus: 8, Speed: 12, BlockRate: 0.1, XPValue: 5900, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The deva makes two melee attacks. Traits: Angelic Weapons, Innate Spellcasting, Magic Resistance"}, "deva": {ID: "deva", Name: "Deva", CR: 10, HP: 136, AC: 17, Attack: 16, AttackBonus: 8, Speed: 12, BlockRate: 0.1, XPValue: 5900, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The deva makes two melee attacks. Traits: Angelic Weapons, Innate Spellcasting, Magic Resistance"},
"guardian_naga": {ID: "guardian_naga", Name: "Guardian Naga", CR: 10, HP: 127, AC: 18, Attack: 16, AttackBonus: 8, Speed: 16, BlockRate: 0.15, XPValue: 5900, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Rejuvenation, Spellcasting"}, "guardian_naga": {ID: "guardian_naga", Name: "Guardian Naga", CR: 10, HP: 127, AC: 18, Attack: 16, AttackBonus: 8, Speed: 16, BlockRate: 0.15, XPValue: 5900, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Rejuvenation, Spellcasting"},
"stone_golem": {ID: "stone_golem", Name: "Stone Golem", CR: 10, HP: 178, AC: 17, Attack: 16, AttackBonus: 10, Speed: 12, BlockRate: 0.1, XPValue: 5900, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The golem makes two slam attacks. Traits: Immutable Form, Magic Resistance, Magic Weapons"}, "stone_golem": {ID: "stone_golem", Name: "Stone Golem", CR: 10, HP: 178, AC: 17, Attack: 16, AttackBonus: 10, Speed: 12, BlockRate: 0.1, XPValue: 5900, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The golem makes two slam attacks. Traits: Immutable Form, Magic Resistance, Magic Weapons"},
"young_gold_dragon": {ID: "young_gold_dragon", Name: "Young Gold Dragon", CR: 10, HP: 178, AC: 18, Attack: 16, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 5900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Amphibious", FireAttacker: true}, "young_gold_dragon": {ID: "young_gold_dragon", Name: "Young Gold Dragon", CR: 10, HP: 178, AC: 18, Attack: 16, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 5900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Amphibious", FireAttacker: true},
"young_red_dragon": {ID: "young_red_dragon", Name: "Young Red Dragon", CR: 10, HP: 178, AC: 18, Attack: 16, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 5900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws.", FireAttacker: true}, "young_red_dragon": {ID: "young_red_dragon", Name: "Young Red Dragon", CR: 10, HP: 178, AC: 18, Attack: 16, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 5900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws.", FireAttacker: true},
"behir": {ID: "behir", Name: "Behir", CR: 11, HP: 168, AC: 17, Attack: 18, AttackBonus: 0, Speed: 18, BlockRate: 0.1, XPValue: 7200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The behir makes two attacks: one with its bite and one to constrict."}, "behir": {ID: "behir", Name: "Behir", CR: 11, HP: 168, AC: 17, Attack: 18, AttackBonus: 0, Speed: 18, BlockRate: 0.1, XPValue: 7200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The behir makes two attacks: one with its bite and one to constrict."},
"chain_devil": {ID: "chain_devil", Name: "Chain Devil", CR: 11, HP: 85, AC: 16, Attack: 18, AttackBonus: 8, Speed: 12, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes two attacks with its chains. Traits: Devil's Sight, Magic Resistance"}, "chain_devil": {ID: "chain_devil", Name: "Chain Devil", CR: 11, HP: 85, AC: 16, Attack: 18, AttackBonus: 8, Speed: 12, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes two attacks with its chains. Traits: Devil's Sight, Magic Resistance"},
"djinni": {ID: "djinni", Name: "Djinni", CR: 11, HP: 161, AC: 17, Attack: 18, AttackBonus: 9, Speed: 12, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Multiattack: The djinni makes three scimitar attacks. Traits: Elemental Demise, Innate Spellcasting, Variant: Genie Powers"}, "djinni": {ID: "djinni", Name: "Djinni", CR: 11, HP: 161, AC: 17, Attack: 18, AttackBonus: 9, Speed: 12, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Multiattack: The djinni makes three scimitar attacks. Traits: Elemental Demise, Innate Spellcasting, Variant: Genie Powers"},
"efreeti": {ID: "efreeti", Name: "Efreeti", CR: 11, HP: 200, AC: 17, Attack: 18, AttackBonus: 7, Speed: 16, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Multiattack: The efreeti makes two scimitar attacks or uses its Hurl Flame twice. Traits: Elemental Demise, Innate Spellcasting, Variant: Genie Powers", FireAttacker: true}, "efreeti": {ID: "efreeti", Name: "Efreeti", CR: 11, HP: 200, AC: 17, Attack: 18, AttackBonus: 7, Speed: 16, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Multiattack: The efreeti makes two scimitar attacks or uses its Hurl Flame twice. Traits: Elemental Demise, Innate Spellcasting, Variant: Genie Powers", FireAttacker: true},
"gynosphinx": {ID: "gynosphinx", Name: "Gynosphinx", CR: 11, HP: 136, AC: 17, Attack: 18, AttackBonus: 9, Speed: 16, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The sphinx makes two claw attacks. Traits: Inscrutable, Magic Weapons, Spellcasting"}, "gynosphinx": {ID: "gynosphinx", Name: "Gynosphinx", CR: 11, HP: 136, AC: 17, Attack: 18, AttackBonus: 9, Speed: 16, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The sphinx makes two claw attacks. Traits: Inscrutable, Magic Weapons, Spellcasting"},
"horned_devil": {ID: "horned_devil", Name: "Horned Devil", CR: 11, HP: 178, AC: 18, Attack: 18, AttackBonus: 10, Speed: 8, BlockRate: 0.15, XPValue: 7200, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes three melee attacks: two with its fork and one with its tail. Traits: Devil's Sight, Magic Resistance"}, "horned_devil": {ID: "horned_devil", Name: "Horned Devil", CR: 11, HP: 178, AC: 18, Attack: 18, AttackBonus: 10, Speed: 8, BlockRate: 0.15, XPValue: 7200, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes three melee attacks: two with its fork and one with its tail. Traits: Devil's Sight, Magic Resistance"},
"remorhaz": {ID: "remorhaz", Name: "Remorhaz", CR: 11, HP: 195, AC: 17, Attack: 18, AttackBonus: 11, Speed: 12, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Traits: Heated Body"}, "remorhaz": {ID: "remorhaz", Name: "Remorhaz", CR: 11, HP: 195, AC: 17, Attack: 18, AttackBonus: 11, Speed: 12, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Traits: Heated Body"},
"roc": {ID: "roc", Name: "Roc", CR: 11, HP: 248, AC: 15, Attack: 18, AttackBonus: 13, Speed: 8, BlockRate: 0.1, XPValue: 7200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The roc makes two attacks: one with its beak and one with its talons. Traits: Keen Sight"}, "roc": {ID: "roc", Name: "Roc", CR: 11, HP: 248, AC: 15, Attack: 18, AttackBonus: 13, Speed: 8, BlockRate: 0.1, XPValue: 7200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The roc makes two attacks: one with its beak and one with its talons. Traits: Keen Sight"},
"archmage": {ID: "archmage", Name: "Archmage", CR: 12, HP: 99, AC: 12, Attack: 19, AttackBonus: 6, Speed: 12, BlockRate: 0, XPValue: 8400, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Magic Resistance, Spellcasting"}, "archmage": {ID: "archmage", Name: "Archmage", CR: 12, HP: 99, AC: 12, Attack: 19, AttackBonus: 6, Speed: 12, BlockRate: 0, XPValue: 8400, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Magic Resistance, Spellcasting"},
"erinyes": {ID: "erinyes", Name: "Erinyes", CR: 12, HP: 153, AC: 18, Attack: 19, AttackBonus: 8, Speed: 12, BlockRate: 0.15, XPValue: 8400, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The erinyes makes three attacks Traits: Hellish Weapons, Magic Resistance"}, "erinyes": {ID: "erinyes", Name: "Erinyes", CR: 12, HP: 153, AC: 18, Attack: 19, AttackBonus: 8, Speed: 12, BlockRate: 0.15, XPValue: 8400, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The erinyes makes three attacks Traits: Hellish Weapons, Magic Resistance"},
"adult_brass_dragon": {ID: "adult_brass_dragon", Name: "Adult Brass Dragon", CR: 13, HP: 172, AC: 18, Attack: 21, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 10000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)", FireAttacker: true}, "adult_brass_dragon": {ID: "adult_brass_dragon", Name: "Adult Brass Dragon", CR: 13, HP: 172, AC: 18, Attack: 21, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 10000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)", FireAttacker: true},
"adult_white_dragon": {ID: "adult_white_dragon", Name: "Adult White Dragon", CR: 13, HP: 200, AC: 18, Attack: 21, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 10000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Ice Walk, Legendary Resistance (3/Day)"}, "adult_white_dragon": {ID: "adult_white_dragon", Name: "Adult White Dragon", CR: 13, HP: 200, AC: 18, Attack: 21, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 10000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Ice Walk, Legendary Resistance (3/Day)"},
"nalfeshnee": {ID: "nalfeshnee", Name: "Nalfeshnee", CR: 13, HP: 184, AC: 18, Attack: 21, AttackBonus: 10, Speed: 8, BlockRate: 0.15, XPValue: 10000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The nalfeshnee uses Horror Nimbus if it can. Traits: Magic Resistance"}, "nalfeshnee": {ID: "nalfeshnee", Name: "Nalfeshnee", CR: 13, HP: 184, AC: 18, Attack: 21, AttackBonus: 10, Speed: 8, BlockRate: 0.15, XPValue: 10000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The nalfeshnee uses Horror Nimbus if it can. Traits: Magic Resistance"},
"rakshasa": {ID: "rakshasa", Name: "Rakshasa", CR: 13, HP: 110, AC: 16, Attack: 21, AttackBonus: 7, Speed: 16, BlockRate: 0.1, XPValue: 10000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The rakshasa makes two claw attacks Traits: Limited Magic Immunity, Innate Spellcasting"}, "rakshasa": {ID: "rakshasa", Name: "Rakshasa", CR: 13, HP: 110, AC: 16, Attack: 21, AttackBonus: 7, Speed: 16, BlockRate: 0.1, XPValue: 10000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The rakshasa makes two claw attacks Traits: Limited Magic Immunity, Innate Spellcasting"},
"storm_giant": {ID: "storm_giant", Name: "Storm Giant", CR: 13, HP: 230, AC: 16, Attack: 21, AttackBonus: 0, Speed: 18, BlockRate: 0.1, XPValue: 10000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The giant makes two greatsword attacks. Traits: Amphibious, Innate Spellcasting"}, "storm_giant": {ID: "storm_giant", Name: "Storm Giant", CR: 13, HP: 230, AC: 16, Attack: 21, AttackBonus: 0, Speed: 18, BlockRate: 0.1, XPValue: 10000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The giant makes two greatsword attacks. Traits: Amphibious, Innate Spellcasting"},
"vampire": {ID: "vampire", Name: "Vampire", CR: 13, HP: 144, AC: 16, Attack: 21, AttackBonus: 9, Speed: 12, BlockRate: 0.1, XPValue: 10000, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Traits: Shapechanger, Legendary Resistance (3/Day), Misty Escape, Regeneration, Spider Climb, Vampire Weaknesses"}, "vampire": {ID: "vampire", Name: "Vampire", CR: 13, HP: 144, AC: 16, Attack: 21, AttackBonus: 9, Speed: 12, BlockRate: 0.1, XPValue: 10000, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Traits: Shapechanger, Legendary Resistance (3/Day), Misty Escape, Regeneration, Spider Climb, Vampire Weaknesses"},
"adult_black_dragon": {ID: "adult_black_dragon", Name: "Adult Black Dragon", CR: 14, HP: 195, AC: 19, Attack: 23, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 11500, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"}, "adult_black_dragon": {ID: "adult_black_dragon", Name: "Adult Black Dragon", CR: 14, HP: 195, AC: 19, Attack: 23, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 11500, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
"adult_copper_dragon": {ID: "adult_copper_dragon", Name: "Adult Copper Dragon", CR: 14, HP: 184, AC: 18, Attack: 23, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 11500, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"}, "adult_copper_dragon": {ID: "adult_copper_dragon", Name: "Adult Copper Dragon", CR: 14, HP: 184, AC: 18, Attack: 23, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 11500, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
"ice_devil": {ID: "ice_devil", Name: "Ice Devil", CR: 14, HP: 180, AC: 18, Attack: 23, AttackBonus: 10, Speed: 16, BlockRate: 0.15, XPValue: 11500, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes three attacks: one with its bite, one with its claws, and one with its tail. Traits: Devil's Sight, Magic Resistance"}, "ice_devil": {ID: "ice_devil", Name: "Ice Devil", CR: 14, HP: 180, AC: 18, Attack: 23, AttackBonus: 10, Speed: 16, BlockRate: 0.15, XPValue: 11500, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes three attacks: one with its bite, one with its claws, and one with its tail. Traits: Devil's Sight, Magic Resistance"},
"adult_bronze_dragon": {ID: "adult_bronze_dragon", Name: "Adult Bronze Dragon", CR: 15, HP: 212, AC: 19, Attack: 24, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 13000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"}, "adult_bronze_dragon": {ID: "adult_bronze_dragon", Name: "Adult Bronze Dragon", CR: 15, HP: 212, AC: 19, Attack: 24, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 13000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
"adult_green_dragon": {ID: "adult_green_dragon", Name: "Adult Green Dragon", CR: 15, HP: 207, AC: 19, Attack: 24, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 13000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"}, "adult_green_dragon": {ID: "adult_green_dragon", Name: "Adult Green Dragon", CR: 15, HP: 207, AC: 19, Attack: 24, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 13000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
"mummy_lord": {ID: "mummy_lord", Name: "Mummy Lord", CR: 15, HP: 97, AC: 17, Attack: 24, AttackBonus: 9, Speed: 8, BlockRate: 0.1, XPValue: 13000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The mummy can use its Dreadful Glare and makes one attack with its rotting fist. Traits: Magic Resistance, Rejuvenation, Spellcasting"}, "mummy_lord": {ID: "mummy_lord", Name: "Mummy Lord", CR: 15, HP: 97, AC: 17, Attack: 24, AttackBonus: 9, Speed: 8, BlockRate: 0.1, XPValue: 13000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The mummy can use its Dreadful Glare and makes one attack with its rotting fist. Traits: Magic Resistance, Rejuvenation, Spellcasting"},
"purple_worm": {ID: "purple_worm", Name: "Purple Worm", CR: 15, HP: 247, AC: 18, Attack: 24, AttackBonus: 9, Speed: 18, BlockRate: 0.15, XPValue: 13000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The worm makes two attacks: one with its bite and one with its stinger. Traits: Tunneler"}, "purple_worm": {ID: "purple_worm", Name: "Purple Worm", CR: 15, HP: 247, AC: 18, Attack: 24, AttackBonus: 9, Speed: 18, BlockRate: 0.15, XPValue: 13000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The worm makes two attacks: one with its bite and one with its stinger. Traits: Tunneler"},
"adult_blue_dragon": {ID: "adult_blue_dragon", Name: "Adult Blue Dragon", CR: 16, HP: 225, AC: 19, Attack: 26, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"}, "adult_blue_dragon": {ID: "adult_blue_dragon", Name: "Adult Blue Dragon", CR: 16, HP: 225, AC: 19, Attack: 26, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
"adult_silver_dragon": {ID: "adult_silver_dragon", Name: "Adult Silver Dragon", CR: 16, HP: 243, AC: 19, Attack: 26, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"}, "adult_silver_dragon": {ID: "adult_silver_dragon", Name: "Adult Silver Dragon", CR: 16, HP: 243, AC: 19, Attack: 26, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
"iron_golem": {ID: "iron_golem", Name: "Iron Golem", CR: 16, HP: 210, AC: 20, Attack: 26, AttackBonus: 0, Speed: 12, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The golem makes two melee attacks. Traits: Fire Absorption, Immutable Form, Magic Resistance, Magic Weapons"}, "iron_golem": {ID: "iron_golem", Name: "Iron Golem", CR: 16, HP: 210, AC: 20, Attack: 26, AttackBonus: 0, Speed: 12, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The golem makes two melee attacks. Traits: Fire Absorption, Immutable Form, Magic Resistance, Magic Weapons"},
"marilith": {ID: "marilith", Name: "Marilith", CR: 16, HP: 189, AC: 18, Attack: 26, AttackBonus: 9, Speed: 16, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The marilith can make seven attacks: six with its longswords and one with its tail. Traits: Magic Resistance, Magic Weapons, Reactive"}, "marilith": {ID: "marilith", Name: "Marilith", CR: 16, HP: 189, AC: 18, Attack: 26, AttackBonus: 9, Speed: 16, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The marilith can make seven attacks: six with its longswords and one with its tail. Traits: Magic Resistance, Magic Weapons, Reactive"},
"planetar": {ID: "planetar", Name: "Planetar", CR: 16, HP: 200, AC: 19, Attack: 26, AttackBonus: 12, Speed: 16, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The planetar makes two melee attacks. Traits: Angelic Weapons, Divine Awareness, Innate Spellcasting, Magic Resistance"}, "planetar": {ID: "planetar", Name: "Planetar", CR: 16, HP: 200, AC: 19, Attack: 26, AttackBonus: 12, Speed: 16, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The planetar makes two melee attacks. Traits: Angelic Weapons, Divine Awareness, Innate Spellcasting, Magic Resistance"},
"adult_gold_dragon": {ID: "adult_gold_dragon", Name: "Adult Gold Dragon", CR: 17, HP: 256, AC: 19, Attack: 28, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 18000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)", FireAttacker: true}, "adult_gold_dragon": {ID: "adult_gold_dragon", Name: "Adult Gold Dragon", CR: 17, HP: 256, AC: 19, Attack: 28, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 18000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)", FireAttacker: true},
"adult_red_dragon": {ID: "adult_red_dragon", Name: "Adult Red Dragon", CR: 17, HP: 256, AC: 19, Attack: 28, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 18000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)", FireAttacker: true}, "adult_red_dragon": {ID: "adult_red_dragon", Name: "Adult Red Dragon", CR: 17, HP: 256, AC: 19, Attack: 28, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 18000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)", FireAttacker: true},
"androsphinx": {ID: "androsphinx", Name: "Androsphinx", CR: 17, HP: 199, AC: 17, Attack: 28, AttackBonus: 12, Speed: 16, BlockRate: 0.1, XPValue: 18000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The sphinx makes two claw attacks. Traits: Inscrutable, Magic Weapons, Spellcasting"}, "androsphinx": {ID: "androsphinx", Name: "Androsphinx", CR: 17, HP: 199, AC: 17, Attack: 28, AttackBonus: 12, Speed: 16, BlockRate: 0.1, XPValue: 18000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The sphinx makes two claw attacks. Traits: Inscrutable, Magic Weapons, Spellcasting"},
"dragon_turtle": {ID: "dragon_turtle", Name: "Dragon Turtle", CR: 17, HP: 341, AC: 20, Attack: 28, AttackBonus: 0, Speed: 8, BlockRate: 0.15, XPValue: 18000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon turtle makes three attacks: one with its bite and two with its claws. Traits: Amphibious", FireAttacker: true}, "dragon_turtle": {ID: "dragon_turtle", Name: "Dragon Turtle", CR: 17, HP: 341, AC: 20, Attack: 28, AttackBonus: 0, Speed: 8, BlockRate: 0.15, XPValue: 18000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon turtle makes three attacks: one with its bite and two with its claws. Traits: Amphibious", FireAttacker: true},
"balor": {ID: "balor", Name: "Balor", CR: 19, HP: 262, AC: 19, Attack: 31, AttackBonus: 14, Speed: 16, BlockRate: 0.15, XPValue: 22000, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Multiattack: The balor makes two attacks: one with its longsword and one with its whip. Traits: Death Throes, Fire Aura, Magic Resistance, Magic Weapons"}, "balor": {ID: "balor", Name: "Balor", CR: 19, HP: 262, AC: 19, Attack: 31, AttackBonus: 14, Speed: 16, BlockRate: 0.15, XPValue: 22000, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Multiattack: The balor makes two attacks: one with its longsword and one with its whip. Traits: Death Throes, Fire Aura, Magic Resistance, Magic Weapons"},
"ancient_brass_dragon": {ID: "ancient_brass_dragon", Name: "Ancient Brass Dragon", CR: 20, HP: 297, AC: 20, Attack: 33, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 25000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)", FireAttacker: true}, "ancient_brass_dragon": {ID: "ancient_brass_dragon", Name: "Ancient Brass Dragon", CR: 20, HP: 297, AC: 20, Attack: 33, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 25000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)", FireAttacker: true},
"ancient_white_dragon": {ID: "ancient_white_dragon", Name: "Ancient White Dragon", CR: 20, HP: 333, AC: 20, Attack: 33, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 25000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Ice Walk, Legendary Resistance (3/Day)"}, "ancient_white_dragon": {ID: "ancient_white_dragon", Name: "Ancient White Dragon", CR: 20, HP: 333, AC: 20, Attack: 33, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 25000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Ice Walk, Legendary Resistance (3/Day)"},
"pit_fiend": {ID: "pit_fiend", Name: "Pit Fiend", CR: 20, HP: 300, AC: 19, Attack: 33, AttackBonus: 14, Speed: 12, BlockRate: 0.15, XPValue: 25000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The pit fiend makes four attacks: one with its bite, one with its claw, one with its mace, and one with its tail. Traits: Fear Aura, Magic Resistance, Magic Weapons, Innate Spellcasting"}, "pit_fiend": {ID: "pit_fiend", Name: "Pit Fiend", CR: 20, HP: 300, AC: 19, Attack: 33, AttackBonus: 14, Speed: 12, BlockRate: 0.15, XPValue: 25000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The pit fiend makes four attacks: one with its bite, one with its claw, one with its mace, and one with its tail. Traits: Fear Aura, Magic Resistance, Magic Weapons, Innate Spellcasting"},
"ancient_black_dragon": {ID: "ancient_black_dragon", Name: "Ancient Black Dragon", CR: 21, HP: 367, AC: 22, Attack: 34, AttackBonus: 15, Speed: 16, BlockRate: 0.15, XPValue: 33000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"}, "ancient_black_dragon": {ID: "ancient_black_dragon", Name: "Ancient Black Dragon", CR: 21, HP: 367, AC: 22, Attack: 34, AttackBonus: 15, Speed: 16, BlockRate: 0.15, XPValue: 33000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
"ancient_copper_dragon": {ID: "ancient_copper_dragon", Name: "Ancient Copper Dragon", CR: 21, HP: 350, AC: 21, Attack: 34, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 33000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"}, "ancient_copper_dragon": {ID: "ancient_copper_dragon", Name: "Ancient Copper Dragon", CR: 21, HP: 350, AC: 21, Attack: 34, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 33000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
"lich": {ID: "lich", Name: "Lich", CR: 21, HP: 135, AC: 17, Attack: 34, AttackBonus: 12, Speed: 12, BlockRate: 0.1, XPValue: 33000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Legendary Resistance (3/Day), Rejuvenation, Spellcasting, Turn Resistance"}, "lich": {ID: "lich", Name: "Lich", CR: 21, HP: 135, AC: 17, Attack: 34, AttackBonus: 12, Speed: 12, BlockRate: 0.1, XPValue: 33000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Legendary Resistance (3/Day), Rejuvenation, Spellcasting, Turn Resistance"},
"solar": {ID: "solar", Name: "Solar", CR: 21, HP: 243, AC: 21, Attack: 34, AttackBonus: 15, Speed: 18, BlockRate: 0.15, XPValue: 33000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The solar makes two greatsword attacks. Traits: Angelic Weapons, Divine Awareness, Innate Spellcasting, Magic Resistance"}, "solar": {ID: "solar", Name: "Solar", CR: 21, HP: 243, AC: 21, Attack: 34, AttackBonus: 15, Speed: 18, BlockRate: 0.15, XPValue: 33000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The solar makes two greatsword attacks. Traits: Angelic Weapons, Divine Awareness, Innate Spellcasting, Magic Resistance"},
"ancient_bronze_dragon": {ID: "ancient_bronze_dragon", Name: "Ancient Bronze Dragon", CR: 22, HP: 444, AC: 22, Attack: 36, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 41000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"}, "ancient_bronze_dragon": {ID: "ancient_bronze_dragon", Name: "Ancient Bronze Dragon", CR: 22, HP: 444, AC: 22, Attack: 36, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 41000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
"ancient_green_dragon": {ID: "ancient_green_dragon", Name: "Ancient Green Dragon", CR: 22, HP: 385, AC: 21, Attack: 36, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 41000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"}, "ancient_green_dragon": {ID: "ancient_green_dragon", Name: "Ancient Green Dragon", CR: 22, HP: 385, AC: 21, Attack: 36, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 41000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
"ancient_blue_dragon": {ID: "ancient_blue_dragon", Name: "Ancient Blue Dragon", CR: 23, HP: 481, AC: 22, Attack: 37, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 50000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"}, "ancient_blue_dragon": {ID: "ancient_blue_dragon", Name: "Ancient Blue Dragon", CR: 23, HP: 481, AC: 22, Attack: 37, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 50000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
"ancient_silver_dragon": {ID: "ancient_silver_dragon", Name: "Ancient Silver Dragon", CR: 23, HP: 487, AC: 22, Attack: 37, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 50000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"}, "ancient_silver_dragon": {ID: "ancient_silver_dragon", Name: "Ancient Silver Dragon", CR: 23, HP: 487, AC: 22, Attack: 37, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 50000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
"kraken": {ID: "kraken", Name: "Kraken", CR: 23, HP: 472, AC: 18, Attack: 37, AttackBonus: 7, Speed: 8, BlockRate: 0.15, XPValue: 50000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The kraken makes three tentacle attacks, each of which it can replace with one use of Fling. Traits: Amphibious, Freedom of Movement, Siege Monster"}, "kraken": {ID: "kraken", Name: "Kraken", CR: 23, HP: 472, AC: 18, Attack: 37, AttackBonus: 7, Speed: 8, BlockRate: 0.15, XPValue: 50000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The kraken makes three tentacle attacks, each of which it can replace with one use of Fling. Traits: Amphibious, Freedom of Movement, Siege Monster"},
"ancient_gold_dragon": {ID: "ancient_gold_dragon", Name: "Ancient Gold Dragon", CR: 24, HP: 546, AC: 22, Attack: 38, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 62000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)", FireAttacker: true}, "ancient_gold_dragon": {ID: "ancient_gold_dragon", Name: "Ancient Gold Dragon", CR: 24, HP: 546, AC: 22, Attack: 38, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 62000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)", FireAttacker: true},
"ancient_red_dragon": {ID: "ancient_red_dragon", Name: "Ancient Red Dragon", CR: 24, HP: 546, AC: 22, Attack: 38, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 62000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)", FireAttacker: true}, "ancient_red_dragon": {ID: "ancient_red_dragon", Name: "Ancient Red Dragon", CR: 24, HP: 546, AC: 22, Attack: 38, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 62000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)", FireAttacker: true},
"tarrasque": {ID: "tarrasque", Name: "Tarrasque", CR: 30, HP: 676, AC: 25, Attack: 46, AttackBonus: 19, Speed: 16, BlockRate: 0.15, XPValue: 155000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The tarrasque can use its Frightful Presence. Traits: Legendary Resistance (3/Day), Magic Resistance, Reflective Carapace, Siege Monster"}, "tarrasque": {ID: "tarrasque", Name: "Tarrasque", CR: 30, HP: 676, AC: 25, Attack: 46, AttackBonus: 19, Speed: 16, BlockRate: 0.15, XPValue: 155000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The tarrasque can use its Frightful Presence. Traits: Legendary Resistance (3/Day), Magic Resistance, Reflective Carapace, Siege Monster"},
} }
} }

View File

@@ -141,11 +141,11 @@ type bjTable struct {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
type bjConfig struct { type bjConfig struct {
TimeoutSeconds int TimeoutSeconds int
AutoplayThreshold int AutoplayThreshold int
MinBet float64 MinBet float64
MaxBet float64 MaxBet float64
DebtLimit float64 DebtLimit float64
} }
func loadBJConfig() bjConfig { func loadBJConfig() bjConfig {
@@ -877,5 +877,3 @@ func (p *BlackjackPlugin) recordBJScore(userID id.UserID, net float64) {
recordBotDefeat(userID, "blackjack") recordBotDefeat(userID, "blackjack")
} }
} }

View File

@@ -592,4 +592,3 @@ func (p *AdventurePlugin) resolveDungeonAction(
return result return result
} }

View File

@@ -46,13 +46,13 @@ type CombatStats struct {
} }
type CombatModifiers struct { type CombatModifiers struct {
DamageBonus float64 // additive: 0 = neutral, 0.25 = +25% damage. Applied as (1 + DamageBonus). DamageBonus float64 // additive: 0 = neutral, 0.25 = +25% damage. Applied as (1 + DamageBonus).
DamageReduct float64 // multiplicative damage-taken reduction, 1.0 = neutral DamageReduct float64 // multiplicative damage-taken reduction, 1.0 = neutral
DeathSave bool // Sovereign reprieve — survive one lethal hit DeathSave bool // Sovereign reprieve — survive one lethal hit
PetAttackProc float64 PetAttackProc float64
PetAttackDmg int PetAttackDmg int
PetDeflectProc float64 PetDeflectProc float64
PetWhiffProc float64 // pet distracts enemy → guaranteed miss PetWhiffProc float64 // pet distracts enemy → guaranteed miss
// Spiritual Weapon — separate channel from the pet so the spectral mace // Spiritual Weapon — separate channel from the pet so the spectral mace
// gets its own narration when a cleric without a companion casts it. // gets its own narration when a cleric without a companion casts it.
// Damage formula mirrors PetAttack (Dmg + d5), proc rolls per round. // Damage formula mirrors PetAttack (Dmg + d5), proc rolls per round.
@@ -1298,4 +1298,3 @@ func trySave(st *combatState, player *Combatant, phaseName string) bool {
} }
return false return false
} }

View File

@@ -218,8 +218,8 @@ func DeriveDungeonMonsterStats(loc *AdvLocation) (CombatStats, CombatModifiers)
death := loc.BaseDeathPct // 8 to 60 death := loc.BaseDeathPct // 8 to 60
stats := CombatStats{ stats := CombatStats{
MaxHP: 12 + int(t*7+death*0.3), // T1≈22, T5≈65 — sized so dnd-scale players can finish a kill within the phase budget MaxHP: 12 + int(t*7+death*0.3), // T1≈22, T5≈65 — sized so dnd-scale players can finish a kill within the phase budget
Attack: int(t*1.5) + int(death*0.04), // T1=1, T5=9 — tuned to dnd HP pools (players have 1380 HP, not 100+) Attack: int(t*1.5) + int(death*0.04), // T1=1, T5=9 — tuned to dnd HP pools (players have 1380 HP, not 100+)
Defense: 2 + int(t*1.2), Defense: 2 + int(t*1.2),
Speed: 4 + int(t*1.5), Speed: 4 + int(t*1.5),
CritRate: 0.03 + death*0.003, CritRate: 0.03 + death*0.003,

View File

@@ -284,11 +284,11 @@ func TestBalanceRegression_DungeonDeathRates(t *testing.T) {
// drift), not 1:1 well-equipped combat. These bounds reflect the // drift), not 1:1 well-equipped combat. These bounds reflect the
// new "fairly breezy with some death" target. // new "fairly breezy with some death" target.
cases := []tc{ cases := []tc{
{1, 1, advDungeons[0], 0.03, 0.0}, // T1: trivial with proper gear {1, 1, advDungeons[0], 0.03, 0.0}, // T1: trivial with proper gear
{2, 2, advDungeons[1], 0.05, 0.0}, // T2: trivial at level {2, 2, advDungeons[1], 0.05, 0.0}, // T2: trivial at level
{5, 3, advDungeons[2], 0.08, 0.0}, // T3: low risk {5, 3, advDungeons[2], 0.08, 0.0}, // T3: low risk
{7, 4, advDungeons[3], 0.15, 0.0}, // T4: still mostly safe {7, 4, advDungeons[3], 0.15, 0.0}, // T4: still mostly safe
{9, 5, advDungeons[4], 0.25, 0.0}, // T5: occasional bad luck {9, 5, advDungeons[4], 0.25, 0.0}, // T5: occasional bad luck
} }
for _, c := range cases { for _, c := range cases {

View File

@@ -32,9 +32,9 @@ type bandsintownEvent struct {
Latitude string `json:"latitude"` Latitude string `json:"latitude"`
Longitude string `json:"longitude"` Longitude string `json:"longitude"`
} `json:"venue"` } `json:"venue"`
Lineup []string `json:"lineup"` Lineup []string `json:"lineup"`
OnSaleAt string `json:"on_sale_datetime"` OnSaleAt string `json:"on_sale_datetime"`
Offers []struct { Offers []struct {
Type string `json:"type"` Type string `json:"type"`
URL string `json:"url"` URL string `json:"url"`
Status string `json:"status"` Status string `json:"status"`

View File

@@ -228,21 +228,21 @@ func computeAC(class DnDClass, dexMod int) int {
// ── DnDCharacter struct + repository ───────────────────────────────────────── // ── DnDCharacter struct + repository ─────────────────────────────────────────
type DnDCharacter struct { type DnDCharacter struct {
UserID id.UserID UserID id.UserID
Race DnDRace Race DnDRace
Class DnDClass Class DnDClass
Level int Level int
XP int XP int
STR, DEX, CON int STR, DEX, CON int
INT, WIS, CHA int INT, WIS, CHA int
HPCurrent int HPCurrent int
HPMax int HPMax int
TempHP int TempHP int
ArmorClass int ArmorClass int
PendingSetup bool PendingSetup bool
AutoMigrated bool AutoMigrated bool
OnboardingSent bool OnboardingSent bool
ArmedAbility string ArmedAbility string
// Phase 9 — spell layer. // Phase 9 — spell layer.
// PendingCast: JSON blob describing the queued spell to fire on next // PendingCast: JSON blob describing the queued spell to fire on next
// one-shot combat (empty string = nothing queued). Set by !cast for // one-shot combat (empty string = nothing queued). Set by !cast for
@@ -251,30 +251,30 @@ type DnDCharacter struct {
// (empty = nothing active). ConcentrationExpiresAt is the wall-clock // (empty = nothing active). ConcentrationExpiresAt is the wall-clock
// expiry; nil for indefinite. Casting another concentration spell // expiry; nil for indefinite. Casting another concentration spell
// supersedes whatever's here. // supersedes whatever's here.
PendingCast string PendingCast string
ConcentrationSpell string ConcentrationSpell string
ConcentrationExpiresAt *time.Time ConcentrationExpiresAt *time.Time
// Phase 10 — subclass. // Phase 10 — subclass.
// Subclass: empty until the player runs !subclass at L5+. Once set, // Subclass: empty until the player runs !subclass at L5+. Once set,
// changing requires `!subclass <name>` again subject to a 30-day // changing requires `!subclass <name>` again subject to a 30-day
// cooldown (LastSubclassRespecAt) and 500-coin cost. // cooldown (LastSubclassRespecAt) and 500-coin cost.
Subclass DnDSubclass Subclass DnDSubclass
LastSubclassRespecAt *time.Time LastSubclassRespecAt *time.Time
// Phase 10 SUB2a — Berserker Frenzy increments after each rage'd // Phase 10 SUB2a — Berserker Frenzy increments after each rage'd
// combat; future class abilities will also tick this. Cleared on // combat; future class abilities will also tick this. Cleared on
// long rest. 5e caps at 6 (death); we let it grow and let consumers // long rest. 5e caps at 6 (death); we let it grow and let consumers
// clamp where needed. // clamp where needed.
Exhaustion int Exhaustion int
// 2026-05-10 immersion: short rest charges (1/level, restored on long // 2026-05-10 immersion: short rest charges (1/level, restored on long
// rest). resting_until gates !zone enter / !expedition start while the // rest). resting_until gates !zone enter / !expedition start while the
// character is "still resting" — short rest = 1h, long rest = 8h. // character is "still resting" — short rest = 1h, long rest = 8h.
ShortRestCharges int ShortRestCharges int
RestingUntil *time.Time RestingUntil *time.Time
LastRespecAt *time.Time LastRespecAt *time.Time
LastShortRestAt *time.Time LastShortRestAt *time.Time
LastLongRestAt *time.Time LastLongRestAt *time.Time
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
} }
// Modifiers returns the six ability modifiers in STR/DEX/CON/INT/WIS/CHA order. // Modifiers returns the six ability modifiers in STR/DEX/CON/INT/WIS/CHA order.

View File

@@ -24,9 +24,9 @@ import (
// ── Ability definitions ────────────────────────────────────────────────────── // ── Ability definitions ──────────────────────────────────────────────────────
type DnDAbility struct { type DnDAbility struct {
ID string ID string
Name string Name string
Class DnDClass Class DnDClass
// Subclass: if non-empty, ability is only available when the player's // Subclass: if non-empty, ability is only available when the player's
// subclass matches. Phase 10 adds the first such ability (Berserker // subclass matches. Phase 10 adds the first such ability (Berserker
// rage). Used by parseAbility/arm gating and by classActiveAbilities // rage). Used by parseAbility/arm gating and by classActiveAbilities

View File

@@ -54,9 +54,9 @@ func TestActiveAbilityCatalogue(t *testing.T) {
func TestParseAbility(t *testing.T) { func TestParseAbility(t *testing.T) {
cases := []struct { cases := []struct {
in string in string
id string id string
ok bool ok bool
}{ }{
{"second_wind", "second_wind", true}, {"second_wind", "second_wind", true},
{"second wind", "second_wind", true}, {"second wind", "second_wind", true},

View File

@@ -38,28 +38,28 @@ func setupAuditTestDB(t *testing.T) {
// regardless of MaxHP parity. // regardless of MaxHP parity.
func TestOrcRage_ThresholdExact(t *testing.T) { func TestOrcRage_ThresholdExact(t *testing.T) {
cases := []struct { cases := []struct {
hp, maxHP int hp, maxHP int
shouldFire bool shouldFire bool
}{ }{
{50, 100, false}, // exactly 50% — does NOT fire (strict <) {50, 100, false}, // exactly 50% — does NOT fire (strict <)
{49, 100, true}, // just under {49, 100, true}, // just under
{8, 16, false}, // exactly 50% even MaxHP {8, 16, false}, // exactly 50% even MaxHP
{7, 16, true}, {7, 16, true},
{8, 15, true}, // 8/15 = 53% — under former threshold of MaxHP/2=7. New: 8*2=16, 16<15 false. Hmm. {8, 15, true}, // 8/15 = 53% — under former threshold of MaxHP/2=7. New: 8*2=16, 16<15 false. Hmm.
} }
// Re-derive: HP*2 < MaxHP. So 8*2=16 < 15? No. Doesn't fire. That means at MaxHP=15, threshold trips at HP*2 < 15, i.e. HP < 7.5, i.e. HP ≤ 7. // Re-derive: HP*2 < MaxHP. So 8*2=16 < 15? No. Doesn't fire. That means at MaxHP=15, threshold trips at HP*2 < 15, i.e. HP < 7.5, i.e. HP ≤ 7.
// Old behavior used HP < MaxHP/2 = HP < 7 (integer div). So fired at HP < 7, i.e., HP ≤ 6. Different by one. // Old behavior used HP < MaxHP/2 = HP < 7 (integer div). So fired at HP < 7, i.e., HP ≤ 6. Different by one.
// Pick cases that distinguish. // Pick cases that distinguish.
cases = []struct { cases = []struct {
hp, maxHP int hp, maxHP int
shouldFire bool shouldFire bool
}{ }{
{50, 100, false}, {50, 100, false},
{49, 100, true}, {49, 100, true},
{8, 16, false}, {8, 16, false},
{7, 16, true}, {7, 16, true},
{6, 15, true}, // 6*2=12 < 15 ✓ {6, 15, true}, // 6*2=12 < 15 ✓
{7, 15, true}, // 7*2=14 < 15 ✓ (old code's MaxHP/2=7 would have NOT fired at HP=7) {7, 15, true}, // 7*2=14 < 15 ✓ (old code's MaxHP/2=7 would have NOT fired at HP=7)
{8, 15, false}, // 8*2=16, not < 15 {8, 15, false}, // 8*2=16, not < 15
} }
for _, c := range cases { for _, c := range cases {

View File

@@ -16,8 +16,8 @@ type DnDMonsterTemplate struct {
CR float32 // challenge rating (display only for now) CR float32 // challenge rating (display only for now)
HP int HP int
AC int AC int
Attack int // engine's "Attack" stat (raw damage value) Attack int // engine's "Attack" stat (raw damage value)
AttackBonus int // d20 to-hit bonus AttackBonus int // d20 to-hit bonus
Speed int Speed int
BlockRate float64 BlockRate float64
Ability *MonsterAbility Ability *MonsterAbility
@@ -176,10 +176,10 @@ var _ = func() bool {
"flameskull": { "flameskull": {
ID: "flameskull", Name: "Flameskull", ID: "flameskull", Name: "Flameskull",
CR: 4, HP: 40, AC: 13, Attack: 7, AttackBonus: 5, Speed: 15, CR: 4, HP: 40, AC: 13, Attack: 7, AttackBonus: 5, Speed: 15,
BlockRate: 0.05, BlockRate: 0.05,
Ability: &MonsterAbility{Name: "Fireball", Phase: "decisive", ProcChance: 0.55, Effect: "aoe_fire"}, Ability: &MonsterAbility{Name: "Fireball", Phase: "decisive", ProcChance: 0.55, Effect: "aoe_fire"},
XPValue: 1100, XPValue: 1100,
Notes: "Elite. Fireball 8d6 DEX DC 13. Rejuvenation in 1h unless holy water used.", Notes: "Elite. Fireball 8d6 DEX DC 13. Rejuvenation in 1h unless holy water used.",
FireAttacker: true, FireAttacker: true,
}, },
"boss_grol_unbroken": { "boss_grol_unbroken": {
@@ -385,37 +385,37 @@ var _ = func() bool {
"magmin": { "magmin": {
ID: "magmin", Name: "Magmin", ID: "magmin", Name: "Magmin",
CR: 0.5, HP: 9, AC: 14, Attack: 2, AttackBonus: 3, Speed: 10, CR: 0.5, HP: 9, AC: 14, Attack: 2, AttackBonus: 3, Speed: 10,
BlockRate: 0.05, BlockRate: 0.05,
Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1.00, Effect: "death_aoe"}, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1.00, Effect: "death_aoe"},
XPValue: 100, XPValue: 100,
Notes: "Death Burst: 2d6 fire in 10 ft on death. Ignite flammable objects on touch.", Notes: "Death Burst: 2d6 fire in 10 ft on death. Ignite flammable objects on touch.",
FireAttacker: true, FireAttacker: true,
}, },
"azer": { "azer": {
ID: "azer", Name: "Azer", ID: "azer", Name: "Azer",
CR: 2, HP: 39, AC: 17, Attack: 4, AttackBonus: 5, Speed: 12, CR: 2, HP: 39, AC: 17, Attack: 4, AttackBonus: 5, Speed: 12,
BlockRate: 0.20, BlockRate: 0.20,
Ability: &MonsterAbility{Name: "Fire Aura", Phase: "any", ProcChance: 0.50, Effect: "retaliate"}, Ability: &MonsterAbility{Name: "Fire Aura", Phase: "any", ProcChance: 0.50, Effect: "retaliate"},
XPValue: 450, XPValue: 450,
Notes: "1d10 fire to melee attackers; immune to fire; weapons deal heated bonus damage.", Notes: "1d10 fire to melee attackers; immune to fire; weapons deal heated bonus damage.",
FireAttacker: true, FireAttacker: true,
}, },
"salamander": { "salamander": {
ID: "salamander", Name: "Salamander", ID: "salamander", Name: "Salamander",
CR: 5, HP: 90, AC: 15, Attack: 8, AttackBonus: 7, Speed: 12, CR: 5, HP: 90, AC: 15, Attack: 8, AttackBonus: 7, Speed: 12,
BlockRate: 0.10, BlockRate: 0.10,
Ability: &MonsterAbility{Name: "Constrict", Phase: "any", ProcChance: 0.40, Effect: "stun"}, Ability: &MonsterAbility{Name: "Constrict", Phase: "any", ProcChance: 0.40, Effect: "stun"},
XPValue: 1800, XPValue: 1800,
Notes: "Constrict grapple + 2d6 fire/turn; spear multiattack; fire aura.", Notes: "Constrict grapple + 2d6 fire/turn; spear multiattack; fire aura.",
FireAttacker: true, FireAttacker: true,
}, },
"fire_elemental": { "fire_elemental": {
ID: "fire_elemental", Name: "Fire Elemental", ID: "fire_elemental", Name: "Fire Elemental",
CR: 5, HP: 102, AC: 13, Attack: 8, AttackBonus: 6, Speed: 14, CR: 5, HP: 102, AC: 13, Attack: 8, AttackBonus: 6, Speed: 14,
BlockRate: 0.05, BlockRate: 0.05,
Ability: &MonsterAbility{Name: "Fire Form", Phase: "any", ProcChance: 0.50, Effect: "retaliate"}, Ability: &MonsterAbility{Name: "Fire Form", Phase: "any", ProcChance: 0.50, Effect: "retaliate"},
XPValue: 1800, XPValue: 1800,
Notes: "1d10 contact fire damage; immune to fire; vulnerable to water/cold.", Notes: "1d10 contact fire damage; immune to fire; vulnerable to water/cold.",
FireAttacker: true, FireAttacker: true,
}, },
"helmed_horror": { "helmed_horror": {
@@ -429,10 +429,10 @@ var _ = func() bool {
"boss_emberlord_thyrak": { "boss_emberlord_thyrak": {
ID: "boss_emberlord_thyrak", Name: "Emberlord Thyrak", ID: "boss_emberlord_thyrak", Name: "Emberlord Thyrak",
CR: 9, HP: 178, AC: 18, Attack: 14, AttackBonus: 8, Speed: 11, CR: 9, HP: 178, AC: 18, Attack: 14, AttackBonus: 8, Speed: 11,
BlockRate: 0.20, BlockRate: 0.20,
Ability: &MonsterAbility{Name: "Forge Breath", Phase: "any", ProcChance: 0.30, Effect: "aoe"}, Ability: &MonsterAbility{Name: "Forge Breath", Phase: "any", ProcChance: 0.30, Effect: "aoe"},
XPValue: 5000, XPValue: 5000,
Notes: "Underforge boss. Phase 2 below 50% HP — spawns 2 Fire Elementals; breath recharge 46.", Notes: "Underforge boss. Phase 2 below 50% HP — spawns 2 Fire Elementals; breath recharge 46.",
FireAttacker: true, FireAttacker: true,
}, },
} }
@@ -594,10 +594,10 @@ var _ = func() bool {
"young_red_dragon": { "young_red_dragon": {
ID: "young_red_dragon", Name: "Young Red Dragon", ID: "young_red_dragon", Name: "Young Red Dragon",
CR: 10, HP: 178, AC: 18, Attack: 16, AttackBonus: 10, Speed: 16, CR: 10, HP: 178, AC: 18, Attack: 16, AttackBonus: 10, Speed: 16,
BlockRate: 0.10, BlockRate: 0.10,
Ability: &MonsterAbility{Name: "Fire Breath", Phase: "any", ProcChance: 0.30, Effect: "aoe_fire"}, Ability: &MonsterAbility{Name: "Fire Breath", Phase: "any", ProcChance: 0.30, Effect: "aoe_fire"},
XPValue: 5900, XPValue: 5900,
Notes: "Elite. Fire Breath 16d6 DEX DC 21; Multiattack; Frightful Presence WIS DC 16.", Notes: "Elite. Fire Breath 16d6 DEX DC 21; Multiattack; Frightful Presence WIS DC 16.",
FireAttacker: true, FireAttacker: true,
}, },
"boss_infernax": { "boss_infernax": {
@@ -612,10 +612,10 @@ var _ = func() bool {
// 460/AC21/42 over-corrected → ~40%; this Pass 3 405/AC20/42 // 460/AC21/42 over-corrected → ~40%; this Pass 3 405/AC20/42
// lands leaders in band.) // lands leaders in band.)
CR: 24, HP: 405, AC: 20, Attack: 38, AttackBonus: 11, Speed: 18, CR: 24, HP: 405, AC: 20, Attack: 38, AttackBonus: 11, Speed: 18,
BlockRate: 0.15, BlockRate: 0.15,
Ability: &MonsterAbility{Name: "Frightful Presence", Phase: "opening", ProcChance: 0.40, Effect: "stun"}, Ability: &MonsterAbility{Name: "Frightful Presence", Phase: "opening", ProcChance: 0.40, Effect: "stun"},
XPValue: 62000, XPValue: 62000,
Notes: "Dragon's Lair boss. Ancient Red Dragon. Legendary Actions; Lair Actions; phase 2 below 50% HP — fire ignores resistance.", Notes: "Dragon's Lair boss. Ancient Red Dragon. Legendary Actions; Lair Actions; phase 2 below 50% HP — fire ignores resistance.",
FireAttacker: true, FireAttacker: true,
}, },
// ── Abyss Portal ───────────────────────────────────────────────── // ── Abyss Portal ─────────────────────────────────────────────────
@@ -670,10 +670,10 @@ var _ = func() bool {
// 360/AC20/45 over-buffed → 19-40% leaders; this is the Pass 2 // 360/AC20/45 over-buffed → 19-40% leaders; this is the Pass 2
// correction back down toward band.) // correction back down toward band.)
CR: 19, HP: 300, AC: 20, Attack: 31, AttackBonus: 11, Speed: 14, CR: 19, HP: 300, AC: 20, Attack: 31, AttackBonus: 11, Speed: 14,
BlockRate: 0.20, BlockRate: 0.20,
Ability: &MonsterAbility{Name: "Lightning Discharge", Phase: "decisive", ProcChance: 0.40, Effect: "aoe"}, Ability: &MonsterAbility{Name: "Lightning Discharge", Phase: "decisive", ProcChance: 0.40, Effect: "aoe"},
XPValue: 22000, 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.", 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.",
FireAttacker: true, FireAttacker: true,
}, },
} }
@@ -700,13 +700,13 @@ func dndBestiaryByCR(maxCR float32) []DnDMonsterTemplate {
// named monsters in the existing combat pipeline. // named monsters in the existing combat pipeline.
func (m DnDMonsterTemplate) toCombatStats() (CombatStats, CombatModifiers) { func (m DnDMonsterTemplate) toCombatStats() (CombatStats, CombatModifiers) {
stats := CombatStats{ stats := CombatStats{
MaxHP: m.HP, MaxHP: m.HP,
Attack: m.Attack, Attack: m.Attack,
Defense: 0, Defense: 0,
Speed: m.Speed, Speed: m.Speed,
BlockRate: m.BlockRate, BlockRate: m.BlockRate,
AC: m.AC, AC: m.AC,
AttackBonus: m.AttackBonus, AttackBonus: m.AttackBonus,
FireAttacker: m.FireAttacker, FireAttacker: m.FireAttacker,
} }
mods := CombatModifiers{DamageReduct: 1.0} mods := CombatModifiers{DamageReduct: 1.0}

View File

@@ -90,9 +90,9 @@ func consumeFiredHealingItems(userID id.UserID, fired int) {
} }
// Filter to heal items, sort cheapest-tier-first. // Filter to heal items, sort cheapest-tier-first.
type entry struct { type entry struct {
id int64 id int64
tier int tier int
name string name string
} }
var heals []entry var heals []entry
for _, item := range items { for _, item := range items {

View File

@@ -36,17 +36,17 @@ var dndClassWeaponBonus = map[DnDClass]int{
// roughly 70-80% at low tiers and 50-60% at high tiers. Adjust after live // roughly 70-80% at low tiers and 50-60% at high tiers. Adjust after live
// data lands. // data lands.
const ( const (
dndArenaACBase = 10 dndArenaACBase = 10
dndArenaACPerThreat = 0.25 // ThreatLevel 0..30 → AC bump 0..7 dndArenaACPerThreat = 0.25 // ThreatLevel 0..30 → AC bump 0..7
dndDungeonACBase = 9 dndDungeonACBase = 9
// Dungeon AC scales linearly with tier: T1=10, T5=14 // Dungeon AC scales linearly with tier: T1=10, T5=14
) )
// Monster attack bonus. Counters player AC growth. T1 monster +5, T5 +9. // Monster attack bonus. Counters player AC growth. T1 monster +5, T5 +9.
const ( const (
dndArenaAtkBase = 4 dndArenaAtkBase = 4
dndArenaAtkPerThreat = 0.30 dndArenaAtkPerThreat = 0.30
dndDungeonAtkBase = 4 dndDungeonAtkBase = 4
// T1 dungeon +5, T5 +9 // T1 dungeon +5, T5 +9
) )
@@ -170,8 +170,8 @@ func applyDnDEquipmentLayer(stats *CombatStats, c *DnDCharacter, equip map[Equip
// buildHarnessPlayer (expedition / class-balance harnesses) so the // buildHarnessPlayer (expedition / class-balance harnesses) so the
// harness's measurement matches what live players experience. // harness's measurement matches what live players experience.
const ( const (
phase5BACBonus = 3 phase5BACBonus = 3
phase5BAttackBonus = 3 phase5BAttackBonus = 3
phase5BWeaponMagicBonus = 3 phase5BWeaponMagicBonus = 3
) )
@@ -335,7 +335,7 @@ func autoBuildCharacter(userID id.UserID, char *AdventureCharacter) *DnDCharacte
Class: sug.Class, Class: sug.Class,
Level: level, Level: level,
STR: scores[0], DEX: scores[1], CON: scores[2], STR: scores[0], DEX: scores[1], CON: scores[2],
INT: scores[3], WIS: scores[4], CHA: scores[5], INT: scores[3], WIS: scores[4], CHA: scores[5],
PendingSetup: false, PendingSetup: false,
AutoMigrated: true, AutoMigrated: true,
CreatedAt: time.Now().UTC(), CreatedAt: time.Now().UTC(),

View File

@@ -44,18 +44,18 @@ const (
// WeaponProfile mirrors the spec's struct. DamageDie is parsed from "1d8", // WeaponProfile mirrors the spec's struct. DamageDie is parsed from "1d8",
// "2d6", "1d10" etc. into Count/Sides on construction. // "2d6", "1d10" etc. into Count/Sides on construction.
type WeaponProfile struct { type WeaponProfile struct {
ID string ID string
Name string Name string
Category WeaponCategory Category WeaponCategory
DamageCount int // dice count DamageCount int // dice count
DamageSides int // die sides DamageSides int // die sides
DamageType string // slashing, piercing, bludgeoning DamageType string // slashing, piercing, bludgeoning
Properties []WeaponProperty Properties []WeaponProperty
VersaCount int // versatile two-handed dice (0 if not versatile) VersaCount int // versatile two-handed dice (0 if not versatile)
VersaSides int VersaSides int
MagicBonus int // 0 for mundane MagicBonus int // 0 for mundane
MagicProp string // empty for mundane MagicProp string // empty for mundane
NamedItem bool // true for §7.2 named magic weapons NamedItem bool // true for §7.2 named magic weapons
} }
// HasProperty reports whether the weapon has the given property. // HasProperty reports whether the weapon has the given property.
@@ -137,16 +137,17 @@ const (
) )
// ArmorProfile is the spec's struct. MaxDEXBonus convention: // ArmorProfile is the spec's struct. MaxDEXBonus convention:
// -1 = unlimited (light armor takes full DEX mod) //
// 2 = medium (cap at +2) // -1 = unlimited (light armor takes full DEX mod)
// 0 = heavy (no DEX bonus) // 2 = medium (cap at +2)
// 0 for shields (shields don't add DEX) // 0 = heavy (no DEX bonus)
// 0 for shields (shields don't add DEX)
type ArmorProfile struct { type ArmorProfile struct {
ID string ID string
Name string Name string
Type ArmorType Type ArmorType
BaseAC int // 0 for shields (shields use ShieldBonus) BaseAC int // 0 for shields (shields use ShieldBonus)
ShieldBonus int // +2 base for shields, 0 for armor ShieldBonus int // +2 base for shields, 0 for armor
MaxDEXBonus int MaxDEXBonus int
STRRequire int STRRequire int
StealthDisad bool StealthDisad bool

View File

@@ -8,17 +8,17 @@ import (
func TestParseDamageDie(t *testing.T) { func TestParseDamageDie(t *testing.T) {
cases := []struct { cases := []struct {
s string s string
count, sides int count, sides int
ok bool ok bool
}{ }{
{"1d8", 1, 8, true}, {"1d8", 1, 8, true},
{"2d6", 2, 6, true}, {"2d6", 2, 6, true},
{"1D10", 1, 10, true}, {"1D10", 1, 10, true},
{" 1d12 ", 1, 12, true}, {" 1d12 ", 1, 12, true},
{"d8", 0, 0, false}, // no count {"d8", 0, 0, false}, // no count
{"1d", 0, 0, false}, // no sides {"1d", 0, 0, false}, // no sides
{"1d1", 0, 0, false}, // sides < 2 {"1d1", 0, 0, false}, // sides < 2
{"banana", 0, 0, false}, {"banana", 0, 0, false},
{"", 0, 0, false}, {"", 0, 0, false},
} }
@@ -359,16 +359,16 @@ func TestSynthesizeWeaponProfile_NonWeaponSlotReturnsNil(t *testing.T) {
func TestSynthesizeArmorProfile_TierMapping(t *testing.T) { func TestSynthesizeArmorProfile_TierMapping(t *testing.T) {
cases := []struct { cases := []struct {
tier int tier int
wantBaseAC int wantBaseAC int
wantType ArmorType wantType ArmorType
}{ }{
{1, 11, ArmorTypeLight}, // Padded {1, 11, ArmorTypeLight}, // Padded
{2, 11, ArmorTypeLight}, // Leather {2, 11, ArmorTypeLight}, // Leather
{3, 13, ArmorTypeMedium}, // Chain Shirt {3, 13, ArmorTypeMedium}, // Chain Shirt
{4, 14, ArmorTypeMedium}, // Scale Mail {4, 14, ArmorTypeMedium}, // Scale Mail
{5, 15, ArmorTypeMedium}, // Half Plate {5, 15, ArmorTypeMedium}, // Half Plate
{6, 18, ArmorTypeHeavy}, // Plate {6, 18, ArmorTypeHeavy}, // Plate
} }
for _, c := range cases { for _, c := range cases {
eq := &AdvEquipment{Slot: SlotArmor, Tier: c.tier, Name: "Armor"} eq := &AdvEquipment{Slot: SlotArmor, Tier: c.tier, Name: "Armor"}
@@ -416,8 +416,8 @@ func TestPickWeaponAbilityMod(t *testing.T) {
}{ }{
{"wpn_greatsword", 3}, // melee non-finesse → STR {"wpn_greatsword", 3}, // melee non-finesse → STR
{"wpn_longsword", 3}, {"wpn_longsword", 3},
{"wpn_dagger", 3}, // finesse melee, but STR > DEX, picks STR {"wpn_dagger", 3}, // finesse melee, but STR > DEX, picks STR
{"wpn_longbow", 2}, // ranged → DEX {"wpn_longbow", 2}, // ranged → DEX
{"wpn_shortbow", 2}, {"wpn_shortbow", 2},
} }
for _, tc := range cases { for _, tc := range cases {

View File

@@ -535,4 +535,3 @@ func nullableString(s string) any {
} }
return s return s
} }

View File

@@ -8,18 +8,18 @@ import (
func TestResolveCombatInterrupt_Brackets(t *testing.T) { func TestResolveCombatInterrupt_Brackets(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
roll int roll int
threat int threat int
tier int tier int
class DnDClass class DnDClass
zone ZoneID zone ZoneID
want CombatInterruptKind want CombatInterruptKind
}{ }{
{"none", 5, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptNone}, // 5+1=6 {"none", 5, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptNone}, // 5+1=6
{"noise lower", 9, 0, 0, ClassFighter, ZoneGoblinWarrens, InterruptNoise}, {"noise lower", 9, 0, 0, ClassFighter, ZoneGoblinWarrens, InterruptNoise},
{"noise upper", 13, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptNoise}, // 13+1=14 {"noise upper", 13, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptNoise}, // 13+1=14
{"standard", 14, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 15 {"standard", 14, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 15
{"standard upper", 17, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 18 {"standard upper", 17, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 18
// Phase 5-B: elite bracket moved from 19+ to 23+. At totals // Phase 5-B: elite bracket moved from 19+ to 23+. At totals
// 19-21 (formerly Elite) the roll now resolves to Standard; // 19-21 (formerly Elite) the roll now resolves to Standard;
@@ -27,11 +27,11 @@ func TestResolveCombatInterrupt_Brackets(t *testing.T) {
// via threat-mod, since base d20+tier+ranger maxes at ~21). // via threat-mod, since base d20+tier+ranger maxes at ~21).
{"standard top of band", 18, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 19 → Standard now {"standard top of band", 18, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 19 → Standard now
{"standard band upper", 18, 0, 3, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 21 → Standard now {"standard band upper", 18, 0, 3, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 21 → Standard now
{"patrol", 20, 0, 2, ClassFighter, ZoneGoblinWarrens, InterruptPatrol}, // 22 {"patrol", 20, 0, 2, ClassFighter, ZoneGoblinWarrens, InterruptPatrol}, // 22
{"elite high threat", 18, 80, 3, ClassFighter, ZoneGoblinWarrens, InterruptElite}, // 18+3+2=23 → Elite {"elite high threat", 18, 80, 3, ClassFighter, ZoneGoblinWarrens, InterruptElite}, // 18+3+2=23 → Elite
{"threat bumps band", 13, 60, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, {"threat bumps band", 13, 60, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard},
{"ranger wilderness drops band", 17, 0, 1, ClassRanger, ZoneForestShadows, InterruptStandard}, // 17+1-3=15 {"ranger wilderness drops band", 17, 0, 1, ClassRanger, ZoneForestShadows, InterruptStandard}, // 17+1-3=15
{"ranger non-wild no drop", 17, 0, 1, ClassRanger, ZoneGoblinWarrens, InterruptStandard}, // 17+1=18 (Standard) {"ranger non-wild no drop", 17, 0, 1, ClassRanger, ZoneGoblinWarrens, InterruptStandard}, // 17+1=18 (Standard)
} }
for _, c := range cases { for _, c := range cases {
@@ -59,14 +59,14 @@ func TestResolveCombatInterrupt_ThreatModifier(t *testing.T) {
func TestMonsterKillTags_GatesKnownMonsters(t *testing.T) { func TestMonsterKillTags_GatesKnownMonsters(t *testing.T) {
cases := map[string][]string{ cases := map[string][]string{
"worg": {"worg", "beast"}, "worg": {"worg", "beast"},
"owlbear": {"owlbear", "beast"}, "owlbear": {"owlbear", "beast"},
"dire_wolf": {"beast"}, "dire_wolf": {"beast"},
"vampire_spawn": {"vampire_spawn"}, "vampire_spawn": {"vampire_spawn"},
"young_red_dragon": {"drake"}, "young_red_dragon": {"drake"},
"boss_belaxath": {"balor", "demon", "portal_boss"}, "boss_belaxath": {"balor", "demon", "portal_boss"},
"boss_thornmother": {"thornmother"}, "boss_thornmother": {"thornmother"},
"goblin_sneak": nil, // goblins gate nothing in §3 "goblin_sneak": nil, // goblins gate nothing in §3
} }
for id, want := range cases { for id, want := range cases {
got := monsterKillTags(id) got := monsterKillTags(id)

View File

@@ -96,7 +96,6 @@ func TestSaveHarvestNodes_DropsLegacyRoomKey(t *testing.T) {
} }
} }
func TestResolveHarvestOutcome_Brackets(t *testing.T) { func TestResolveHarvestOutcome_Brackets(t *testing.T) {
cases := []struct { cases := []struct {
roll, dc int roll, dc int

View File

@@ -56,11 +56,11 @@ func TestRenderRegionChain_CurrentMarker(t *testing.T) {
func TestRegionGlyph(t *testing.T) { func TestRegionGlyph(t *testing.T) {
cases := map[string]string{ cases := map[string]string{
"Surface Tunnels": "ST", "Surface Tunnels": "ST",
"The Deep Throne": "TDT", "The Deep Throne": "TDT",
"Drow Outpost": "DO", "Drow Outpost": "DO",
"Illithid Warren": "IW", "Illithid Warren": "IW",
"Kobold Warrens": "KW", "Kobold Warrens": "KW",
} }
for name, want := range cases { for name, want := range cases {
if got := regionGlyph(name); got != want { if got := regionGlyph(name); got != want {

View File

@@ -38,9 +38,9 @@ func TestWanderingOutcome_BucketBoundaries(t *testing.T) {
func TestResolveWanderingCheck_RoughCampBumpsRoll(t *testing.T) { func TestResolveWanderingCheck_RoughCampBumpsRoll(t *testing.T) {
exp := &Expedition{ exp := &Expedition{
ZoneID: ZoneGoblinWarrens, ZoneID: ZoneGoblinWarrens,
ThreatLevel: 0, ThreatLevel: 0,
Camp: &CampState{Active: true, Type: CampTypeRough}, Camp: &CampState{Active: true, Type: CampTypeRough},
} }
// roll 8 → +3 rough → total 11 → Minor (would have been Passage at 8). // roll 8 → +3 rough → total 11 → Minor (would have been Passage at 8).
nc := resolveWanderingCheck(exp, ClassFighter, func() int { return 8 }) nc := resolveWanderingCheck(exp, ClassFighter, func() int { return 8 })
@@ -54,9 +54,9 @@ func TestResolveWanderingCheck_RoughCampBumpsRoll(t *testing.T) {
func TestResolveWanderingCheck_FortifiedCampReducesRoll(t *testing.T) { func TestResolveWanderingCheck_FortifiedCampReducesRoll(t *testing.T) {
exp := &Expedition{ exp := &Expedition{
ZoneID: ZoneGoblinWarrens, ZoneID: ZoneGoblinWarrens,
ThreatLevel: 0, ThreatLevel: 0,
Camp: &CampState{Active: true, Type: CampTypeFortified}, Camp: &CampState{Active: true, Type: CampTypeFortified},
} }
// roll 14 → -4 fortified → total 10 → Passage. // roll 14 → -4 fortified → total 10 → Passage.
nc := resolveWanderingCheck(exp, ClassFighter, func() int { return 14 }) nc := resolveWanderingCheck(exp, ClassFighter, func() int { return 14 })
@@ -83,9 +83,9 @@ func TestResolveWanderingCheck_ThreatModAbove30(t *testing.T) {
func TestResolveWanderingCheck_RangerWildernessDiscount(t *testing.T) { func TestResolveWanderingCheck_RangerWildernessDiscount(t *testing.T) {
exp := &Expedition{ exp := &Expedition{
ZoneID: ZoneForestShadows, ZoneID: ZoneForestShadows,
ThreatLevel: 0, ThreatLevel: 0,
Camp: &CampState{Active: true, Type: CampTypeStandard}, Camp: &CampState{Active: true, Type: CampTypeStandard},
} }
nc := resolveWanderingCheck(exp, ClassRanger, func() int { return 12 }) nc := resolveWanderingCheck(exp, ClassRanger, func() int { return 12 })
if nc.ClassMod != -2 { if nc.ClassMod != -2 {
@@ -124,7 +124,7 @@ func TestProcessNightCheck_LogsAndBumpsThreatOnPassage(t *testing.T) {
} }
nc := NightCheck{ nc := NightCheck{
Outcome: NightOutcomePassage, Roll: 8, Total: 8, Outcome: NightOutcomePassage, Roll: 8, Total: 8,
Summary: "Signs of passage near camp; no encounter.", Summary: "Signs of passage near camp; no encounter.",
ThreatBumped: true, ThreatBumped: true,
} }
if err := processNightCheck(exp, nc); err != nil { if err := processNightCheck(exp, nc); err != nil {

View File

@@ -61,7 +61,7 @@ var regionsByZone = map[ZoneID][]ExpeditionRegion{
{ {
ID: "underdark_deep_throne", ZoneID: ZoneUnderdark, Order: 4, ID: "underdark_deep_throne", ZoneID: ZoneUnderdark, Order: 4,
Name: "The Deep Throne", BaseCampSite: false, Name: "The Deep Throne", BaseCampSite: false,
RegionBoss: "Ilvaras Xunyl (Zone Boss)", IsZoneBoss: true, RegionBoss: "Ilvaras Xunyl (Zone Boss)", IsZoneBoss: true,
EnemySubset: []string{"drow", "drow_elite_warrior", "drow_mage"}, EnemySubset: []string{"drow", "drow_elite_warrior", "drow_mage"},
}, },
}, },
@@ -87,7 +87,7 @@ var regionsByZone = map[ZoneID][]ExpeditionRegion{
{ {
ID: "dragons_lair_infernax_chamber", ZoneID: ZoneDragonsLair, Order: 4, ID: "dragons_lair_infernax_chamber", ZoneID: ZoneDragonsLair, Order: 4,
Name: "Infernax's Chamber", BaseCampSite: false, Name: "Infernax's Chamber", BaseCampSite: false,
RegionBoss: "Infernax (Zone Boss)", IsZoneBoss: true, RegionBoss: "Infernax (Zone Boss)", IsZoneBoss: true,
EnemySubset: []string{"young_red_dragon"}, EnemySubset: []string{"young_red_dragon"},
}, },
}, },
@@ -113,7 +113,7 @@ var regionsByZone = map[ZoneID][]ExpeditionRegion{
{ {
ID: "abyss_the_tear", ZoneID: ZoneAbyssPortal, Order: 4, ID: "abyss_the_tear", ZoneID: ZoneAbyssPortal, Order: 4,
Name: "The Tear", BaseCampSite: false, Name: "The Tear", BaseCampSite: false,
RegionBoss: "Belaxath (Zone Boss)", IsZoneBoss: true, RegionBoss: "Belaxath (Zone Boss)", IsZoneBoss: true,
EnemySubset: []string{"marilith"}, EnemySubset: []string{"marilith"},
}, },
}, },

View File

@@ -323,6 +323,7 @@ func addSupplyPurchase(s ExpeditionSupplies, p SupplyPurchase) ExpeditionSupplie
// - siege overrides everything with a hard 2× floor (even for tier 1 // - siege overrides everything with a hard 2× floor (even for tier 1
// where HarshMod is 1×) — the dungeon is actively starving you out. // where HarshMod is 1×) — the dungeon is actively starving you out.
// - otherwise, harshActive applies HarshMod (zone-tier scaled). // - otherwise, harshActive applies HarshMod (zone-tier scaled).
//
// phase5BDailyBurnRatePct is the shipped daily-burn multiplier from // phase5BDailyBurnRatePct is the shipped daily-burn multiplier from
// Phase 3-B's sweep + Phase 5-B's post-buff re-validation. 50 means // Phase 3-B's sweep + Phase 5-B's post-buff re-validation. 50 means
// "half live burn" — needed because the Phase 5-B player power floor // "half live burn" — needed because the Phase 5-B player power floor

View File

@@ -44,11 +44,11 @@ func TestSupplyDepletion_Brackets(t *testing.T) {
max float32 max float32
want SupplyDepletionState want SupplyDepletionState
}{ }{
{10, 10, SupplyNormal}, // 100% {10, 10, SupplyNormal}, // 100%
{3, 10, SupplyNormal}, // 30% {3, 10, SupplyNormal}, // 30%
{2.5, 10, SupplyNormal}, // 25% — boundary stays normal {2.5, 10, SupplyNormal}, // 25% — boundary stays normal
{2, 10, SupplyRationing}, // 20% {2, 10, SupplyRationing}, // 20%
{1, 10, SupplyRationing}, // 10% — boundary stays rationing {1, 10, SupplyRationing}, // 10% — boundary stays rationing
{0.9, 10, SupplySevereRationing}, {0.9, 10, SupplySevereRationing},
{0.1, 10, SupplySevereRationing}, {0.1, 10, SupplySevereRationing},
{0, 10, SupplyStarvation}, {0, 10, SupplyStarvation},

View File

@@ -455,12 +455,13 @@ const AbyssInstabilityPerDay = 5
const AbyssMaxInstability = 100 const AbyssMaxInstability = 100
// AbyssInstabilityBandFor classifies the §7.6 instability tiers. // AbyssInstabilityBandFor classifies the §7.6 instability tiers.
// normal: 020 //
// mild: 2140 (-1 WIS) // normal: 020
// warp: 4160 (rooms shift order) // mild: 2140 (-1 WIS)
// surges: 6180 (12h wandering checks) // warp: 4160 (rooms shift order)
// unravel: 8199 (supply ×2, -2 all rolls) // surges: 6180 (12h wandering checks)
// collapse: 100 // unravel: 8199 (supply ×2, -2 all rolls)
// collapse: 100
func AbyssInstabilityBandFor(stack int) string { func AbyssInstabilityBandFor(stack int) string {
switch { switch {
case stack <= 20: case stack <= 20:

View File

@@ -366,8 +366,8 @@ func TestUnderforge_FortifiedRestReducesHeat(t *testing.T) {
defer cleanupExpeditions(uid) defer cleanupExpeditions(uid)
exp := &Expedition{ exp := &Expedition{
ID: "fake-uf", ID: "fake-uf",
ZoneID: ZoneUnderforge, ZoneID: ZoneUnderforge,
TemporalStack: 5, TemporalStack: 5,
} }
// Insert minimal row so updateTemporalStack works. // Insert minimal row so updateTemporalStack works.

View File

@@ -39,14 +39,14 @@ const (
// currentBurn() (already wired); ShortRest disabled is checked by the // currentBurn() (already wired); ShortRest disabled is checked by the
// rest plumbing once expedition combat is wired. // rest plumbing once expedition combat is wired.
type ThreatBandInfo struct { type ThreatBandInfo struct {
Band ThreatBand Band ThreatBand
Label string Label string
Perception int // enemy passive perception bonus Perception int // enemy passive perception bonus
AC int // enemy AC bonus AC int // enemy AC bonus
InitAdv bool // enemy initiative advantage InitAdv bool // enemy initiative advantage
TrapsArm bool // re-arm cleared traps each day TrapsArm bool // re-arm cleared traps each day
SupplyMult float32 // multiplier on daily supply burn SupplyMult float32 // multiplier on daily supply burn
NoShortRest bool // siege-mode only NoShortRest bool // siege-mode only
} }
// threatBandFor returns the band for a given level, honouring siege override. // threatBandFor returns the band for a given level, honouring siege override.

View File

@@ -11,25 +11,25 @@ import (
// flavor files that would silently produce empty narrative. // flavor files that would silently produce empty narrative.
func TestDnDFlavorPoolsNonEmpty(t *testing.T) { func TestDnDFlavorPoolsNonEmpty(t *testing.T) {
pools := map[string][]string{ pools := map[string][]string{
"CombatStart": flavor.CombatStart, "CombatStart": flavor.CombatStart,
"CombatVictory": flavor.CombatVictory, "CombatVictory": flavor.CombatVictory,
"BossDeath": flavor.BossDeath, "BossDeath": flavor.BossDeath,
"PlayerDeath": flavor.PlayerDeath, "PlayerDeath": flavor.PlayerDeath,
"ZoneComplete": flavor.ZoneComplete, "ZoneComplete": flavor.ZoneComplete,
"Nat20": flavor.Nat20, "Nat20": flavor.Nat20,
"Nat1": flavor.Nat1, "Nat1": flavor.Nat1,
"LevelUp": flavor.LevelUp, "LevelUp": flavor.LevelUp,
"ItemFound": flavor.ItemFound, "ItemFound": flavor.ItemFound,
"RestShort": flavor.RestShort, "RestShort": flavor.RestShort,
"RestLong": flavor.RestLong, "RestLong": flavor.RestLong,
"HomeLongRest": flavor.HomeLongRest, "HomeLongRest": flavor.HomeLongRest,
"MistyGreeting": flavor.MistyGreeting, "MistyGreeting": flavor.MistyGreeting,
"MistyInsightSuccess": flavor.MistyInsightSuccess, "MistyInsightSuccess": flavor.MistyInsightSuccess,
"MistySkillFail": flavor.MistySkillFail, "MistySkillFail": flavor.MistySkillFail,
"ArinaGreeting": flavor.ArinaGreeting, "ArinaGreeting": flavor.ArinaGreeting,
"ArinaArcanaSuccess": flavor.ArinaArcanaSuccess, "ArinaArcanaSuccess": flavor.ArinaArcanaSuccess,
"ArinaSkillFail": flavor.ArinaSkillFail, "ArinaSkillFail": flavor.ArinaSkillFail,
"ExpeditionStart": flavor.ExpeditionStart, "ExpeditionStart": flavor.ExpeditionStart,
} }
for name, pool := range pools { for name, pool := range pools {
if len(pool) == 0 { if len(pool) == 0 {
@@ -47,18 +47,18 @@ func TestDnDFlavorPoolsNonEmpty(t *testing.T) {
// string when its underlying pool has entries. // string when its underlying pool has entries.
func TestDnDFlavorHelpersReturnNonEmpty(t *testing.T) { func TestDnDFlavorHelpersReturnNonEmpty(t *testing.T) {
cases := map[string]func() string{ cases := map[string]func() string{
"dndCombatOpeningLine": dndCombatOpeningLine, "dndCombatOpeningLine": dndCombatOpeningLine,
"dndZoneCompleteLine": dndZoneCompleteLine, "dndZoneCompleteLine": dndZoneCompleteLine,
"dndNat20Line": dndNat20Line, "dndNat20Line": dndNat20Line,
"dndNat1Line": dndNat1Line, "dndNat1Line": dndNat1Line,
"dndLevelUpFlavorLine": dndLevelUpFlavorLine, "dndLevelUpFlavorLine": dndLevelUpFlavorLine,
"dndItemFoundLine": dndItemFoundLine, "dndItemFoundLine": dndItemFoundLine,
"dndRestShortFlavorLine": dndRestShortFlavorLine, "dndRestShortFlavorLine": dndRestShortFlavorLine,
"dndCombatClosingLine_Win": func() string { return dndCombatClosingLine(true, false) }, "dndCombatClosingLine_Win": func() string { return dndCombatClosingLine(true, false) },
"dndCombatClosingLine_Boss": func() string { return dndCombatClosingLine(true, true) }, "dndCombatClosingLine_Boss": func() string { return dndCombatClosingLine(true, true) },
"dndCombatClosingLine_Death": func() string { return dndCombatClosingLine(false, false) }, "dndCombatClosingLine_Death": func() string { return dndCombatClosingLine(false, false) },
"dndRestLongFlavorLine_Home": func() string { return dndRestLongFlavorLine(true) }, "dndRestLongFlavorLine_Home": func() string { return dndRestLongFlavorLine(true) },
"dndRestLongFlavorLine_Inn": func() string { return dndRestLongFlavorLine(false) }, "dndRestLongFlavorLine_Inn": func() string { return dndRestLongFlavorLine(false) },
} }
for name, fn := range cases { for name, fn := range cases {
// Run several times — flavor pulls a random pool member, so a single // Run several times — flavor pulls a random pool member, so a single

View File

@@ -102,9 +102,10 @@ func applyRacePassives(stats *CombatStats, mods *CombatModifiers, c *DnDCharacte
// BEFORE consumable application (so consumables can stack on top). // BEFORE consumable application (so consumables can stack on top).
// //
// Some passives ride on existing CombatModifiers fields: // Some passives ride on existing CombatModifiers fields:
// Rogue's Sneak Attack reuses AutoCritFirst (the consumable Crystal Berry //
// field) — the engine already implements first-hit-auto-crit semantics. // Rogue's Sneak Attack reuses AutoCritFirst (the consumable Crystal Berry
// Cleric's Divine Favor reuses HealItem, the under-50%-HP heal trigger. // field) — the engine already implements first-hit-auto-crit semantics.
// Cleric's Divine Favor reuses HealItem, the under-50%-HP heal trigger.
// //
// This means: // This means:
// - A Rogue carrying a Crystal Berry doesn't get *two* auto-crits; // - A Rogue carrying a Crystal Berry doesn't get *two* auto-crits;

View File

@@ -112,9 +112,9 @@ func TestDnDRollSummaryLine_HighestNonNat20(t *testing.T) {
func TestParseDice(t *testing.T) { func TestParseDice(t *testing.T) {
cases := []struct { cases := []struct {
in string in string
count, sides, mod int count, sides, mod int
ok bool ok bool
}{ }{
{"d20", 1, 20, 0, true}, {"d20", 1, 20, 0, true},
{"1d20", 1, 20, 0, true}, {"1d20", 1, 20, 0, true},

View File

@@ -105,13 +105,13 @@ func (p *AdventurePlugin) handleDnDSetupCmd(ctx MessageContext, args string) err
case lower == "cancel": case lower == "cancel":
return p.dndSetupCancel(ctx) return p.dndSetupCancel(ctx)
} }
return p.SendDM(ctx.Sender,"Unknown !setup subcommand. Try !setup with no args for instructions.") return p.SendDM(ctx.Sender, "Unknown !setup subcommand. Try !setup with no args for instructions.")
} }
func (p *AdventurePlugin) dndSetupStatus(ctx MessageContext) error { func (p *AdventurePlugin) dndSetupStatus(ctx MessageContext) error {
c, err := LoadDnDCharacter(ctx.Sender) c, err := LoadDnDCharacter(ctx.Sender)
if err != nil { if err != nil {
return p.SendDM(ctx.Sender,"Couldn't load your Adv 2.0 draft: "+err.Error()) return p.SendDM(ctx.Sender, "Couldn't load your Adv 2.0 draft: "+err.Error())
} }
// No row yet → fresh setup. Show suggestion + race menu. // No row yet → fresh setup. Show suggestion + race menu.
@@ -125,7 +125,7 @@ func (p *AdventurePlugin) dndSetupStatus(ctx MessageContext) error {
b.WriteString("**Step 1 — Race.** Pick one of:\n") b.WriteString("**Step 1 — Race.** Pick one of:\n")
b.WriteString(renderRaceMenu()) b.WriteString(renderRaceMenu())
b.WriteString("\nReply: `!setup race <name>` (e.g. `!setup race elf`)\n") b.WriteString("\nReply: `!setup race <name>` (e.g. `!setup race elf`)\n")
return p.SendDM(ctx.Sender,b.String()) return p.SendDM(ctx.Sender, b.String())
} }
// Confirmed already → reroute to !sheet, unless this is an auto-migrated // Confirmed already → reroute to !sheet, unless this is an auto-migrated
@@ -152,62 +152,62 @@ func (p *AdventurePlugin) dndSetupStatus(ctx MessageContext) error {
b.WriteString("**Next: Step 1 — Race.**\n") b.WriteString("**Next: Step 1 — Race.**\n")
b.WriteString(renderRaceMenu()) b.WriteString(renderRaceMenu())
b.WriteString("\nReply: `!setup race <name>`\n") b.WriteString("\nReply: `!setup race <name>`\n")
return p.SendDM(ctx.Sender,b.String()) return p.SendDM(ctx.Sender, b.String())
} }
b.WriteString(fmt.Sprintf("Race: **%s** ✓\n", titleRace(c.Race))) b.WriteString(fmt.Sprintf("Race: **%s** ✓\n", titleRace(c.Race)))
if c.Class == "" { if c.Class == "" {
b.WriteString("\n**Next: Step 2 — Class.**\n") b.WriteString("\n**Next: Step 2 — Class.**\n")
b.WriteString(renderClassMenu()) b.WriteString(renderClassMenu())
b.WriteString("\nReply: `!setup class <name>`\n") b.WriteString("\nReply: `!setup class <name>`\n")
return p.SendDM(ctx.Sender,b.String()) return p.SendDM(ctx.Sender, b.String())
} }
b.WriteString(fmt.Sprintf("Class: **%s** ✓\n", titleClass(c.Class))) b.WriteString(fmt.Sprintf("Class: **%s** ✓\n", titleClass(c.Class)))
if !statsAssigned(c) { if !statsAssigned(c) {
b.WriteString("\n**Next: Step 3 — Ability Scores.**\n") b.WriteString("\n**Next: Step 3 — Ability Scores.**\n")
b.WriteString("Assign these six values — **15 14 13 12 10 8** — to STR, DEX, CON, INT, WIS, CHA. Each value used exactly once.\n\n") b.WriteString("Assign these six values — **15 14 13 12 10 8** — to STR, DEX, CON, INT, WIS, CHA. Each value used exactly once.\n\n")
b.WriteString("Reply: `!setup stats 15 14 13 12 10 8` (rearrange to taste).\n") b.WriteString("Reply: `!setup stats 15 14 13 12 10 8` (rearrange to taste).\n")
return p.SendDM(ctx.Sender,b.String()) return p.SendDM(ctx.Sender, b.String())
} }
b.WriteString(fmt.Sprintf("Stats (pre-racial): STR %d DEX %d CON %d INT %d WIS %d CHA %d ✓\n", b.WriteString(fmt.Sprintf("Stats (pre-racial): STR %d DEX %d CON %d INT %d WIS %d CHA %d ✓\n",
c.STR, c.DEX, c.CON, c.INT, c.WIS, c.CHA)) c.STR, c.DEX, c.CON, c.INT, c.WIS, c.CHA))
b.WriteString("\n**Step 4 — Confirm.** Reply `!setup confirm` to finalize, or `!setup cancel` to scrap and start over.\n") b.WriteString("\n**Step 4 — Confirm.** Reply `!setup confirm` to finalize, or `!setup cancel` to scrap and start over.\n")
b.WriteString(renderConfirmPreview(c)) b.WriteString(renderConfirmPreview(c))
return p.SendDM(ctx.Sender,b.String()) return p.SendDM(ctx.Sender, b.String())
} }
func (p *AdventurePlugin) dndSetupRace(ctx MessageContext, raceArg string) error { func (p *AdventurePlugin) dndSetupRace(ctx MessageContext, raceArg string) error {
r, ok := parseRace(raceArg) r, ok := parseRace(raceArg)
if !ok { if !ok {
return p.SendDM(ctx.Sender,"Unknown race. Options: human, elf, dwarf, halfling, orc, tiefling, half-elf") return p.SendDM(ctx.Sender, "Unknown race. Options: human, elf, dwarf, halfling, orc, tiefling, half-elf")
} }
c, err := loadOrInitDraft(ctx.Sender) c, err := loadOrInitDraft(ctx.Sender)
if err != nil { if err != nil {
return p.SendDM(ctx.Sender,"Couldn't open your draft: "+err.Error()) return p.SendDM(ctx.Sender, "Couldn't open your draft: "+err.Error())
} }
c.Race = r c.Race = r
if err := SaveDnDCharacter(c); err != nil { if err := SaveDnDCharacter(c); err != nil {
return p.SendDM(ctx.Sender,"Couldn't save: "+err.Error()) return p.SendDM(ctx.Sender, "Couldn't save: "+err.Error())
} }
ri, _ := raceInfo(r) ri, _ := raceInfo(r)
return p.SendDM(ctx.Sender,fmt.Sprintf("Race set: **%s**. _%s_\n\nNext: `!setup class <name>` — see options with `!setup`.", return p.SendDM(ctx.Sender, fmt.Sprintf("Race set: **%s**. _%s_\n\nNext: `!setup class <name>` — see options with `!setup`.",
ri.Display, ri.Passive)) ri.Display, ri.Passive))
} }
func (p *AdventurePlugin) dndSetupClass(ctx MessageContext, classArg string) error { func (p *AdventurePlugin) dndSetupClass(ctx MessageContext, classArg string) error {
cl, ok := parseClass(classArg) cl, ok := parseClass(classArg)
if !ok { if !ok {
return p.SendDM(ctx.Sender,"Unknown class. Options: fighter, rogue, mage, cleric, ranger") return p.SendDM(ctx.Sender, "Unknown class. Options: fighter, rogue, mage, cleric, ranger")
} }
c, err := loadOrInitDraft(ctx.Sender) c, err := loadOrInitDraft(ctx.Sender)
if err != nil { if err != nil {
return p.SendDM(ctx.Sender,"Couldn't open your draft: "+err.Error()) return p.SendDM(ctx.Sender, "Couldn't open your draft: "+err.Error())
} }
c.Class = cl c.Class = cl
if err := SaveDnDCharacter(c); err != nil { if err := SaveDnDCharacter(c); err != nil {
return p.SendDM(ctx.Sender,"Couldn't save: "+err.Error()) return p.SendDM(ctx.Sender, "Couldn't save: "+err.Error())
} }
ci, _ := classInfo(cl) ci, _ := classInfo(cl)
return p.SendDM(ctx.Sender,fmt.Sprintf("Class set: **%s** — leans on %s & %s.\n\n"+ return p.SendDM(ctx.Sender, fmt.Sprintf("Class set: **%s** — leans on %s & %s.\n\n"+
"Next: assign your stats. The standard array is **15 14 13 12 10 8** — six numbers, each used once.\n"+ "Next: assign your stats. The standard array is **15 14 13 12 10 8** — six numbers, each used once.\n"+
"Order: STR DEX CON INT WIS CHA.\n\n"+ "Order: STR DEX CON INT WIS CHA.\n\n"+
"Example: `!setup stats 15 14 13 12 10 8` (all rolled into STR-first; rearrange to taste).", "Example: `!setup stats 15 14 13 12 10 8` (all rolled into STR-first; rearrange to taste).",
@@ -227,29 +227,29 @@ func (p *AdventurePlugin) dndSetupStats(ctx MessageContext, statsArg string) err
} }
c, err := loadOrInitDraft(ctx.Sender) c, err := loadOrInitDraft(ctx.Sender)
if err != nil { if err != nil {
return p.SendDM(ctx.Sender,"Couldn't open your draft: "+err.Error()) return p.SendDM(ctx.Sender, "Couldn't open your draft: "+err.Error())
} }
c.STR, c.DEX, c.CON, c.INT, c.WIS, c.CHA = scores[0], scores[1], scores[2], scores[3], scores[4], scores[5] c.STR, c.DEX, c.CON, c.INT, c.WIS, c.CHA = scores[0], scores[1], scores[2], scores[3], scores[4], scores[5]
if err := SaveDnDCharacter(c); err != nil { if err := SaveDnDCharacter(c); err != nil {
return p.SendDM(ctx.Sender,"Couldn't save: "+err.Error()) return p.SendDM(ctx.Sender, "Couldn't save: "+err.Error())
} }
var b strings.Builder var b strings.Builder
b.WriteString("Stats saved (pre-racial bonuses).\n") b.WriteString("Stats saved (pre-racial bonuses).\n")
b.WriteString(renderConfirmPreview(c)) b.WriteString(renderConfirmPreview(c))
b.WriteString("\nReply `!setup confirm` to finalize.") b.WriteString("\nReply `!setup confirm` to finalize.")
return p.SendDM(ctx.Sender,b.String()) return p.SendDM(ctx.Sender, b.String())
} }
func (p *AdventurePlugin) dndSetupConfirm(ctx MessageContext) error { func (p *AdventurePlugin) dndSetupConfirm(ctx MessageContext) error {
c, err := LoadDnDCharacter(ctx.Sender) c, err := LoadDnDCharacter(ctx.Sender)
if err != nil || c == nil { if err != nil || c == nil {
return p.SendDM(ctx.Sender,"No setup draft found. Run `!setup` to start.") return p.SendDM(ctx.Sender, "No setup draft found. Run `!setup` to start.")
} }
if !c.PendingSetup { if !c.PendingSetup {
return p.SendDM(ctx.Sender,"Already confirmed. Use `!sheet` to view your character.") return p.SendDM(ctx.Sender, "Already confirmed. Use `!sheet` to view your character.")
} }
if c.Race == "" || c.Class == "" || !statsAssigned(c) { if c.Race == "" || c.Class == "" || !statsAssigned(c) {
return p.SendDM(ctx.Sender,"Draft incomplete. Run `!setup` to see what's missing.") return p.SendDM(ctx.Sender, "Draft incomplete. Run `!setup` to see what's missing.")
} }
// Apply racial modifiers to the assigned scores. // Apply racial modifiers to the assigned scores.
@@ -283,28 +283,28 @@ func (p *AdventurePlugin) dndSetupConfirm(ctx MessageContext) error {
c.UpdatedAt = time.Now().UTC() c.UpdatedAt = time.Now().UTC()
if err := SaveDnDCharacter(c); err != nil { if err := SaveDnDCharacter(c); err != nil {
return p.SendDM(ctx.Sender,"Couldn't save: "+err.Error()) return p.SendDM(ctx.Sender, "Couldn't save: "+err.Error())
} }
_ = initResources(ctx.Sender, c.Class) _ = initResources(ctx.Sender, c.Class)
// Phase 9: caster classes get a default known-spell list and slot pool. // Phase 9: caster classes get a default known-spell list and slot pool.
// Idempotent — !setup confirm after a respec wipe will repopulate. // Idempotent — !setup confirm after a respec wipe will repopulate.
_ = ensureSpellsForCharacter(c) _ = ensureSpellsForCharacter(c)
return p.SendDM(ctx.Sender,renderSetupComplete(c)) return p.SendDM(ctx.Sender, renderSetupComplete(c))
} }
func (p *AdventurePlugin) dndSetupCancel(ctx MessageContext) error { func (p *AdventurePlugin) dndSetupCancel(ctx MessageContext) error {
c, err := LoadDnDCharacter(ctx.Sender) c, err := LoadDnDCharacter(ctx.Sender)
if err != nil || c == nil { if err != nil || c == nil {
return p.SendDM(ctx.Sender,"No draft to cancel.") return p.SendDM(ctx.Sender, "No draft to cancel.")
} }
if !c.PendingSetup { if !c.PendingSetup {
return p.SendDM(ctx.Sender,"Your character is already finalized — use `!respec` instead.") return p.SendDM(ctx.Sender, "Your character is already finalized — use `!respec` instead.")
} }
if _, err := dbExecCancel(ctx.Sender); err != nil { if _, err := dbExecCancel(ctx.Sender); err != nil {
return p.SendDM(ctx.Sender,"Couldn't cancel: "+err.Error()) return p.SendDM(ctx.Sender, "Couldn't cancel: "+err.Error())
} }
return p.SendDM(ctx.Sender,"Draft scrapped. Run `!setup` to start over.") return p.SendDM(ctx.Sender, "Draft scrapped. Run `!setup` to start over.")
} }
// ── !respec ────────────────────────────────────────────────────────────────── // ── !respec ──────────────────────────────────────────────────────────────────
@@ -480,11 +480,11 @@ func dbExecCancel(userID id.UserID) (int64, error) {
// parseStatsArg accepts the six standard-array values in any of several // parseStatsArg accepts the six standard-array values in any of several
// natural forms. All of these work: // natural forms. All of these work:
// //
// 15 14 13 12 10 8 // 15 14 13 12 10 8
// 15, 14, 13, 12, 10, 8 // 15, 14, 13, 12, 10, 8
// (15, 14, 13, 12, 10, 8) // (15, 14, 13, 12, 10, 8)
// {15 14 13 12 10 8} // {15 14 13 12 10 8}
// [15,14,13,12,10,8] // [15,14,13,12,10,8]
// //
// Returns the scores in input order. Validation that they're a permutation // Returns the scores in input order. Validation that they're a permutation
// of the standard array happens in the caller via isStandardArray. // of the standard array happens in the caller via isStandardArray.

View File

@@ -21,17 +21,17 @@ import (
type DnDSkill string type DnDSkill string
const ( const (
SkillAthletics DnDSkill = "athletics" SkillAthletics DnDSkill = "athletics"
SkillAcrobatics DnDSkill = "acrobatics" SkillAcrobatics DnDSkill = "acrobatics"
SkillStealth DnDSkill = "stealth" SkillStealth DnDSkill = "stealth"
SkillSleightOfHand DnDSkill = "sleight_of_hand" SkillSleightOfHand DnDSkill = "sleight_of_hand"
SkillArcana DnDSkill = "arcana" SkillArcana DnDSkill = "arcana"
SkillInvestigation DnDSkill = "investigation" SkillInvestigation DnDSkill = "investigation"
SkillPerception DnDSkill = "perception" SkillPerception DnDSkill = "perception"
SkillInsight DnDSkill = "insight" SkillInsight DnDSkill = "insight"
SkillPersuasion DnDSkill = "persuasion" SkillPersuasion DnDSkill = "persuasion"
SkillIntimidation DnDSkill = "intimidation" SkillIntimidation DnDSkill = "intimidation"
SkillDeception DnDSkill = "deception" SkillDeception DnDSkill = "deception"
) )
type dndSkillInfo struct { type dndSkillInfo struct {

View File

@@ -459,20 +459,20 @@ func (d turnBuffDelta) any() bool {
// numerically identical to the auto-resolve engine. // numerically identical to the auto-resolve engine.
func diffTurnBuff(bs, as CombatStats, bm, am CombatModifiers) turnBuffDelta { func diffTurnBuff(bs, as CombatStats, bm, am CombatModifiers) turnBuffDelta {
d := turnBuffDelta{ d := turnBuffDelta{
dAC: as.AC - bs.AC, dAC: as.AC - bs.AC,
dAtk: as.AttackBonus - bs.AttackBonus, dAtk: as.AttackBonus - bs.AttackBonus,
dSpeed: as.Speed - bs.Speed, dSpeed: as.Speed - bs.Speed,
dPetDmg: am.PetAttackDmg - bm.PetAttackDmg, dPetDmg: am.PetAttackDmg - bm.PetAttackDmg,
dCrit: as.CritRate - bs.CritRate, dCrit: as.CritRate - bs.CritRate,
dDmgBonus: am.DamageBonus - bm.DamageBonus, dDmgBonus: am.DamageBonus - bm.DamageBonus,
dPetProc: am.PetAttackProc - bm.PetAttackProc, dPetProc: am.PetAttackProc - bm.PetAttackProc,
ward: am.WardCharges - bm.WardCharges, ward: am.WardCharges - bm.WardCharges,
spore: am.SporeCloud - bm.SporeCloud, spore: am.SporeCloud - bm.SporeCloud,
reflect: am.ReflectNext - bm.ReflectNext, reflect: am.ReflectNext - bm.ReflectNext,
arcaneWard: am.ArcaneWardHP - bm.ArcaneWardHP, arcaneWard: am.ArcaneWardHP - bm.ArcaneWardHP,
autoCrit: am.AutoCritFirst && !bm.AutoCritFirst, autoCrit: am.AutoCritFirst && !bm.AutoCritFirst,
enemySkip: am.SpellEnemySkipFirst && !bm.SpellEnemySkipFirst, enemySkip: am.SpellEnemySkipFirst && !bm.SpellEnemySkipFirst,
heal: as.MaxHP - bs.MaxHP, heal: as.MaxHP - bs.MaxHP,
dSpiritProc: am.SpiritWeaponProc - bm.SpiritWeaponProc, dSpiritProc: am.SpiritWeaponProc - bm.SpiritWeaponProc,
dSpiritDmg: am.SpiritWeaponDmg - bm.SpiritWeaponDmg, dSpiritDmg: am.SpiritWeaponDmg - bm.SpiritWeaponDmg,
} }

View File

@@ -29,14 +29,14 @@ func TestEnemySpellSaveMod_Scales(t *testing.T) {
atk int atk int
want int want int
}{ }{
{0, 0}, // unstatted enemy {0, 0}, // unstatted enemy
{2, 1}, // T1 {2, 1}, // T1
{4, 2}, // T2 {4, 2}, // T2
{6, 3}, // T3 {6, 3}, // T3
{8, 4}, // T4 {8, 4}, // T4
{10, 5}, // T5 floor {10, 5}, // T5 floor
{12, 6}, // T5 boss {12, 6}, // T5 boss
{20, 10}, // outsized boss {20, 10}, // outsized boss
} }
for _, c := range cases { for _, c := range cases {
enemy := &CombatStats{AttackBonus: c.atk} enemy := &CombatStats{AttackBonus: c.atk}

View File

@@ -295,7 +295,7 @@ func buildSpellList() []SpellDefinition {
{ID: "spirit_guardians", Name: "Spirit Guardians", Level: 3, School: "conjuration", {ID: "spirit_guardians", Name: "Spirit Guardians", Level: 3, School: "conjuration",
Classes: cleric, Effect: EffectDamageSave, CastTime: CastAction, Classes: cleric, Effect: EffectDamageSave, CastTime: CastAction,
Concentration: true, SaveStat: "WIS", DamageDice: "3d8", Concentration: true, SaveStat: "WIS", DamageDice: "3d8",
DamageType: "radiant", AOE: true, DamageType: "radiant", AOE: true,
Description: "Ghostly guardians swarm around you, slashing at anything hostile that comes close.", Description: "Ghostly guardians swarm around you, slashing at anything hostile that comes close.",
Upcast: "more guardians per slot above 3rd"}, Upcast: "more guardians per slot above 3rd"},
{ID: "revivify", Name: "Revivify", Level: 3, School: "necromancy", {ID: "revivify", Name: "Revivify", Level: 3, School: "necromancy",
@@ -349,7 +349,7 @@ func buildSpellList() []SpellDefinition {
{ID: "wall_of_fire", Name: "Wall of Fire", Level: 4, School: "evocation", {ID: "wall_of_fire", Name: "Wall of Fire", Level: 4, School: "evocation",
Classes: mage, Effect: EffectDamageSave, CastTime: CastAction, Classes: mage, Effect: EffectDamageSave, CastTime: CastAction,
Concentration: true, SaveStat: "DEX", DamageDice: "5d8", Concentration: true, SaveStat: "DEX", DamageDice: "5d8",
DamageType: "fire", AOE: true, DamageType: "fire", AOE: true,
Description: "A long curtain of flame roars up from the ground. Anyone trying to cross regrets it loudly."}, Description: "A long curtain of flame roars up from the ground. Anyone trying to cross regrets it loudly."},
{ID: "greater_invisibility", Name: "Greater Invisibility", Level: 4, School: "illusion", {ID: "greater_invisibility", Name: "Greater Invisibility", Level: 4, School: "illusion",
Classes: mage, Effect: EffectBuffSelf, CastTime: CastAction, Classes: mage, Effect: EffectBuffSelf, CastTime: CastAction,
@@ -422,7 +422,7 @@ func buildSpellList() []SpellDefinition {
Description: "Call up a small spirit-companion in animal shape — cat, raven, frog, the usual. It scouts, fetches, and judges you silently."}, Description: "Call up a small spirit-companion in animal shape — cat, raven, frog, the usual. It scouts, fetches, and judges you silently."},
{ID: "clairvoyance", Name: "Clairvoyance", Level: 3, School: "divination", {ID: "clairvoyance", Name: "Clairvoyance", Level: 3, School: "divination",
Classes: []DnDClass{ClassMage, ClassCleric, ClassBard, ClassSorcerer, ClassWarlock}, Classes: []DnDClass{ClassMage, ClassCleric, ClassBard, ClassSorcerer, ClassWarlock},
Effect: EffectBuffSelf, CastTime: CastAction, Concentration: true, MaterialCost: 100, Effect: EffectBuffSelf, CastTime: CastAction, Concentration: true, MaterialCost: 100,
Description: "Plant an invisible eye-or-ear in a place you know or can describe, and watch the world through it from afar."}, Description: "Plant an invisible eye-or-ear in a place you know or can describe, and watch the world through it from afar."},
{ID: "glyph_of_warding", Name: "Glyph of Warding", Level: 3, School: "abjuration", {ID: "glyph_of_warding", Name: "Glyph of Warding", Level: 3, School: "abjuration",
Classes: []DnDClass{ClassMage, ClassCleric, ClassBard}, Effect: EffectDamageSave, CastTime: CastAction, Classes: []DnDClass{ClassMage, ClassCleric, ClassBard}, Effect: EffectDamageSave, CastTime: CastAction,

View File

@@ -140,7 +140,7 @@ func ragingPlayer() Combatant {
CritRate: 0.05, DodgeRate: 0.05, BlockRate: 0.05, CritRate: 0.05, DodgeRate: 0.05, BlockRate: 0.05,
}, },
Mods: CombatModifiers{ Mods: CombatModifiers{
DamageReduct: 1.0, DamageReduct: 1.0,
BerserkerRage: true, RageMeleeDmg: 2, BerserkerRage: true, RageMeleeDmg: 2,
PhysicalResistRage: true, FrenzyDmgBonus: 0.5, PhysicalResistRage: true, FrenzyDmgBonus: 0.5,
}, },

View File

@@ -205,11 +205,11 @@ func TestParseStatsArg(t *testing.T) {
} }
bad := []string{ bad := []string{
"", // empty "", // empty
"15 14 13 12 10", // 5 numbers "15 14 13 12 10", // 5 numbers
"15 14 13 12 10 8 7", // 7 numbers "15 14 13 12 10 8 7", // 7 numbers
"15 14 13 12 10 abc", // non-number "15 14 13 12 10 abc", // non-number
"banana", // garbage "banana", // garbage
} }
for _, in := range bad { for _, in := range bad {
if _, err := parseStatsArg(in); err == nil { if _, err := parseStatsArg(in); err == nil {
@@ -231,20 +231,20 @@ func TestParseStatsArg(t *testing.T) {
func TestDnDLevelFromCombatLevel(t *testing.T) { func TestDnDLevelFromCombatLevel(t *testing.T) {
cases := []struct{ combat, want int }{ cases := []struct{ combat, want int }{
{0, 1}, // floor {0, 1}, // floor
{1, 1}, {1, 1},
{4, 1}, {4, 1},
{5, 1}, {5, 1},
{9, 1}, {9, 1},
{10, 2}, {10, 2},
{15, 3}, {15, 3},
{20, 4}, // nonk {20, 4}, // nonk
{24, 4}, // quack {24, 4}, // quack
{25, 5}, {25, 5},
{28, 5}, // prosolis {28, 5}, // prosolis
{30, 6}, {30, 6},
{45, 9}, {45, 9},
{49, 9}, // holymachina {49, 9}, // holymachina
{50, 10}, {50, 10},
{99, 19}, {99, 19},
{100, 20}, // clamp {100, 20}, // clamp

View File

@@ -25,26 +25,26 @@ import (
// dndXPTable[L] = cumulative XP needed to reach level L from level 1. // dndXPTable[L] = cumulative XP needed to reach level L from level 1.
// L=0 is unused (D&D characters start at L1). // L=0 is unused (D&D characters start at L1).
var dndXPTable = [...]int{ var dndXPTable = [...]int{
0, // L1 (start) 0, // L1 (start)
300, // L2 300, // L2
900, // L3 900, // L3
1700, // L4 1700, // L4
2700, // L5 (anchor: doc) 2700, // L5 (anchor: doc)
4300, // L6 4300, // L6
6500, // L7 (anchor: doc) 6500, // L7 (anchor: doc)
9000, // L8 9000, // L8
11500, // L9 11500, // L9
14000, // L10 (anchor: doc) 14000, // L10 (anchor: doc)
18000, // L11 18000, // L11
22000, // L12 22000, // L12
26000, // L13 26000, // L13
30000, // L14 30000, // L14
34000, // L15 (anchor: doc) 34000, // L15 (anchor: doc)
44000, // L16 44000, // L16
54000, // L17 54000, // L17
64000, // L18 64000, // L18
74000, // L19 74000, // L19
85000, // L20 (anchor: doc) 85000, // L20 (anchor: doc)
} }
const dndMaxLevel = 20 const dndMaxLevel = 20
@@ -223,11 +223,11 @@ func buildLevelUpMessage(c *DnDCharacter, events []LevelUpEvent, flavor string)
// Combat XP formulas. Tuned so a L1 player needs ~3 wins per level early // Combat XP formulas. Tuned so a L1 player needs ~3 wins per level early
// and many more at high levels. // and many more at high levels.
const ( const (
dndArenaXPPerThreat = 12 // arena win XP = threat * this dndArenaXPPerThreat = 12 // arena win XP = threat * this
dndArenaXPMin = 30 // arena win XP floor (low-threat fights still pay) dndArenaXPMin = 30 // arena win XP floor (low-threat fights still pay)
dndDungeonXPPerTier = 60 // dungeon win XP = tier^1.4 * this, roughly dndDungeonXPPerTier = 60 // dungeon win XP = tier^1.4 * this, roughly
dndLossXPFraction = 0.25 dndLossXPFraction = 0.25
dndNearDeathXPBonus = 1.25 // multiplier for clutch wins dndNearDeathXPBonus = 1.25 // multiplier for clutch wins
) )
// arenaCombatXP returns the D&D XP to grant for an arena combat outcome. // arenaCombatXP returns the D&D XP to grant for an arena combat outcome.

View File

@@ -12,13 +12,13 @@ import (
func TestDnDXPToNextLevel(t *testing.T) { func TestDnDXPToNextLevel(t *testing.T) {
cases := []struct{ level, want int }{ cases := []struct{ level, want int }{
{1, 300}, // L1→L2: 300 {1, 300}, // L1→L2: 300
{2, 600}, // L2→L3: 900-300 {2, 600}, // L2→L3: 900-300
{4, 1000}, // L4→L5: 2700-1700 {4, 1000}, // L4→L5: 2700-1700
{6, 2200}, // L6→L7: 6500-4300 {6, 2200}, // L6→L7: 6500-4300
{19, 11000}, // L19→L20: 85000-74000 {19, 11000}, // L19→L20: 85000-74000
{20, 0}, // capped {20, 0}, // capped
{0, 300}, // clamp to 1 {0, 300}, // clamp to 1
} }
for _, c := range cases { for _, c := range cases {
if got := dndXPToNextLevel(c.level); got != c.want { if got := dndXPToNextLevel(c.level); got != c.want {

View File

@@ -1010,10 +1010,10 @@ func (p *AdventurePlugin) resolveRoom(userID id.UserID, run *DungeonRun, zone Zo
// runs combat, persists side effects, fires nat-1/nat-20 mood deltas, // runs combat, persists side effects, fires nat-1/nat-20 mood deltas,
// and renders the staged narration. Returns: // and renders the staged narration. Returns:
// //
// intro — pre-combat block (TwinBee combat-start + monster stat block) // intro — pre-combat block (TwinBee combat-start + monster stat block)
// phases — RenderCombatLog output, streamed with delays by the caller // phases — RenderCombatLog output, streamed with delays by the caller
// outcome — post-combat block: nat20/nat1 flavor, kill line, loot, d20 summary // outcome — post-combat block: nat20/nat1 flavor, kill line, loot, d20 summary
// ended — true when the player went down (caller skips next-room teaser) // ended — true when the player went down (caller skips next-room teaser)
// //
// Phases will be nil only on a "no roster" skip — caller treats that as a // Phases will be nil only on a "no roster" skip — caller treats that as a
// non-paced fallthrough. // non-paced fallthrough.

View File

@@ -56,13 +56,13 @@ func TestPassiveDecayMood_DriftsTowardFifty(t *testing.T) {
hoursAgo int hoursAgo int
want int want int
}{ }{
{"high-score-decays-down", 80, 5, 70}, // -2/hr * 5 {"high-score-decays-down", 80, 5, 70}, // -2/hr * 5
{"high-clamps-at-50", 80, 100, 50}, // would overshoot {"high-clamps-at-50", 80, 100, 50}, // would overshoot
{"low-score-decays-up", 10, 5, 20}, // +2/hr * 5 {"low-score-decays-up", 10, 5, 20}, // +2/hr * 5
{"low-clamps-at-50", 10, 100, 50}, // would overshoot {"low-clamps-at-50", 10, 100, 50}, // would overshoot
{"already-50-no-change", 50, 100, 50}, // neutral stays neutral {"already-50-no-change", 50, 100, 50}, // neutral stays neutral
{"fresh-no-decay", 80, 0, 80}, // no time elapsed {"fresh-no-decay", 80, 0, 80}, // no time elapsed
{"sub-hour-no-decay", 80, 0, 80}, // hours==0 → no drift {"sub-hour-no-decay", 80, 0, 80}, // hours==0 → no drift
} }
for _, c := range cases { for _, c := range cases {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {

View File

@@ -73,9 +73,9 @@ type DungeonRun struct {
// path of node_ids the player has resolved. NodeChoices stores // path of node_ids the player has resolved. NodeChoices stores
// pending fork-prompt state (G5 surface) — populated when the player // pending fork-prompt state (G5 surface) — populated when the player
// arrives at a fork with 2+ unlocked outgoing edges. // arrives at a fork with 2+ unlocked outgoing edges.
CurrentNode string CurrentNode string
VisitedNodes []string VisitedNodes []string
NodeChoices map[string]any NodeChoices map[string]any
// Revisit R1 — monotonic count of node entries, including the entry // Revisit R1 — monotonic count of node entries, including the entry
// room. Distinct from CurrentRoom: CurrentRoom answers "where am I on // room. Distinct from CurrentRoom: CurrentRoom answers "where am I on
@@ -623,4 +623,3 @@ func addLoot(runID string, itemID string) error {
WHERE run_id = ?`, string(lootJSON), runID) WHERE run_id = ?`, string(lootJSON), runID)
return err return err
} }

View File

@@ -37,18 +37,18 @@ const (
// (count, sides) form; AlertsEnemies flags traps that have no damage // (count, sides) form; AlertsEnemies flags traps that have no damage
// but raise the alarm. Weight biases the room-level random pick. // but raise the alarm. Weight biases the room-level random pick.
type zoneTrapDef struct { type zoneTrapDef struct {
Kind ZoneTrapKind Kind ZoneTrapKind
Display string Display string
Trigger string // short flavor descriptor for the trip narration Trigger string // short flavor descriptor for the trip narration
DetectSkill DnDSkill DetectSkill DnDSkill
DetectDC int DetectDC int
DamageDiceN int // 0 if no direct damage DamageDiceN int // 0 if no direct damage
DamageDiceD int // sides DamageDiceD int // sides
DamageType string // "bludgeoning", "piercing", "poison", ... DamageType string // "bludgeoning", "piercing", "poison", ...
AlertsEnemies bool // tripwire-class: 0 damage, raises alarm AlertsEnemies bool // tripwire-class: 0 damage, raises alarm
SuppressesMagic bool // antimagic-class: 0 damage, narrative suppression SuppressesMagic bool // antimagic-class: 0 damage, narrative suppression
Tier ZoneTier Tier ZoneTier
Weight int // 1..10, default 5 Weight int // 1..10, default 5
} }
// tier1TrapCatalog — design doc §6 Tier 1 table. // tier1TrapCatalog — design doc §6 Tier 1 table.

View File

@@ -583,4 +583,3 @@ func (p *EuroPlugin) handleGrant(ctx MessageContext) error {
return p.SendReply(ctx.RoomID, ctx.EventID, return p.SendReply(ctx.RoomID, ctx.EventID,
fmt.Sprintf("✅ Granted €%d to **%s**. New balance: €%d.", int(amount), targetName, int(newBalance))) fmt.Sprintf("✅ Granted €%d to **%s**. New balance: €%d.", int(amount), targetName, int(newBalance)))
} }

View File

@@ -209,9 +209,9 @@ func (p *AdventurePlugin) deliverAmbient(e *Expedition, now time.Time) error {
// ambientEvent — one entry in the ambient pool. Weight is relative; the // ambientEvent — one entry in the ambient pool. Weight is relative; the
// resolver normalizes over eligible events at pick time. // resolver normalizes over eligible events at pick time.
type ambientEvent struct { type ambientEvent struct {
Kind string Kind string
Pool []string Pool []string
Weight int Weight int
Eligible func(*Expedition) bool Eligible func(*Expedition) bool
} }
@@ -219,9 +219,9 @@ type ambientEvent struct {
// out so deliverAmbient can degrade to it if a chosen pool is empty. // out so deliverAmbient can degrade to it if a chosen pool is empty.
func ambientEventMonologue() ambientEvent { func ambientEventMonologue() ambientEvent {
return ambientEvent{ return ambientEvent{
Kind: "monologue", Kind: "monologue",
Pool: flavor.AmbientMonologue, Pool: flavor.AmbientMonologue,
Weight: 35, Weight: 35,
Eligible: func(*Expedition) bool { return true }, Eligible: func(*Expedition) bool { return true },
} }
} }
@@ -233,15 +233,15 @@ func ambientEvents() []ambientEvent {
return []ambientEvent{ return []ambientEvent{
ambientEventMonologue(), ambientEventMonologue(),
{ {
Kind: "small_find", Kind: "small_find",
Pool: flavor.AmbientSmallFind, Pool: flavor.AmbientSmallFind,
Weight: 10, Weight: 10,
Eligible: always, Eligible: always,
}, },
{ {
Kind: "noise", Kind: "noise",
Pool: flavor.AmbientNoise, Pool: flavor.AmbientNoise,
Weight: 15, Weight: 15,
Eligible: always, Eligible: always,
}, },
{ {
@@ -256,9 +256,9 @@ func ambientEvents() []ambientEvent {
}, },
}, },
{ {
Kind: "lucky_find", Kind: "lucky_find",
Pool: flavor.AmbientLuckyFind, Pool: flavor.AmbientLuckyFind,
Weight: 10, Weight: 10,
Eligible: always, Eligible: always,
}, },
{ {

View File

@@ -9,9 +9,9 @@ import (
func TestDecideAutopilotCamp_SkipsWhenCamped(t *testing.T) { func TestDecideAutopilotCamp_SkipsWhenCamped(t *testing.T) {
in := autoCampInputs{ in := autoCampInputs{
Camp: &CampState{Active: true, Type: CampTypeRough}, Camp: &CampState{Active: true, Type: CampTypeRough},
Run: &DungeonRun{CurrentRoom: 0, RoomsCleared: []int{0}}, Run: &DungeonRun{CurrentRoom: 0, RoomsCleared: []int{0}},
HPCur: 1, HPMax: 20, // very low — would otherwise camp HPCur: 1, HPMax: 20, // very low — would otherwise camp
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1}, Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
} }
if _, ok := decideAutopilotCamp(in); ok { if _, ok := decideAutopilotCamp(in); ok {
@@ -21,8 +21,8 @@ func TestDecideAutopilotCamp_SkipsWhenCamped(t *testing.T) {
func TestDecideAutopilotCamp_SkipsHealthyHP(t *testing.T) { func TestDecideAutopilotCamp_SkipsHealthyHP(t *testing.T) {
in := autoCampInputs{ in := autoCampInputs{
Run: &DungeonRun{CurrentRoom: 0, RoomsCleared: []int{0}}, Run: &DungeonRun{CurrentRoom: 0, RoomsCleared: []int{0}},
HPCur: 20, HPMax: 20, HPCur: 20, HPMax: 20,
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1}, Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
} }
if _, ok := decideAutopilotCamp(in); ok { if _, ok := decideAutopilotCamp(in); ok {
@@ -32,8 +32,8 @@ func TestDecideAutopilotCamp_SkipsHealthyHP(t *testing.T) {
func TestDecideAutopilotCamp_LowHPClearedPitchesStandard(t *testing.T) { func TestDecideAutopilotCamp_LowHPClearedPitchesStandard(t *testing.T) {
in := autoCampInputs{ in := autoCampInputs{
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}}, Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
HPCur: 5, HPMax: 20, // 25% — below 55% threshold HPCur: 5, HPMax: 20, // 25% — below 55% threshold
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1}, Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
} }
d, ok := decideAutopilotCamp(in) d, ok := decideAutopilotCamp(in)
@@ -44,8 +44,8 @@ func TestDecideAutopilotCamp_LowHPClearedPitchesStandard(t *testing.T) {
func TestDecideAutopilotCamp_LowHPUnclearedPitchesRough(t *testing.T) { func TestDecideAutopilotCamp_LowHPUnclearedPitchesRough(t *testing.T) {
in := autoCampInputs{ in := autoCampInputs{
Run: &DungeonRun{CurrentRoom: 2, RoomsCleared: []int{0, 1}}, Run: &DungeonRun{CurrentRoom: 2, RoomsCleared: []int{0, 1}},
HPCur: 5, HPMax: 20, HPCur: 5, HPMax: 20,
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1}, Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
} }
d, ok := decideAutopilotCamp(in) d, ok := decideAutopilotCamp(in)
@@ -56,8 +56,8 @@ func TestDecideAutopilotCamp_LowHPUnclearedPitchesRough(t *testing.T) {
func TestDecideAutopilotCamp_DowngradesWhenSuppliesTight(t *testing.T) { func TestDecideAutopilotCamp_DowngradesWhenSuppliesTight(t *testing.T) {
in := autoCampInputs{ in := autoCampInputs{
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}}, Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
HPCur: 5, HPMax: 20, HPCur: 5, HPMax: 20,
Supplies: ExpeditionSupplies{Current: 0.6, Max: 5, DailyBurn: 1}, // can't afford 1.0 Supplies: ExpeditionSupplies{Current: 0.6, Max: 5, DailyBurn: 1}, // can't afford 1.0
} }
d, ok := decideAutopilotCamp(in) d, ok := decideAutopilotCamp(in)
@@ -227,8 +227,8 @@ func TestDecideAutopilotCamp_NightTriggerOnEventAnchored(t *testing.T) {
now := time.Now().UTC() now := time.Now().UTC()
stale := now.Add(-(nightCampWindow + time.Hour)) stale := now.Add(-(nightCampWindow + time.Hour))
in := autoCampInputs{ in := autoCampInputs{
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}}, Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
HPCur: 20, HPMax: 20, // healthy — only the night window should pitch HPCur: 20, HPMax: 20, // healthy — only the night window should pitch
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1}, Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
Now: now, Now: now,
EventAnchored: true, EventAnchored: true,
@@ -250,8 +250,8 @@ func TestDecideAutopilotCamp_NightSkippedOnLegacy(t *testing.T) {
now := time.Now().UTC() now := time.Now().UTC()
stale := now.Add(-24 * time.Hour) stale := now.Add(-24 * time.Hour)
in := autoCampInputs{ in := autoCampInputs{
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}}, Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
HPCur: 20, HPMax: 20, HPCur: 20, HPMax: 20,
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1}, Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
Now: now, Now: now,
EventAnchored: false, // legacy — only HP-low triggers EventAnchored: false, // legacy — only HP-low triggers
@@ -266,8 +266,8 @@ func TestDecideAutopilotCamp_NightFlagSetEvenOnHPLow(t *testing.T) {
now := time.Now().UTC() now := time.Now().UTC()
stale := now.Add(-(nightCampWindow + time.Hour)) stale := now.Add(-(nightCampWindow + time.Hour))
in := autoCampInputs{ in := autoCampInputs{
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}}, Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
HPCur: 5, HPMax: 20, // also low HPCur: 5, HPMax: 20, // also low
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1}, Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
Now: now, Now: now,
EventAnchored: true, EventAnchored: true,

View File

@@ -329,9 +329,9 @@ func (p *AdventurePlugin) tryAutoRun(e *Expedition, now time.Time) error {
// - expID: the expedition row id (for the EoD digest log fetch). // - expID: the expedition row id (for the EoD digest log fetch).
// - r: walk result, including reason + accumulated stream. // - r: walk result, including reason + accumulated stream.
// - camp: rendered camp block from maybeAutoCamp / pitchBossSafetyCamp, // - camp: rendered camp block from maybeAutoCamp / pitchBossSafetyCamp,
// or "" when no camp was pitched this tick. // or "" when no camp was pitched this tick.
// - dec: the camp decision; dec.Night is the trigger for the EoD // - dec: the camp decision; dec.Night is the trigger for the EoD
// digest variant. Zero-value when no pitch happened. // digest variant. Zero-value when no pitch happened.
// //
// Surface rules: // Surface rules:
// - stopFork / stopEnded / stopComplete → render the walk DM. These // - stopFork / stopEnded / stopComplete → render the walk DM. These

View File

@@ -192,14 +192,14 @@ type expeditionTrialResult struct {
// expeditionBalanceResult is the aggregated win-rate per cell. Mirrors // expeditionBalanceResult is the aggregated win-rate per cell. Mirrors
// classBalanceResult — same shape, different lens. // classBalanceResult — same shape, different lens.
type expeditionBalanceResult struct { type expeditionBalanceResult struct {
Profile expeditionBalanceProfile Profile expeditionBalanceProfile
Trials int Trials int
Completions int Completions int
Deaths int Deaths int
StarvedOuts int StarvedOuts int
MedianDays int MedianDays int
MedianThreatEnd int MedianThreatEnd int
AvgEncounters float64 AvgEncounters float64
AvgHPRemainingPct float64 // mean across survivors AvgHPRemainingPct float64 // mean across survivors
} }
@@ -655,11 +655,11 @@ func buildHarnessZoneEnemy(monster DnDMonsterTemplate, tier int) Combatant {
// expeditionHarness threads per-trial mutable state (RNG, expedition, // expeditionHarness threads per-trial mutable state (RNG, expedition,
// character HP carryover, encounter count) through the day loop. // character HP carryover, encounter count) through the day loop.
type expeditionHarness struct { type expeditionHarness struct {
exp *Expedition exp *Expedition
char *DnDCharacter char *DnDCharacter
rng *harnessRNG rng *harnessRNG
encounters int encounters int
rollsPerDay int // resolved from profile + default; never zero rollsPerDay int // resolved from profile + default; never zero
// Lever overrides for the Phase 2 sweep. Zero on both fields ⇒ // Lever overrides for the Phase 2 sweep. Zero on both fields ⇒
// live behavior (retreatThreatBump=5, /5 surprise-nick divisor). // live behavior (retreatThreatBump=5, /5 surprise-nick divisor).
retreatThreatBumpOverride int retreatThreatBumpOverride int
@@ -743,12 +743,12 @@ func runExpeditionBalanceTrial(p expeditionBalanceProfile, seed uint64) expediti
rolls = harnessHarvestRollsPerDay rolls = harnessHarvestRollsPerDay
} }
h := &expeditionHarness{ h := &expeditionHarness{
exp: exp, exp: exp,
char: char, char: char,
rng: newHarnessRNG(seed), rng: newHarnessRNG(seed),
rollsPerDay: rolls, rollsPerDay: rolls,
retreatThreatBumpOverride: p.RetreatThreatBumpOverride, retreatThreatBumpOverride: p.RetreatThreatBumpOverride,
surpriseNickDivisorOverride: p.SurpriseNickDivisorOverride, surpriseNickDivisorOverride: p.SurpriseNickDivisorOverride,
eliteInterruptThresholdOverride: p.EliteInterruptThresholdOverride, eliteInterruptThresholdOverride: p.EliteInterruptThresholdOverride,
threatDriftBaseOverride: p.ThreatDriftBaseOverride, threatDriftBaseOverride: p.ThreatDriftBaseOverride,
surpriseNickFloorOverride: p.SurpriseNickFloorOverride, surpriseNickFloorOverride: p.SurpriseNickFloorOverride,

View File

@@ -528,8 +528,8 @@ func TestExpeditionBalance_Phase2_LeverSweep(t *testing.T) {
// sweep walks two settings tighter (gentler) and one looser // sweep walks two settings tighter (gentler) and one looser
// (harsher) for each knob so the live point sits in the middle of // (harsher) for each knob so the live point sits in the middle of
// the grid and we can read the slope in both directions. // the grid and we can read the slope in both directions.
bumps := []int{2, 5, 10} // threat-bump per retreat bumps := []int{2, 5, 10} // threat-bump per retreat
divisors := []int{3, 5, 8, 12} // /N wounded-nick divisor; bigger = gentler divisors := []int{3, 5, 8, 12} // /N wounded-nick divisor; bigger = gentler
t.Logf("phase2 lever sweep — %d zones × %d bump × %d divisor × %d trials, Fighter @ tier centerline (rolls=4)", t.Logf("phase2 lever sweep — %d zones × %d bump × %d divisor × %d trials, Fighter @ tier centerline (rolls=4)",
len(zoneOrder), len(bumps), len(divisors), trialsPerCell) len(zoneOrder), len(bumps), len(divisors), trialsPerCell)
@@ -622,16 +622,16 @@ func TestExpeditionBalance_Phase2_LeverSweep(t *testing.T) {
// This sweep walks two of the global knobs called out in the plan-doc // This sweep walks two of the global knobs called out in the plan-doc
// "Phase 3 — global lever tuning" section: // "Phase 3 — global lever tuning" section:
// //
// eliteInterruptThreshold (live=19, total roll cutoff for Elite // eliteInterruptThreshold (live=19, total roll cutoff for Elite
// bracket) — directly controls how often fresh-entry elite fights // bracket) — directly controls how often fresh-entry elite fights
// trigger during the daytime harvest pipeline. Sweep {17, 19, 23}: // trigger during the daytime harvest pipeline. Sweep {17, 19, 23}:
// 17 = more elites (slope check below live), 19 = live baseline, // 17 = more elites (slope check below live), 19 = live baseline,
// 23 = elites only when roll+mod ≥ 23 (rare even at T5). // 23 = elites only when roll+mod ≥ 23 (rare even at T5).
// //
// threatDriftBase (live=3, daily threat clock drift before mood-mod) // threatDriftBase (live=3, daily threat clock drift before mood-mod)
// — slows the multi-day AC/init/supply-burn creep that compounds // — slows the multi-day AC/init/supply-burn creep that compounds
// over a 14-day expedition. Sweep {1, 3, 5}: 1 = nearly flat, 3 = // over a 14-day expedition. Sweep {1, 3, 5}: 1 = nearly flat, 3 =
// live, 5 = harsher. // live, 5 = harsher.
// //
// 3×3 = 9 combos × 10 zones × 200 trials/cell. Diagnostic-only — no // 3×3 = 9 combos × 10 zones × 200 trials/cell. Diagnostic-only — no
// gates beyond the Phase 1 wiring sanity. -short skips. // gates beyond the Phase 1 wiring sanity. -short skips.
@@ -736,19 +736,19 @@ func TestExpeditionBalance_Phase3_GlobalLeverSweep(t *testing.T) {
// Phase 3-B holds the Phase 3-A best cell (eliteInterruptThreshold=23, // Phase 3-B holds the Phase 3-A best cell (eliteInterruptThreshold=23,
// threatDriftBase=1) and walks two more global knobs: // threatDriftBase=1) and walks two more global knobs:
// //
// surpriseNickFloor (live=tier, i.e. 1..5 by zone tier) — the raw // surpriseNickFloor (live=tier, i.e. 1..5 by zone tier) — the raw
// surprise-round nick on a fresh-HP entry. Lower floor = less // surprise-round nick on a fresh-HP entry. Lower floor = less
// per-fight chip on T4/T5 standard fights, where the live tier=4-5 // per-fight chip on T4/T5 standard fights, where the live tier=4-5
// floor is the biggest single contributor to the wear-down curve // floor is the biggest single contributor to the wear-down curve
// that funnels survivors into starvation. Sweep {-1 (disable, =0), // that funnels survivors into starvation. Sweep {-1 (disable, =0),
// 1 (flat), 0-sentinel (live tier)}. // 1 (flat), 0-sentinel (live tier)}.
// //
// supplyBurnRatePct (live=100, %) — daily supply burn scalar. Lower // supplyBurnRatePct (live=100, %) — daily supply burn scalar. Lower
// = more days of margin. T4 and T5 burns are 3×/4× the T1 baseline; // = more days of margin. T4 and T5 burns are 3×/4× the T1 baseline;
// halving them ought to convert the starvation outs we saw in // halving them ought to convert the starvation outs we saw in
// Phase 3-A into actual completions if survivability is the only // Phase 3-A into actual completions if survivability is the only
// remaining blocker, or leave them dying in combat if it isn't. // remaining blocker, or leave them dying in combat if it isn't.
// Sweep {100 (live), 75, 50}. // Sweep {100 (live), 75, 50}.
// //
// 3×3 = 9 combos × 10 zones × 200 trials/cell, all on top of the // 3×3 = 9 combos × 10 zones × 200 trials/cell, all on top of the
// Phase 3-A best cell. Diagnostic-only — no gates beyond Phase 1 // Phase 3-A best cell. Diagnostic-only — no gates beyond Phase 1
@@ -925,14 +925,14 @@ func TestExpeditionBalance_Phase4A_OutlierDiagnostic(t *testing.T) {
killAttributed int // last fight of a died_combat trial killAttributed int // last fight of a died_combat trial
} }
type zoneAgg struct { type zoneAgg struct {
profile expeditionBalanceProfile profile expeditionBalanceProfile
completions int completions int
deaths int deaths int
starves int starves int
monsters map[string]*monsterStat monsters map[string]*monsterStat
dayHist [16]int // 1..14 + overflow bucket; index 0 unused dayHist [16]int // 1..14 + overflow bucket; index 0 unused
eliteFights int eliteFights int
stdFights int stdFights int
// for the trial currently in flight (mutated during traceFightStruct) // for the trial currently in flight (mutated during traceFightStruct)
lastFight harnessFightTrace lastFight harnessFightTrace
} }
@@ -1094,9 +1094,9 @@ func TestExpeditionBalance_Phase4A_OutlierDiagnostic(t *testing.T) {
// the lever has to be tier-wide. The plan doc names three candidate // the lever has to be tier-wide. The plan doc names three candidate
// moves: // moves:
// //
// 1. Gear-tier centerline remap (phase1TierCenterline) → axis L // 1. Gear-tier centerline remap (phase1TierCenterline) → axis L
// 2. Per-tier elite-bracket threshold (vs global) → axis E // 2. Per-tier elite-bracket threshold (vs global) → axis E
// 3. Ship burn=75 globally (Phase 3-B's high-burn cell) → axis B // 3. Ship burn=75 globally (Phase 3-B's high-burn cell) → axis B
// //
// This diagnostic does *not* pick the lever. It runs a one-axis-at-a- // This diagnostic does *not* pick the lever. It runs a one-axis-at-a-
// time sensitivity sweep on the six under-band zones and lets the // time sensitivity sweep on the six under-band zones and lets the
@@ -1106,9 +1106,9 @@ func TestExpeditionBalance_Phase4A_OutlierDiagnostic(t *testing.T) {
// means the lever is inert for that tier, monotone climb means it's // means the lever is inert for that tier, monotone climb means it's
// the lever, non-monotone means a confound. // the lever, non-monotone means a confound.
// //
// L: player level = {centerline-2, centerline, centerline+2} // L: player level = {centerline-2, centerline, centerline+2}
// E: elite threshold = {18, 23, 28} // E: elite threshold = {18, 23, 28}
// B: supply burn pct = {40, 50, 60} // B: supply burn pct = {40, 50, 60}
// //
// Per cell we log comp%/death%/starve% plus the elite-vs-standard // Per cell we log comp%/death%/starve% plus the elite-vs-standard
// fight share (so axis E's slope can be cross-checked against the // fight share (so axis E's slope can be cross-checked against the
@@ -1145,7 +1145,7 @@ func TestExpeditionBalance_Phase5A_TierWideSensitivity(t *testing.T) {
// Returns comp/death/starve counts plus elite/standard fight share // Returns comp/death/starve counts plus elite/standard fight share
// so the caller can attribute the slope across axis values. // so the caller can attribute the slope across axis values.
type cellOut struct { type cellOut struct {
comp, death, starve int comp, death, starve int
eliteFights, stdFights int eliteFights, stdFights int
} }
runCell := func(zid ZoneID, tier ZoneTier, level, elite, burn int) cellOut { runCell := func(zid ZoneID, tier ZoneTier, level, elite, burn int) cellOut {
@@ -1281,8 +1281,9 @@ func TestExpeditionBalance_Phase5A_TierWideSensitivity(t *testing.T) {
// what flat delta on top of the ladder lands T1-T5 in band. // what flat delta on top of the ladder lands T1-T5 in band.
// //
// Bands (gogobee_expedition_difficulty.md): // Bands (gogobee_expedition_difficulty.md):
// T1 80% (70-90%) T2 72% (62-82%) T3 65% (55-75%) //
// T4 55% (45-65%) T5 45% (35-55%) // T1 80% (70-90%) T2 72% (62-82%) T3 65% (55-75%)
// T4 55% (45-65%) T5 45% (35-55%)
// //
// 10 zones × 3 deltas × 200 trials = 6k trials; runs in ~1s. // 10 zones × 3 deltas × 200 trials = 6k trials; runs in ~1s.
// Diagnostic-only — picks the delta Phase 5-B ships in // Diagnostic-only — picks the delta Phase 5-B ships in

View File

@@ -171,8 +171,6 @@ func recordBotDefeat(userID id.UserID, game string) {
) )
} }
// redirectToGamesRoom returns the room ID for games-restricted redirect. // redirectToGamesRoom returns the room ID for games-restricted redirect.
func redirectToGamesRoom(sender id.UserID) string { func redirectToGamesRoom(sender id.UserID) string {
_ = sender _ = sender

View File

@@ -24,8 +24,8 @@ func BenchmarkComputeSignalFromRates_CrossPairSize(b *testing.B) {
baseRates := make([]float64, 260) baseRates := make([]float64, 260)
quoteRates := make([]float64, 260) quoteRates := make([]float64, 260)
for i := range baseRates { for i := range baseRates {
baseRates[i] = 0.90 + rand.Float64()*0.10 // EUR/USD ~0.90-1.00 baseRates[i] = 0.90 + rand.Float64()*0.10 // EUR/USD ~0.90-1.00
quoteRates[i] = 145 + rand.Float64()*10 // JPY/USD ~145-155 quoteRates[i] = 145 + rand.Float64()*10 // JPY/USD ~145-155
} }
// Compute cross-rates (simulating the join) // Compute cross-rates (simulating the join)

View File

@@ -20,13 +20,13 @@ func TestFxParseConvert(t *testing.T) {
{[]string{"1500", "EUR/USD"}, 1500, "EUR", "USD", false}, {[]string{"1500", "EUR/USD"}, 1500, "EUR", "USD", false},
{[]string{"1500", "JPY", "USD"}, 1500, "JPY", "USD", false}, {[]string{"1500", "JPY", "USD"}, 1500, "JPY", "USD", false},
{[]string{"USD", "EUR"}, 0, "", "", true}, // no amount {[]string{"USD", "EUR"}, 0, "", "", true}, // no amount
{[]string{"1500", "USD"}, 0, "", "", true}, // missing quote {[]string{"1500", "USD"}, 0, "", "", true}, // missing quote
{[]string{"1500", "USD", "USD"}, 0, "", "", true}, // same currency {[]string{"1500", "USD", "USD"}, 0, "", "", true}, // same currency
{[]string{"1500", "ZZZ", "EUR"}, 0, "", "", true}, // unknown currency {[]string{"1500", "ZZZ", "EUR"}, 0, "", "", true}, // unknown currency
{[]string{"1500", "USD", "EUR", "JPY"}, 0, "", "", true}, // too many currencies {[]string{"1500", "USD", "EUR", "JPY"}, 0, "", "", true}, // too many currencies
{[]string{"1500", "100", "USD", "EUR"}, 0, "", "", true}, // multiple amounts {[]string{"1500", "100", "USD", "EUR"}, 0, "", "", true}, // multiple amounts
{[]string{"-100", "USD", "EUR"}, 0, "", "", true}, // negative {[]string{"-100", "USD", "EUR"}, 0, "", "", true}, // negative
} }
for _, c := range cases { for _, c := range cases {

View File

@@ -23,45 +23,45 @@ import (
// cityTimezones maps common city names to IANA timezone strings. // cityTimezones maps common city names to IANA timezone strings.
var cityTimezones = map[string]string{ var cityTimezones = map[string]string{
"new york": "America/New_York", "new york": "America/New_York",
"nyc": "America/New_York", "nyc": "America/New_York",
"los angeles": "America/Los_Angeles", "los angeles": "America/Los_Angeles",
"la": "America/Los_Angeles", "la": "America/Los_Angeles",
"chicago": "America/Chicago", "chicago": "America/Chicago",
"denver": "America/Denver", "denver": "America/Denver",
"london": "Europe/London", "london": "Europe/London",
"paris": "Europe/Paris", "paris": "Europe/Paris",
"berlin": "Europe/Berlin", "berlin": "Europe/Berlin",
"tokyo": "Asia/Tokyo", "tokyo": "Asia/Tokyo",
"sydney": "Australia/Sydney", "sydney": "Australia/Sydney",
"dubai": "Asia/Dubai", "dubai": "Asia/Dubai",
"moscow": "Europe/Moscow", "moscow": "Europe/Moscow",
"mumbai": "Asia/Kolkata", "mumbai": "Asia/Kolkata",
"beijing": "Asia/Shanghai", "beijing": "Asia/Shanghai",
"shanghai": "Asia/Shanghai", "shanghai": "Asia/Shanghai",
"singapore": "Asia/Singapore", "singapore": "Asia/Singapore",
"hong kong": "Asia/Hong_Kong", "hong kong": "Asia/Hong_Kong",
"seoul": "Asia/Seoul", "seoul": "Asia/Seoul",
"toronto": "America/Toronto", "toronto": "America/Toronto",
"vancouver": "America/Vancouver", "vancouver": "America/Vancouver",
"sao paulo": "America/Sao_Paulo", "sao paulo": "America/Sao_Paulo",
"mexico city": "America/Mexico_City", "mexico city": "America/Mexico_City",
"cairo": "Africa/Cairo", "cairo": "Africa/Cairo",
"johannesburg": "Africa/Johannesburg", "johannesburg": "Africa/Johannesburg",
"auckland": "Pacific/Auckland", "auckland": "Pacific/Auckland",
"honolulu": "Pacific/Honolulu", "honolulu": "Pacific/Honolulu",
"hawaii": "Pacific/Honolulu", "hawaii": "Pacific/Honolulu",
"anchorage": "America/Anchorage", "anchorage": "America/Anchorage",
"amsterdam": "Europe/Amsterdam", "amsterdam": "Europe/Amsterdam",
"rome": "Europe/Rome", "rome": "Europe/Rome",
"madrid": "Europe/Madrid", "madrid": "Europe/Madrid",
"lisbon": "Europe/Lisbon", "lisbon": "Europe/Lisbon",
"bangkok": "Asia/Bangkok", "bangkok": "Asia/Bangkok",
"jakarta": "Asia/Jakarta", "jakarta": "Asia/Jakarta",
"manila": "Asia/Manila", "manila": "Asia/Manila",
"taipei": "Asia/Taipei", "taipei": "Asia/Taipei",
"utc": "UTC", "utc": "UTC",
"gmt": "UTC", "gmt": "UTC",
} }
var eightBallResponses = []string{ var eightBallResponses = []string{
@@ -382,10 +382,10 @@ func scrapeHLTB(gameName string) (string, error) {
"rangeYear": map[string]interface{}{"min": "", "max": ""}, "rangeYear": map[string]interface{}{"min": "", "max": ""},
"modifier": "", "modifier": "",
}, },
"users": map[string]interface{}{"sortCategory": "postcount"}, "users": map[string]interface{}{"sortCategory": "postcount"},
"lists": map[string]interface{}{"sortCategory": "follows"}, "lists": map[string]interface{}{"sortCategory": "follows"},
"filter": "", "filter": "",
"sort": 0, "sort": 0,
"randomizer": 0, "randomizer": 0,
}, },
"useCache": true, "useCache": true,

View File

@@ -19,18 +19,18 @@ import (
// releaseListResponse is the top-level RAWG API response for game releases. // releaseListResponse is the top-level RAWG API response for game releases.
type releaseListResponse struct { type releaseListResponse struct {
Count int `json:"count"` Count int `json:"count"`
Results []releaseEntry `json:"results"` Results []releaseEntry `json:"results"`
} }
// releaseEntry is a game entry from the RAWG releases API. // releaseEntry is a game entry from the RAWG releases API.
type releaseEntry struct { type releaseEntry struct {
Name string `json:"name"` Name string `json:"name"`
Slug string `json:"slug"` Slug string `json:"slug"`
Released string `json:"released"` Released string `json:"released"`
Rating float64 `json:"rating"` Rating float64 `json:"rating"`
Metacritic int `json:"metacritic"` Metacritic int `json:"metacritic"`
Platforms []struct { Platforms []struct {
Platform struct { Platform struct {
Name string `json:"name"` Name string `json:"name"`
} `json:"platform"` } `json:"platform"`

View File

@@ -973,4 +973,3 @@ func (p *HangmanPlugin) recordHangmanScore(userID id.UserID, earned float64) {
string(userID), earned, earned, string(userID), earned, earned,
) )
} }

View File

@@ -8,7 +8,10 @@ import (
// ── formatNumber (xp.go) ─────────────────────────────────────────────────── // ── formatNumber (xp.go) ───────────────────────────────────────────────────
func TestFormatNumber_Small(t *testing.T) { func TestFormatNumber_Small(t *testing.T) {
cases := []struct{ in int; want string }{ cases := []struct {
in int
want string
}{
{0, "0"}, {0, "0"},
{1, "1"}, {1, "1"},
{999, "999"}, {999, "999"},
@@ -21,7 +24,10 @@ func TestFormatNumber_Small(t *testing.T) {
} }
func TestFormatNumber_WithCommas(t *testing.T) { func TestFormatNumber_WithCommas(t *testing.T) {
cases := []struct{ in int; want string }{ cases := []struct {
in int
want string
}{
{1000, "1,000"}, {1000, "1,000"},
{10000, "10,000"}, {10000, "10,000"},
{1000000, "1,000,000"}, {1000000, "1,000,000"},

View File

@@ -1249,7 +1249,6 @@ func (p *HoldemPlugin) removePlayer(game *HoldemGame, uid id.UserID) {
} }
} }
func (p *HoldemPlugin) getNPCBalance() int64 { func (p *HoldemPlugin) getNPCBalance() int64 {
d := db.Get() d := db.Get()
var balance int64 var balance int64

View File

@@ -74,9 +74,9 @@ func (g *HoldemGame) firstToActPostflop() int {
// ActionResult describes what happened after an action. // ActionResult describes what happened after an action.
type ActionResult struct { type ActionResult struct {
Announcement string Announcement string
HandOver bool // only 1 player remains HandOver bool // only 1 player remains
StreetOver bool // street betting is complete StreetOver bool // street betting is complete
AllAllIn bool // all remaining players are all-in AllAllIn bool // all remaining players are all-in
} }
// doFold processes a fold action. // doFold processes a fold action.
@@ -381,4 +381,3 @@ func (g *HoldemGame) returnUncalledBet() (name string, amount int64) {
return "", 0 return "", 0
} }

View File

@@ -355,9 +355,9 @@ type CFRData struct {
func packInfoSetKey(street Street, posIP bool, eqBucket, sprBkt, boardTex int, history string) InfoSetKey { func packInfoSetKey(street Street, posIP bool, eqBucket, sprBkt, boardTex int, history string) InfoSetKey {
var key uint64 var key uint64
key |= uint64(street) & 0x7 // bits 0-2 key |= uint64(street) & 0x7 // bits 0-2
if posIP { if posIP {
key |= 1 << 3 // bit 3 key |= 1 << 3 // bit 3
} }
key |= (uint64(eqBucket) & 0xF) << 4 // bits 4-7 (4 bits for 12 buckets) key |= (uint64(eqBucket) & 0xF) << 4 // bits 4-7 (4 bits for 12 buckets)
key |= (uint64(sprBkt) & 0x7) << 8 // bits 8-10 key |= (uint64(sprBkt) & 0x7) << 8 // bits 8-10

View File

@@ -72,13 +72,13 @@ type holdemTipContext struct {
EquityVsShove EquityResult EquityVsShove EquityResult
// Derived fields for the rules engine. // Derived fields for the rules engine.
HoleRanks [2]int // 0-12, for preflop classification HoleRanks [2]int // 0-12, for preflop classification
HoleSuited bool HoleSuited bool
PreflopTier string // "premium", "strong", "speculative", "playable", "trash" PreflopTier string // "premium", "strong", "speculative", "playable", "trash"
BoardPaired bool BoardPaired bool
BoardMaxSuit int // largest same-suit count on the board BoardMaxSuit int // largest same-suit count on the board
BoardStraightRisk int // 0-4, rough straight-possibility score BoardStraightRisk int // 0-4, rough straight-possibility score
BoardHighRank int // highest rank on board, -1 preflop BoardHighRank int // highest rank on board, -1 preflop
} }
// tipSnapshot holds game data captured under lock for async tip generation. // tipSnapshot holds game data captured under lock for async tip generation.

View File

@@ -137,11 +137,11 @@ func TestHasStraight(t *testing.T) {
ranks uint16 ranks uint16
want bool want bool
}{ }{
{"A-high straight (T-J-Q-K-A)", 0x1F00, true}, // bits 8-12 {"A-high straight (T-J-Q-K-A)", 0x1F00, true}, // bits 8-12
{"low straight (A-2-3-4-5)", 0x100F, true}, // bits 0,1,2,3,12 {"low straight (A-2-3-4-5)", 0x100F, true}, // bits 0,1,2,3,12
{"5-6-7-8-9", 0x00F8, true}, // bits 3-7 {"5-6-7-8-9", 0x00F8, true}, // bits 3-7
{"four consecutive", 0x000F, false}, // bits 0-3 {"four consecutive", 0x000F, false}, // bits 0-3
{"scattered", 0x1111, false}, // bits 0,4,8,12 {"scattered", 0x1111, false}, // bits 0,4,8,12
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
@@ -254,14 +254,14 @@ func TestBuildTipUserPrompt_NoDrawWhenMadeHand(t *testing.T) {
func TestBuildTipUserPrompt_PotOddsWhenFacingBet(t *testing.T) { func TestBuildTipUserPrompt_PotOddsWhenFacingBet(t *testing.T) {
ctx := holdemTipContext{ ctx := holdemTipContext{
Hole: [2]string{"A♠", "K♠"}, Hole: [2]string{"A♠", "K♠"},
Community: "2♣ 7♦ J♠", Community: "2♣ 7♦ J♠",
Street: StreetFlop, Street: StreetFlop,
Position: "CO", Position: "CO",
NumActive: 4, NumActive: 4,
Equity: EquityResult{Win: 0.45, Tie: 0.02, Loss: 0.53}, Equity: EquityResult{Win: 0.45, Tie: 0.02, Loss: 0.53},
ToCall: 50, ToCall: 50,
PotOddsPct: 25.0, PotOddsPct: 25.0,
HandCategory: "High Card", HandCategory: "High Card",
} }

View File

@@ -168,11 +168,11 @@ func (p *HolidaysPlugin) Prefetch() error {
slog.Error("holidays: aladhan fetch failed", "err", err) slog.Error("holidays: aladhan fetch failed", "err", err)
} else if islamicInfo != "" { } else if islamicInfo != "" {
allHolidays = append(allHolidays, Holiday{ allHolidays = append(allHolidays, Holiday{
Name: "Islamic Date", Name: "Islamic Date",
Description: islamicInfo, Description: islamicInfo,
Country: "International", Country: "International",
Type: "islamic-calendar", Type: "islamic-calendar",
Date: dateStr, Date: dateStr,
}) })
} }
@@ -471,9 +471,9 @@ func (p *HolidaysPlugin) fetchCalendarific(date time.Time, country string) ([]Ho
// Types to exclude — too regional to be interesting // Types to exclude — too regional to be interesting
skipTypes := map[string]bool{ skipTypes := map[string]bool{
"Local holiday": true, "Local holiday": true,
"Common local holiday": true, "Common local holiday": true,
"Local observance": true, "Local observance": true,
"Clock change/Daylight Saving Time": true, "Clock change/Daylight Saving Time": true,
} }

View File

@@ -57,8 +57,8 @@ type queueItem struct {
// LLMPassivePlugin classifies messages using Ollama and reacts accordingly. // LLMPassivePlugin classifies messages using Ollama and reacts accordingly.
type LLMPassivePlugin struct { type LLMPassivePlugin struct {
Base Base
xp *XPPlugin xp *XPPlugin
dict *dreamclient.Client dict *dreamclient.Client
ollamaHost string ollamaHost string
ollamaModel string ollamaModel string
sampleRate float64 sampleRate float64

View File

@@ -40,15 +40,15 @@ const (
// MilkCartonPlugin generates milk carton style "missing person" images. // MilkCartonPlugin generates milk carton style "missing person" images.
type MilkCartonPlugin struct { type MilkCartonPlugin struct {
Base Base
rateLimiter *RateLimitsPlugin rateLimiter *RateLimitsPlugin
thresholdDays int thresholdDays int
maxDays int maxDays int
minMessages int minMessages int
excludeUsers map[string]bool excludeUsers map[string]bool
regularFont font.Face regularFont font.Face
boldFont font.Face boldFont font.Face
smallFont font.Face smallFont font.Face
headerFont font.Face headerFont font.Face
} }
// NewMilkCartonPlugin creates a new milk carton plugin. // NewMilkCartonPlugin creates a new milk carton plugin.

View File

@@ -53,13 +53,13 @@ type modConfig struct {
RepeatSimilarityThreshold float64 RepeatSimilarityThreshold float64
// Mention flood // Mention flood
MentionMax int MentionMax int
MentionFloodCount int MentionFloodCount int
MentionFloodWindowSeconds int MentionFloodWindowSeconds int
// Link rate (new members) // Link rate (new members)
LinkRateNewMember int LinkRateNewMember int
LinkRateWindowSeconds int LinkRateWindowSeconds int
// Invite flooding // Invite flooding
InviteMaxPerHour int InviteMaxPerHour int
@@ -123,16 +123,16 @@ func loadModConfig() modConfig {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
var dmTemplates = map[string]string{ var dmTemplates = map[string]string{
"strike1_word": "Hey — that word isn't something we allow in this community. Your message has been removed. This has been noted. Please keep it in mind going forward.", "strike1_word": "Hey — that word isn't something we allow in this community. Your message has been removed. This has been noted. Please keep it in mind going forward.",
"strike1_flood": "Hey — looks like you might have gotten a bit trigger-happy there. Slow it down a little. Your messages have been removed and this has been noted.", "strike1_flood": "Hey — looks like you might have gotten a bit trigger-happy there. Slow it down a little. Your messages have been removed and this has been noted.",
"strike1_wall": "Hey — that message was way too long for this room. It's been removed and noted. Please keep it shorter.", "strike1_wall": "Hey — that message was way too long for this room. It's been removed and noted. Please keep it shorter.",
"strike1_repeat": "Hey — repeating the same message isn't cool. Your messages have been removed and this has been noted.", "strike1_repeat": "Hey — repeating the same message isn't cool. Your messages have been removed and this has been noted.",
"strike1_mention": "Hey — too many mentions in a short time. Please don't mass-ping. This has been noted.", "strike1_mention": "Hey — too many mentions in a short time. Please don't mass-ping. This has been noted.",
"strike1_link": "Hey — slow down on the links. New members have a limit. This has been noted.", "strike1_link": "Hey — slow down on the links. New members have a limit. This has been noted.",
"strike1_invite": "Hey — you've been sending too many invites. This has been flagged and noted.", "strike1_invite": "Hey — you've been sending too many invites. This has been flagged and noted.",
"strike2": "This is the second time we've had to step in. You've been muted for %s. When you're back, please keep the community guidelines in mind. This is your last warning before a permanent ban.", "strike2": "This is the second time we've had to step in. You've been muted for %s. When you're back, please keep the community guidelines in mind. This is your last warning before a permanent ban.",
"strike3": "This is the third strike. You're being removed from the community permanently. Take care.", "strike3": "This is the third strike. You're being removed from the community permanently. Take care.",
"manual_warn": "A community admin has issued you a formal warning: %s. Please keep this in mind.", "manual_warn": "A community admin has issued you a formal warning: %s. Please keep this in mind.",
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -316,12 +316,12 @@ func petStateFromAdvChar(c *AdventureCharacter) PetState {
// payment, autopay toggle, weekly tick, rate refresh), so the helper takes // payment, autopay toggle, weekly tick, rate refresh), so the helper takes
// the whole struct rather than splitting per-field. // the whole struct rather than splitting per-field.
type HouseState struct { type HouseState struct {
Tier int Tier int
LoanBalance int LoanBalance int
LoanFrozen bool LoanFrozen bool
MissedPayments int MissedPayments int
Autopay bool Autopay bool
CurrentRate float64 CurrentRate float64
} }
// HasHouse mirrors AdventureCharacter.HasHouse: a player counts as housed // HasHouse mirrors AdventureCharacter.HasHouse: a player counts as housed
@@ -830,9 +830,9 @@ func upsertPlayerMetaLifecycleState(userID id.UserID, s LifecycleState) error {
// loadLifecycleState returns lifecycle state from player_meta. // loadLifecycleState returns lifecycle state from player_meta.
func loadLifecycleState(userID id.UserID) (LifecycleState, error) { func loadLifecycleState(userID id.UserID) (LifecycleState, error) {
var ( var (
s LifecycleState s LifecycleState
streakDecayed, actionTaken, holidayTaken int streakDecayed, actionTaken, holidayTaken int
createdAt, lastActiveAt sql.NullTime createdAt, lastActiveAt sql.NullTime
) )
err := db.Get().QueryRow( err := db.Get().QueryRow(
`SELECT current_streak, best_streak, last_action_date, streak_decayed, `SELECT current_streak, best_streak, last_action_date, streak_decayed,
@@ -1038,9 +1038,9 @@ func deathStateFromAdvChar(c *AdventureCharacter) DeathState {
// `saveAdvCharacter` alongside the L5e death state, since these fields // `saveAdvCharacter` alongside the L5e death state, since these fields
// either rarely mutate or already mutate at every save site. // either rarely mutate or already mutate at every save site.
type MiscState struct { type MiscState struct {
Title string Title string
TreasuresLocked bool TreasuresLocked bool
CraftsSucceeded int CraftsSucceeded int
} }
func upsertPlayerMetaMiscState(userID id.UserID, s MiscState) error { func upsertPlayerMetaMiscState(userID id.UserID, s MiscState) error {

View File

@@ -259,8 +259,8 @@ func TestScenario_MagicItemPlumbing(t *testing.T) {
// in the slot the fix intended. Slots are baked into the generated // in the slot the fix intended. Slots are baked into the generated
// data file by the importer's classifier; the lookup is a fixed table. // data file by the importer's classifier; the lookup is a fixed table.
type slotCheck struct { type slotCheck struct {
id string id string
wantSlot DnDSlot wantSlot DnDSlot
mustNotBeRingForSubstring string // sanity vs word-boundary regressions mustNotBeRingForSubstring string // sanity vs word-boundary regressions
} }
checks := []slotCheck{ checks := []slotCheck{
@@ -399,8 +399,8 @@ func TestScenario_SpellJargonRegression(t *testing.T) {
bannedRegexes := []*regexp.Regexp{ bannedRegexes := []*regexp.Regexp{
regexp.MustCompile(`\bd(4|6|8|10|12|20|100)\b`), regexp.MustCompile(`\bd(4|6|8|10|12|20|100)\b`),
regexp.MustCompile(`\b\d+d\d+\b`), regexp.MustCompile(`\b\d+d\d+\b`),
regexp.MustCompile(`\bWhatever [a-z]`), // placeholder signature regexp.MustCompile(`\bWhatever [a-z]`), // placeholder signature
regexp.MustCompile(`(?:\.\.\.|…)\s*$`), // trailing ellipsis regexp.MustCompile(`(?:\.\.\.|…)\s*$`), // trailing ellipsis
regexp.MustCompile(`\b[a-z]{1,3}(?:\.\.\.|…)\s*$`), // truncated word regexp.MustCompile(`\b[a-z]{1,3}(?:\.\.\.|…)\s*$`), // truncated word
regexp.MustCompile(`(?i)\bno larger than in any\b`), regexp.MustCompile(`(?i)\bno larger than in any\b`),
} }

View File

@@ -90,8 +90,8 @@ func NewSpaceInviterPlugin(client *mautrix.Client) *SpaceInviterPlugin {
} }
} }
func (p *SpaceInviterPlugin) Name() string { return "space_inviter" } func (p *SpaceInviterPlugin) Name() string { return "space_inviter" }
func (p *SpaceInviterPlugin) Commands() []CommandDef { return nil } func (p *SpaceInviterPlugin) Commands() []CommandDef { return nil }
func (p *SpaceInviterPlugin) OnReaction(ReactionContext) error { return nil } func (p *SpaceInviterPlugin) OnReaction(ReactionContext) error { return nil }
func (p *SpaceInviterPlugin) Init() error { func (p *SpaceInviterPlugin) Init() error {

View File

@@ -17,24 +17,24 @@ import (
// finnhubQuote is the response from the Finnhub quote endpoint. // finnhubQuote is the response from the Finnhub quote endpoint.
type finnhubQuote struct { type finnhubQuote struct {
Current float64 `json:"c"` // Current price Current float64 `json:"c"` // Current price
Change float64 `json:"d"` // Change Change float64 `json:"d"` // Change
ChangePct float64 `json:"dp"` // Percent change ChangePct float64 `json:"dp"` // Percent change
High float64 `json:"h"` // High price of the day High float64 `json:"h"` // High price of the day
Low float64 `json:"l"` // Low price of the day Low float64 `json:"l"` // Low price of the day
Open float64 `json:"o"` // Open price of the day Open float64 `json:"o"` // Open price of the day
PrevClose float64 `json:"pc"` // Previous close price PrevClose float64 `json:"pc"` // Previous close price
Timestamp int64 `json:"t"` // Timestamp Timestamp int64 `json:"t"` // Timestamp
} }
// finnhubProfile is the response from the Finnhub company profile endpoint. // finnhubProfile is the response from the Finnhub company profile endpoint.
type finnhubProfile struct { type finnhubProfile struct {
Name string `json:"name"` Name string `json:"name"`
Ticker string `json:"ticker"` Ticker string `json:"ticker"`
Exchange string `json:"exchange"` Exchange string `json:"exchange"`
Industry string `json:"finnhubIndustry"` Industry string `json:"finnhubIndustry"`
MarketCap float64 `json:"marketCapitalization"` // in millions MarketCap float64 `json:"marketCapitalization"` // in millions
Currency string `json:"currency"` Currency string `json:"currency"`
} }
// stockCacheEntry holds cached stock data. // stockCacheEntry holds cached stock data.

View File

@@ -68,15 +68,15 @@ type openTDBResult struct {
} }
type activeSession struct { type activeSession struct {
RoomID id.RoomID RoomID id.RoomID
QuestionEventID id.EventID QuestionEventID id.EventID
Question string Question string
CorrectAnswer string CorrectAnswer string
AllAnswers []string AllAnswers []string
CorrectIndex int CorrectIndex int
StartedAt time.Time StartedAt time.Time
Difficulty string Difficulty string
Category string Category string
} }
// TriviaPlugin handles trivia game sessions. // TriviaPlugin handles trivia game sessions.

View File

@@ -2039,5 +2039,3 @@ func (p *UnoPlugin) recordGame(game *unoGame, result string, potBefore float64)
time.Now().UTC().Format("2006-01-02 15:04:05"), time.Now().UTC().Format("2006-01-02 15:04:05"),
) )
} }

View File

@@ -39,28 +39,28 @@ type unoMultiPlayer struct {
} }
type unoMultiGame struct { type unoMultiGame struct {
id string id string
roomID id.RoomID // games room roomID id.RoomID // games room
ante float64 ante float64
players []*unoMultiPlayer // in turn order (includes bot) players []*unoMultiPlayer // in turn order (includes bot)
currentIdx int currentIdx int
direction int // +1 or -1 direction int // +1 or -1
drawPile []unoCard drawPile []unoCard
discardTop unoCard discardTop unoCard
topColor unoColor topColor unoColor
phase unoMultiPhase phase unoMultiPhase
drawnCard *unoCard // card drawn this turn drawnCard *unoCard // card drawn this turn
pendingCard *unoCard // wild waiting for color pendingCard *unoCard // wild waiting for color
// Wild Draw Four challenge state // Wild Draw Four challenge state
wd4Player *unoMultiPlayer // who played the WD4 wd4Player *unoMultiPlayer // who played the WD4
wd4Victim *unoMultiPlayer // who can challenge wd4Victim *unoMultiPlayer // who can challenge
wd4PrevColor unoColor // color before the wild was played wd4PrevColor unoColor // color before the wild was played
turns int turns int
turnID int // monotonic, used to invalidate stale timers turnID int // monotonic, used to invalidate stale timers
startedAt time.Time startedAt time.Time
done bool done bool
bookDown bool bookDown bool
// No Mercy mode // No Mercy mode
noMercy bool noMercy bool

Some files were not shown because too many files have changed in this diff Show More