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.
//
// 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):
// 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,
// 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
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) {
runner, err := plugin.NewSimRunner(dataDir)
if err != nil {

View File

@@ -129,7 +129,7 @@ var (
reSpaces = regexp.MustCompile(`\s+`)
reOrphanMustMake = regexp.MustCompile(`(?i)\s+must\s+(?:make|succeed on|attempt)([.,;])`)
reOrphanTrailingAnd = regexp.MustCompile(`(?i),?\s+and\.`)
descScrub = []*regexp.Regexp{
descScrub = []*regexp.Regexp{
// Range / distance jargon.
regexp.MustCompile(`(?i)\s*\bwithin range\b`),
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.
func TestStripNameParenthetical(t *testing.T) {
cases := map[string]string{
"Giant Rat (Diseased)": "Giant Rat",
"Deep Gnome (Svirfneblin)": "Deep Gnome",
"Giant Rat (Diseased)": "Giant Rat",
"Deep Gnome (Svirfneblin)": "Deep Gnome",
"Stone of Good Luck (Luckstone)": "Stone of Good Luck",
"Ordinary Name": "Ordinary Name",
"": "",
"Ordinary Name": "Ordinary Name",
"": "",
}
for in, want := range cases {
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
// fields are ignored; the vendored JSON keeps the full payload.
type open5eSpell struct {
Slug string `json:"slug"`
Name string `json:"name"`
Desc string `json:"desc"`
HigherLevel string `json:"higher_level"`
Range string `json:"range"`
Material string `json:"material"`
CanBeCastAsRitual bool `json:"can_be_cast_as_ritual"`
Duration string `json:"duration"`
RequiresConcentration bool `json:"requires_concentration"`
CastingTime string `json:"casting_time"`
LevelInt int `json:"level_int"`
School string `json:"school"`
Slug string `json:"slug"`
Name string `json:"name"`
Desc string `json:"desc"`
HigherLevel string `json:"higher_level"`
Range string `json:"range"`
Material string `json:"material"`
CanBeCastAsRitual bool `json:"can_be_cast_as_ritual"`
Duration string `json:"duration"`
RequiresConcentration bool `json:"requires_concentration"`
CastingTime string `json:"casting_time"`
LevelInt int `json:"level_int"`
School string `json:"school"`
// SpellLists is the structured class list, but the SRD dump leaves it
// incomplete (no paladin entries at all). DndClass is the free-text
// "Druid, Wizard" field and is the more complete source — mapClasses

View File

@@ -21,10 +21,10 @@ import (
type AdventurePlugin struct {
Base
euro *EuroPlugin
xp *XPPlugin
achievements *AchievementsPlugin
mu sync.Mutex
euro *EuroPlugin
xp *XPPlugin
achievements *AchievementsPlugin
mu sync.Mutex
dmToPlayer map[id.RoomID]id.UserID
pending sync.Map // userID string -> *advPendingInteraction
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)
craftResults sync.Map // userID string -> []CraftResult (pending craft results for narrative)
treasureUndo sync.Map // userID string -> *advTreasureUndoToken (10-min auto-swap reversal)
morningHour int
summaryHour int
morningHour int
summaryHour int
// stopCh is closed by Stop() to signal all background tickers
// (morning/summary/midnight/event/rival/robbie/hospital/mortgage/
@@ -1558,4 +1558,3 @@ func (p *AdventurePlugin) handleBoostCmd(ctx MessageContext) error {
advSetBoost(true)
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
EquipBroken []EquipmentSlot
NearDeath bool
StreakBonus int
StreakBonus int
CombatLog *CombatResult
// MasteryCrossings records equipment slots whose ActionsUsed crossed a
// 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() {
var (
uid, zoneID string
visitedJSON string
totalRooms int
abandoned, bossDefeated int
completedAt *time.Time
lastAction time.Time
uid, zoneID string
visitedJSON string
totalRooms int
abandoned, bossDefeated int
completedAt *time.Time
lastAction time.Time
)
if err := rows.Scan(&uid, &zoneID, &visitedJSON, &totalRooms,
&abandoned, &bossDefeated, &completedAt, &lastAction); err != nil {

View File

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

View File

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

View File

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

View File

@@ -318,15 +318,15 @@ func renderBabysitSummary(char *AdventureCharacter, logs []babysitLogEntry) stri
// ── Babysit Log CRUD ────────────────────────────────────────────────────────
type babysitLogEntry struct {
ID int64
UserID id.UserID
LogDate string
Activity string
Outcome string
GoldEarned int
XPGained int
ItemsDropped string
RivalRefused string
ID int64
UserID id.UserID
LogDate string
Activity string
Outcome string
GoldEarned int
XPGained int
ItemsDropped string
RivalRefused 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() }()
char := &AdventureCharacter{
UserID: id.UserID("@accum:example"),
PetType: "dog",
PetName: "Rex",
PetLevel: 1, // needs 10 XP = 1000 centi-XP to L2
PetXP: 900,
UserID: id.UserID("@accum:example"),
PetType: "dog",
PetName: "Rex",
PetLevel: 1, // needs 10 XP = 1000 centi-XP to L2
PetXP: 900,
BabysitActive: true,
}
expires := time.Now().UTC().Add(48 * time.Hour)

View File

@@ -24,32 +24,32 @@ var allSlots = []EquipmentSlot{SlotWeapon, SlotArmor, SlotHelmet, SlotBoots, Slo
// ── Core Types ───────────────────────────────────────────────────────────────
type AdventureCharacter struct {
UserID id.UserID
DisplayName string
CombatLevel int
MiningSkill int
ForagingSkill int
FishingSkill int // v2
CombatXP int
MiningXP int
ForagingXP int
FishingXP int // v2
Alive bool
DeadUntil *time.Time
ActionTakenToday bool
HolidayActionTaken bool
CombatActionsUsed int
HarvestActionsUsed int
ArenaWins int // v2
ArenaLosses int // v2
InvasionScore int // v2
Title string // v2
CurrentStreak int
BestStreak int
LastActionDate string
GrudgeLocation string
CreatedAt time.Time
LastActiveAt time.Time
UserID id.UserID
DisplayName string
CombatLevel int
MiningSkill int
ForagingSkill int
FishingSkill int // v2
CombatXP int
MiningXP int
ForagingXP int
FishingXP int // v2
Alive bool
DeadUntil *time.Time
ActionTakenToday bool
HolidayActionTaken bool
CombatActionsUsed int
HarvestActionsUsed int
ArenaWins int // v2
ArenaLosses int // v2
InvasionScore int // v2
Title string // v2
CurrentStreak int
BestStreak int
LastActionDate string
GrudgeLocation string
CreatedAt time.Time
LastActiveAt time.Time
DeathReprieveLast *time.Time
MasterworkDropsReceived int
RivalPool int
@@ -78,27 +78,27 @@ type AdventureCharacter struct {
HouseAutopay bool
HouseCurrentRate float64
// Pets
PetType string
PetName string
PetXP int
PetLevel int
PetArmorTier int
PetChasedAway bool
PetReactivated bool
PetArrived bool
MistyEncounterCount int
MistyDonatedCount int
ThomAnimalLineFired bool
PetType string
PetName string
PetXP int
PetLevel int
PetArmorTier int
PetChasedAway bool
PetReactivated bool
PetArrived bool
MistyEncounterCount int
MistyDonatedCount int
ThomAnimalLineFired bool
PetSupplyShopUnlocked bool
PetLevel10Date string
PetMorningDefense bool
AutoBabysit bool
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
StreakDecayed bool
CraftsSucceeded int
DeathSource string // "adventure" | "arena" | "" (legacy/unknown — treated as adventure)
DeathLocation string // human-readable location of last death; cleared on revive is not required
PetLevel10Date string
PetMorningDefense bool
AutoBabysit bool
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
StreakDecayed bool
CraftsSucceeded int
DeathSource string // "adventure" | "arena" | "" (legacy/unknown — treated as adventure)
DeathLocation string // human-readable location of last death; cleared on revive is not required
}
type AdvEquipment struct {

View File

@@ -17,26 +17,26 @@ import (
type ConsumableEffect string
const (
EffectHeal ConsumableEffect = "heal"
EffectDefBoost ConsumableEffect = "def_boost"
EffectAtkBoost ConsumableEffect = "atk_boost"
EffectWard ConsumableEffect = "ward"
EffectHeal ConsumableEffect = "heal"
EffectDefBoost ConsumableEffect = "def_boost"
EffectAtkBoost ConsumableEffect = "atk_boost"
EffectWard ConsumableEffect = "ward"
EffectSpeedBoost ConsumableEffect = "speed_boost"
EffectCritBoost ConsumableEffect = "crit_boost"
EffectFlatDmg ConsumableEffect = "flat_dmg"
EffectSpore ConsumableEffect = "spore"
EffectReflect ConsumableEffect = "reflect"
EffectAutoCrit ConsumableEffect = "auto_crit"
EffectCritBoost ConsumableEffect = "crit_boost"
EffectFlatDmg ConsumableEffect = "flat_dmg"
EffectSpore ConsumableEffect = "spore"
EffectReflect ConsumableEffect = "reflect"
EffectAutoCrit ConsumableEffect = "auto_crit"
)
type ConsumableDef struct {
Name string
Effect ConsumableEffect
Value float64 // meaning depends on effect
Tier int
Buyable bool
Price int64
Slot string // "offensive" or "defensive"
Name string
Effect ConsumableEffect
Value float64 // meaning depends on effect
Tier int
Buyable bool
Price int64
Slot string // "offensive" or "defensive"
}
// 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.",
"This could have been avoided with regular visits. _slides the cost estimate across the counter without breaking eye contact_",
}

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" +
"The way cats do.",
}
// Fires randomly in the morning DM. Cat has left something.
// Results in a defense boost for the day.
// 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) {
cases := []struct {
name string
used map[EquipmentSlot]int
wantCrossed int
wantMaxed int
wantBonus float64
wantAnyProgress bool
name string
used map[EquipmentSlot]int
wantCrossed int
wantMaxed int
wantBonus float64
wantAnyProgress bool
}{
{"empty", nil, 0, 0, 0, false},
{"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) {
cases := []struct {
name string
foraging int
craftsSucceeded int
wantEmpty bool
wantContainsAny []string // any of these (OR)
wantNotContains []string
name string
foraging int
craftsSucceeded int
wantEmpty bool
wantContainsAny []string // any of these (OR)
wantNotContains []string
}{
{"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"}},
@@ -297,7 +297,7 @@ func TestCraftingReminderWeekday_Spread(t *testing.T) {
func TestCraftingReminderWeekday_StableWithinWeek(t *testing.T) {
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
if craftingReminderWeekday(uid, mon) != craftingReminderWeekday(uid, sun) {
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
p.pending.Store(string(ctx.Sender), &advPendingInteraction{
Type: "hospital_pay",
Type: "hospital_pay",
Data: &advPendingHospitalPay{
Cost: afterInsurance,
Discounted: haggleOutcome == haggleWin,
@@ -277,7 +277,7 @@ func (p *AdventurePlugin) resolveHospitalPay(ctx MessageContext, interaction *ad
gr := gamesRoom()
if gr != "" {
name, _ := loadDisplayName(char.UserID)
p.SendMessage(gr, fmt.Sprintf(hospitalDitchAnnounce, name))
p.SendMessage(gr, fmt.Sprintf(hospitalDitchAnnounce, name))
}
return nil
}

View File

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

View File

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

View File

@@ -16,11 +16,11 @@ import (
// ── NPC Constants ──────────────────────────────────────────────────────────
const (
mistyCost = 100
arinaCost = 5000
npcCooldownDays = 7
mistyCost = 100
arinaCost = 5000
npcCooldownDays = 7
npcEncounterChance = 0.075 // 7.5%
npcBuffDuration = 7 * 24 * time.Hour
npcBuffDuration = 7 * 24 * time.Hour
// Arena effect chances per round
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.
type npcArenaResult struct {
Text string
EnemyDmg int // damage dealt to enemy
PlayerDmg int // damage dealt to player
SniperKill bool // enemy instant kill
CondRepair int // equipment condition repair amount
EnemyDmg int // damage dealt to enemy
PlayerDmg int // damage dealt to player
SniperKill bool // enemy instant kill
CondRepair int // equipment condition repair amount
}
func npcCheckArenaEffects(char *AdventureCharacter, monsterName string) *npcArenaResult {

View File

@@ -729,12 +729,12 @@ type AdvPlayerDaySummary struct {
ForagingSkill int
FishingSkill int
Activity string
Location string
Outcome string
LootValue int64
IsDead bool
DeadUntil string
Activity string
Location string
Outcome string
LootValue int64
IsDead bool
DeadUntil string
// DeadUntilHours is the integer hours-from-now until revival, used by
// the standout-death template's {hours} placeholder. Computed when
// 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
// rounded {hours} count.
DeadUntilDuration string
IsResting bool
SummaryLine string
HolidayActions int // 0 = not holiday or no action; 1 = took one; 2 = took both
DeathSource string
DeathLocation string
IsResting bool
SummaryLine string
HolidayActions int // 0 = not holiday or no action; 1 = took one; 2 = took both
DeathSource string
DeathLocation string
}
// 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")
}
// Standout
if bestPlayer != nil && bestPlayer.LootValue > 0 {
pool := SummaryStandoutGood
@@ -984,8 +983,8 @@ func renderAdvLeaderboard(chars []AdvLeaderboardEntry) string {
// Sort by score
type entry struct {
Name string
Score int
Name string
Score int
Levels string
Streak int
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -29,51 +29,51 @@ const (
thLinkmasterMinMsgs = 50
// Temporal
thNightOwlPct = 40
thNightOwlMinMsgs = 100
thEarlyBirdPct = 40
thNightOwlPct = 40
thNightOwlMinMsgs = 100
thEarlyBirdPct = 40
thEarlyBirdMinMsgs = 100
// Emotional (LLM-gated)
thEmotionalMinClassified = 100
thCheerleaderPosPct = 50
thPhilosopherNeutPct = 40
thPhilosopherQPct = 10
thPhilosopherAvgWords = 8
thAgitatorNegPct = 30
thAgitatorMinMsgs = 200
thWildcardStdDev = 0.5
thWildcardMinClassified = 150
thHypeMachineExclPct = 20
thHypeMachinePosPct = 60
thEmotionalMinClassified = 100
thCheerleaderPosPct = 50
thPhilosopherNeutPct = 40
thPhilosopherQPct = 10
thPhilosopherAvgWords = 8
thAgitatorNegPct = 30
thAgitatorMinMsgs = 200
thWildcardStdDev = 0.5
thWildcardMinClassified = 150
thHypeMachineExclPct = 20
thHypeMachinePosPct = 60
// Economy
thBrokeSpiritedMaxBalance = 100.0
thDegenerateMinLosses = 10
// Games
thSharkWinRate = 55
thSharkMinGames = 15
thWordleMinPuzzles = 10
thArenaChampMinTier = 4
thArenaChampWinRate = 50
thArenaCowardMinRuns = 3
thArenaCowardMaxAvgTier = 2
thTriviaNerdMinCorrect = 10
thSharkWinRate = 55
thSharkMinGames = 15
thWordleMinPuzzles = 10
thArenaChampMinTier = 4
thArenaChampWinRate = 50
thArenaCowardMinRuns = 3
thArenaCowardMaxAvgTier = 2
thTriviaNerdMinCorrect = 10
// Adventure
thAdvMinDays = 10
thAdvDiverseMinDays = 15
thAdvDiverseMinTypes = 3
thAdvDiverseMaxShare = 40
thAdvMinDays = 10
thAdvDiverseMinDays = 15
thAdvDiverseMinTypes = 3
thAdvDiverseMaxShare = 40
thGearheadMinMasterwork = 3
// Communication (vocabulary)
thWordsmithMinFancyWords = 10
// Social
thPatronMinRepGiven = 5
thPatronRatioMultiplier = 2
thPatronMinRepGiven = 5
thPatronRatioMultiplier = 2
// Display
maxDisplayArchetypes = 6
@@ -83,13 +83,13 @@ const (
var archetypeFlavors = map[string]string{
// Communication
"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.",
"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.",
"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.",
"Wordsmith": "Uses words most people have to look up. The thesaurus fears them.",
"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.",
"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.",
"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.",
"Wordsmith": "Uses words most people have to look up. The thesaurus fears them.",
// Temporal
"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.",
// Economy
"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.",
"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.",
"Broke But Spirited": "Down but not out. The pot fears them anyway.",
// Games
@@ -221,16 +221,16 @@ type userData struct {
userID string
// user_stats
totalMsgs int
totalWords int
totalLinks int
totalImages int
totalMsgs int
totalWords int
totalLinks int
totalImages int
totalQuestions int
totalExcl int
totalEmojis int
nightMsgs int
morningMsgs int
fancyWords int
totalExcl int
totalEmojis int
nightMsgs int
morningMsgs int
fancyWords int
// sentiment_stats
sentPositive int
@@ -242,7 +242,7 @@ type userData struct {
sentVariance float64
// economy
balance float64
balance float64
gamblingLosses int
recentGaming bool
@@ -251,7 +251,7 @@ type userData struct {
hmPlayed, hmWon int
holdemPlayed int
holdemNetPositive bool
unoSoloPlayed, unoSoloWon int
unoSoloPlayed, unoSoloWon int
unoMultiPlayed, unoMultiWon int
wordlePlayed int
triviaCorrect int
@@ -263,13 +263,13 @@ type userData struct {
arenaAvgCashTier float64
// adventure
advDays int
advActivities map[string]int // activity_type -> count
advDays int
advActivities map[string]int // activity_type -> count
masterworkCount int
// social
repGiven int
repReceived int
repGiven int
repReceived int
reactionsGiven int
}

View File

@@ -14,327 +14,327 @@ package plugin
func buildTunedBestiarySRD() 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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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_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_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)."},
"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"},
"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"},
"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)."},
"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)."},
"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"},
"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"},
"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)."},
"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."},
"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"},
"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_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_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_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_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"},
"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"},
"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)."},
"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)."},
"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_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"},
"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"},
"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."},
"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)."},
"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"},
"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)"},
"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_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"},
"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"},
"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"},
"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},
"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"},
"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"},
"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"},
"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_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_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"},
"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_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"},
"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},
"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"},
"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"},
"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"},
"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)."},
"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_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_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"},
"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"},
"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"},
"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"},
"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"},
"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)."},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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_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"},
"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"},
"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"},
"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"},
"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"},
"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_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)."},
"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"},
"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"},
"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"},
"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)."},
"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"},
"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"},
"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)."},
"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)."},
"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"},
"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"},
"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"},
"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"},
"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)."},
"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"},
"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"},
"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)."},
"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"},
"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"},
"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)."},
"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)."},
"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"},
"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_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"},
"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"},
"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"},
"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)."},
"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)."},
"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"},
"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)."},
"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)"},
"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)."},
"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"},
"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"},
"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_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_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_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"},
"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"},
"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"},
"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)."},
"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)"},
"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_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"},
"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"},
"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"},
"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"},
"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"},
"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_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)."},
"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"},
"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)"},
"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)"},
"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"},
"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"},
"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"},
"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"},
"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_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_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"},
"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)."},
"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"},
"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"},
"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)."},
"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"},
"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"},
"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_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_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_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."},
"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"},
"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"},
"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)"},
"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"},
"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"},
"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."},
"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"},
"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"},
"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"},
"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"},
"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"},
"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_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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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)"},
"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"},
"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)."},
"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"},
"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)."},
"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."},
"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)"},
"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"},
"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"},
"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."},
"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"},
"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."},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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)"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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."},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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},
"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"},
"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"},
"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"},
"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_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."},
"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"},
"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_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)"},
"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."},
"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"},
"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."},
"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"},
"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"},
"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."},
"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"},
"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."},
"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"},
"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"},
"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},
"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"},
"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},
"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"},
"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"},
"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"},
"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)"},
"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"},
"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"},
"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)"},
"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_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"},
"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_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"},
"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"},
"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},
"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},
"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_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"},
"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)"},
"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"},
"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_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)"},
"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_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"},
"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)."},
"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"},
"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)."},
"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."},
"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)"},
"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"},
"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"},
"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."},
"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"},
"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."},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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)"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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."},
"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"},
"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"},
"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"},
"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"},
"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"},
"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"},
"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},
"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"},
"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"},
"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"},
"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_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."},
"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"},
"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_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)"},
"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."},
"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"},
"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."},
"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"},
"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"},
"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."},
"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"},
"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."},
"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"},
"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"},
"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},
"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"},
"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},
"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"},
"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"},
"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"},
"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)"},
"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"},
"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"},
"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)"},
"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_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"},
"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_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"},
"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"},
"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},
"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},
"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_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"},
"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)"},
"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"},
"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_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)"},
"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_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"},
}
}

View File

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

View File

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

View File

@@ -46,13 +46,13 @@ type CombatStats struct {
}
type CombatModifiers struct {
DamageBonus float64 // additive: 0 = neutral, 0.25 = +25% damage. Applied as (1 + DamageBonus).
DamageReduct float64 // multiplicative damage-taken reduction, 1.0 = neutral
DeathSave bool // Sovereign reprieve — survive one lethal hit
PetAttackProc float64
PetAttackDmg int
PetDeflectProc float64
PetWhiffProc float64 // pet distracts enemy → guaranteed miss
DamageBonus float64 // additive: 0 = neutral, 0.25 = +25% damage. Applied as (1 + DamageBonus).
DamageReduct float64 // multiplicative damage-taken reduction, 1.0 = neutral
DeathSave bool // Sovereign reprieve — survive one lethal hit
PetAttackProc float64
PetAttackDmg int
PetDeflectProc float64
PetWhiffProc float64 // pet distracts enemy → guaranteed miss
// Spiritual Weapon — separate channel from the pet so the spectral mace
// gets its own narration when a cleric without a companion casts it.
// 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
}

View File

@@ -218,8 +218,8 @@ func DeriveDungeonMonsterStats(loc *AdvLocation) (CombatStats, CombatModifiers)
death := loc.BaseDeathPct // 8 to 60
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
Attack: int(t*1.5) + int(death*0.04), // T1=1, T5=9 — tuned to dnd HP pools (players have 1380 HP, not 100+)
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+)
Defense: 2 + int(t*1.2),
Speed: 4 + int(t*1.5),
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
// new "fairly breezy with some death" target.
cases := []tc{
{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
{5, 3, advDungeons[2], 0.08, 0.0}, // T3: low risk
{7, 4, advDungeons[3], 0.15, 0.0}, // T4: still mostly safe
{9, 5, advDungeons[4], 0.25, 0.0}, // T5: occasional bad luck
{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
{5, 3, advDungeons[2], 0.08, 0.0}, // T3: low risk
{7, 4, advDungeons[3], 0.15, 0.0}, // T4: still mostly safe
{9, 5, advDungeons[4], 0.25, 0.0}, // T5: occasional bad luck
}
for _, c := range cases {

View File

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

View File

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

View File

@@ -24,9 +24,9 @@ import (
// ── Ability definitions ──────────────────────────────────────────────────────
type DnDAbility struct {
ID string
Name string
Class DnDClass
ID string
Name string
Class DnDClass
// Subclass: if non-empty, ability is only available when the player's
// subclass matches. Phase 10 adds the first such ability (Berserker
// 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) {
cases := []struct {
in string
id string
ok bool
in string
id string
ok bool
}{
{"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.
func TestOrcRage_ThresholdExact(t *testing.T) {
cases := []struct {
hp, maxHP int
hp, maxHP int
shouldFire bool
}{
{50, 100, false}, // exactly 50% — does NOT fire (strict <)
{49, 100, true}, // just under
{8, 16, false}, // exactly 50% even MaxHP
{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.
// 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.
cases = []struct {
hp, maxHP int
hp, maxHP int
shouldFire bool
}{
{50, 100, false},
{49, 100, true},
{8, 16, false},
{7, 16, true},
{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)
{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)
{8, 15, false}, // 8*2=16, not < 15
}
for _, c := range cases {

View File

@@ -16,8 +16,8 @@ type DnDMonsterTemplate struct {
CR float32 // challenge rating (display only for now)
HP int
AC int
Attack int // engine's "Attack" stat (raw damage value)
AttackBonus int // d20 to-hit bonus
Attack int // engine's "Attack" stat (raw damage value)
AttackBonus int // d20 to-hit bonus
Speed int
BlockRate float64
Ability *MonsterAbility
@@ -176,10 +176,10 @@ var _ = func() bool {
"flameskull": {
ID: "flameskull", Name: "Flameskull",
CR: 4, HP: 40, AC: 13, Attack: 7, AttackBonus: 5, Speed: 15,
BlockRate: 0.05,
Ability: &MonsterAbility{Name: "Fireball", Phase: "decisive", ProcChance: 0.55, Effect: "aoe_fire"},
XPValue: 1100,
Notes: "Elite. Fireball 8d6 DEX DC 13. Rejuvenation in 1h unless holy water used.",
BlockRate: 0.05,
Ability: &MonsterAbility{Name: "Fireball", Phase: "decisive", ProcChance: 0.55, Effect: "aoe_fire"},
XPValue: 1100,
Notes: "Elite. Fireball 8d6 DEX DC 13. Rejuvenation in 1h unless holy water used.",
FireAttacker: true,
},
"boss_grol_unbroken": {
@@ -385,37 +385,37 @@ var _ = func() bool {
"magmin": {
ID: "magmin", Name: "Magmin",
CR: 0.5, HP: 9, AC: 14, Attack: 2, AttackBonus: 3, Speed: 10,
BlockRate: 0.05,
Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1.00, Effect: "death_aoe"},
XPValue: 100,
Notes: "Death Burst: 2d6 fire in 10 ft on death. Ignite flammable objects on touch.",
BlockRate: 0.05,
Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1.00, Effect: "death_aoe"},
XPValue: 100,
Notes: "Death Burst: 2d6 fire in 10 ft on death. Ignite flammable objects on touch.",
FireAttacker: true,
},
"azer": {
ID: "azer", Name: "Azer",
CR: 2, HP: 39, AC: 17, Attack: 4, AttackBonus: 5, Speed: 12,
BlockRate: 0.20,
Ability: &MonsterAbility{Name: "Fire Aura", Phase: "any", ProcChance: 0.50, Effect: "retaliate"},
XPValue: 450,
Notes: "1d10 fire to melee attackers; immune to fire; weapons deal heated bonus damage.",
BlockRate: 0.20,
Ability: &MonsterAbility{Name: "Fire Aura", Phase: "any", ProcChance: 0.50, Effect: "retaliate"},
XPValue: 450,
Notes: "1d10 fire to melee attackers; immune to fire; weapons deal heated bonus damage.",
FireAttacker: true,
},
"salamander": {
ID: "salamander", Name: "Salamander",
CR: 5, HP: 90, AC: 15, Attack: 8, AttackBonus: 7, Speed: 12,
BlockRate: 0.10,
Ability: &MonsterAbility{Name: "Constrict", Phase: "any", ProcChance: 0.40, Effect: "stun"},
XPValue: 1800,
Notes: "Constrict grapple + 2d6 fire/turn; spear multiattack; fire aura.",
BlockRate: 0.10,
Ability: &MonsterAbility{Name: "Constrict", Phase: "any", ProcChance: 0.40, Effect: "stun"},
XPValue: 1800,
Notes: "Constrict grapple + 2d6 fire/turn; spear multiattack; fire aura.",
FireAttacker: true,
},
"fire_elemental": {
ID: "fire_elemental", Name: "Fire Elemental",
CR: 5, HP: 102, AC: 13, Attack: 8, AttackBonus: 6, Speed: 14,
BlockRate: 0.05,
Ability: &MonsterAbility{Name: "Fire Form", Phase: "any", ProcChance: 0.50, Effect: "retaliate"},
XPValue: 1800,
Notes: "1d10 contact fire damage; immune to fire; vulnerable to water/cold.",
BlockRate: 0.05,
Ability: &MonsterAbility{Name: "Fire Form", Phase: "any", ProcChance: 0.50, Effect: "retaliate"},
XPValue: 1800,
Notes: "1d10 contact fire damage; immune to fire; vulnerable to water/cold.",
FireAttacker: true,
},
"helmed_horror": {
@@ -429,10 +429,10 @@ var _ = func() bool {
"boss_emberlord_thyrak": {
ID: "boss_emberlord_thyrak", Name: "Emberlord Thyrak",
CR: 9, HP: 178, AC: 18, Attack: 14, AttackBonus: 8, Speed: 11,
BlockRate: 0.20,
Ability: &MonsterAbility{Name: "Forge Breath", Phase: "any", ProcChance: 0.30, Effect: "aoe"},
XPValue: 5000,
Notes: "Underforge boss. Phase 2 below 50% HP — spawns 2 Fire Elementals; breath recharge 46.",
BlockRate: 0.20,
Ability: &MonsterAbility{Name: "Forge Breath", Phase: "any", ProcChance: 0.30, Effect: "aoe"},
XPValue: 5000,
Notes: "Underforge boss. Phase 2 below 50% HP — spawns 2 Fire Elementals; breath recharge 46.",
FireAttacker: true,
},
}
@@ -594,10 +594,10 @@ var _ = func() bool {
"young_red_dragon": {
ID: "young_red_dragon", Name: "Young Red Dragon",
CR: 10, HP: 178, AC: 18, Attack: 16, AttackBonus: 10, Speed: 16,
BlockRate: 0.10,
Ability: &MonsterAbility{Name: "Fire Breath", Phase: "any", ProcChance: 0.30, Effect: "aoe_fire"},
XPValue: 5900,
Notes: "Elite. Fire Breath 16d6 DEX DC 21; Multiattack; Frightful Presence WIS DC 16.",
BlockRate: 0.10,
Ability: &MonsterAbility{Name: "Fire Breath", Phase: "any", ProcChance: 0.30, Effect: "aoe_fire"},
XPValue: 5900,
Notes: "Elite. Fire Breath 16d6 DEX DC 21; Multiattack; Frightful Presence WIS DC 16.",
FireAttacker: true,
},
"boss_infernax": {
@@ -612,10 +612,10 @@ var _ = func() bool {
// 460/AC21/42 over-corrected → ~40%; this Pass 3 405/AC20/42
// lands leaders in band.)
CR: 24, HP: 405, AC: 20, Attack: 38, AttackBonus: 11, Speed: 18,
BlockRate: 0.15,
Ability: &MonsterAbility{Name: "Frightful Presence", Phase: "opening", ProcChance: 0.40, Effect: "stun"},
XPValue: 62000,
Notes: "Dragon's Lair boss. Ancient Red Dragon. Legendary Actions; Lair Actions; phase 2 below 50% HP — fire ignores resistance.",
BlockRate: 0.15,
Ability: &MonsterAbility{Name: "Frightful Presence", Phase: "opening", ProcChance: 0.40, Effect: "stun"},
XPValue: 62000,
Notes: "Dragon's Lair boss. Ancient Red Dragon. Legendary Actions; Lair Actions; phase 2 below 50% HP — fire ignores resistance.",
FireAttacker: true,
},
// ── Abyss Portal ─────────────────────────────────────────────────
@@ -670,10 +670,10 @@ var _ = func() bool {
// 360/AC20/45 over-buffed → 19-40% leaders; this is the Pass 2
// correction back down toward band.)
CR: 19, HP: 300, AC: 20, Attack: 31, AttackBonus: 11, Speed: 14,
BlockRate: 0.20,
Ability: &MonsterAbility{Name: "Lightning Discharge", Phase: "decisive", ProcChance: 0.40, Effect: "aoe"},
XPValue: 22000,
Notes: "Abyss Portal boss. Balor. Fire Aura; Death Throes on death; Demonic Resilience; Legendary Resistance 3/combat; phase 2 below 40% HP — Huge size, advantage on attacks.",
BlockRate: 0.20,
Ability: &MonsterAbility{Name: "Lightning Discharge", Phase: "decisive", ProcChance: 0.40, Effect: "aoe"},
XPValue: 22000,
Notes: "Abyss Portal boss. Balor. Fire Aura; Death Throes on death; Demonic Resilience; Legendary Resistance 3/combat; phase 2 below 40% HP — Huge size, advantage on attacks.",
FireAttacker: true,
},
}
@@ -700,13 +700,13 @@ func dndBestiaryByCR(maxCR float32) []DnDMonsterTemplate {
// named monsters in the existing combat pipeline.
func (m DnDMonsterTemplate) toCombatStats() (CombatStats, CombatModifiers) {
stats := CombatStats{
MaxHP: m.HP,
Attack: m.Attack,
Defense: 0,
Speed: m.Speed,
BlockRate: m.BlockRate,
AC: m.AC,
AttackBonus: m.AttackBonus,
MaxHP: m.HP,
Attack: m.Attack,
Defense: 0,
Speed: m.Speed,
BlockRate: m.BlockRate,
AC: m.AC,
AttackBonus: m.AttackBonus,
FireAttacker: m.FireAttacker,
}
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.
type entry struct {
id int64
tier int
name string
id int64
tier int
name string
}
var heals []entry
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
// data lands.
const (
dndArenaACBase = 10
dndArenaACBase = 10
dndArenaACPerThreat = 0.25 // ThreatLevel 0..30 → AC bump 0..7
dndDungeonACBase = 9
dndDungeonACBase = 9
// Dungeon AC scales linearly with tier: T1=10, T5=14
)
// Monster attack bonus. Counters player AC growth. T1 monster +5, T5 +9.
const (
dndArenaAtkBase = 4
dndArenaAtkBase = 4
dndArenaAtkPerThreat = 0.30
dndDungeonAtkBase = 4
dndDungeonAtkBase = 4
// 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
// harness's measurement matches what live players experience.
const (
phase5BACBonus = 3
phase5BAttackBonus = 3
phase5BACBonus = 3
phase5BAttackBonus = 3
phase5BWeaponMagicBonus = 3
)
@@ -335,7 +335,7 @@ func autoBuildCharacter(userID id.UserID, char *AdventureCharacter) *DnDCharacte
Class: sug.Class,
Level: level,
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,
AutoMigrated: true,
CreatedAt: time.Now().UTC(),

View File

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

View File

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

View File

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

View File

@@ -8,18 +8,18 @@ import (
func TestResolveCombatInterrupt_Brackets(t *testing.T) {
cases := []struct {
name string
roll int
threat int
tier int
class DnDClass
zone ZoneID
want CombatInterruptKind
name string
roll int
threat int
tier int
class DnDClass
zone ZoneID
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 upper", 13, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptNoise}, // 13+1=14
{"standard", 14, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 15
{"noise upper", 13, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptNoise}, // 13+1=14
{"standard", 14, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 15
{"standard upper", 17, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 18
// Phase 5-B: elite bracket moved from 19+ to 23+. At totals
// 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).
{"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
{"patrol", 20, 0, 2, ClassFighter, ZoneGoblinWarrens, InterruptPatrol}, // 22
{"elite high threat", 18, 80, 3, ClassFighter, ZoneGoblinWarrens, InterruptElite}, // 18+3+2=23 → Elite
{"patrol", 20, 0, 2, ClassFighter, ZoneGoblinWarrens, InterruptPatrol}, // 22
{"elite high threat", 18, 80, 3, ClassFighter, ZoneGoblinWarrens, InterruptElite}, // 18+3+2=23 → Elite
{"threat bumps band", 13, 60, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard},
{"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 {
@@ -59,14 +59,14 @@ func TestResolveCombatInterrupt_ThreatModifier(t *testing.T) {
func TestMonsterKillTags_GatesKnownMonsters(t *testing.T) {
cases := map[string][]string{
"worg": {"worg", "beast"},
"owlbear": {"owlbear", "beast"},
"dire_wolf": {"beast"},
"vampire_spawn": {"vampire_spawn"},
"young_red_dragon": {"drake"},
"boss_belaxath": {"balor", "demon", "portal_boss"},
"boss_thornmother": {"thornmother"},
"goblin_sneak": nil, // goblins gate nothing in §3
"worg": {"worg", "beast"},
"owlbear": {"owlbear", "beast"},
"dire_wolf": {"beast"},
"vampire_spawn": {"vampire_spawn"},
"young_red_dragon": {"drake"},
"boss_belaxath": {"balor", "demon", "portal_boss"},
"boss_thornmother": {"thornmother"},
"goblin_sneak": nil, // goblins gate nothing in §3
}
for id, want := range cases {
got := monsterKillTags(id)

View File

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

View File

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

View File

@@ -38,9 +38,9 @@ func TestWanderingOutcome_BucketBoundaries(t *testing.T) {
func TestResolveWanderingCheck_RoughCampBumpsRoll(t *testing.T) {
exp := &Expedition{
ZoneID: ZoneGoblinWarrens,
ZoneID: ZoneGoblinWarrens,
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).
nc := resolveWanderingCheck(exp, ClassFighter, func() int { return 8 })
@@ -54,9 +54,9 @@ func TestResolveWanderingCheck_RoughCampBumpsRoll(t *testing.T) {
func TestResolveWanderingCheck_FortifiedCampReducesRoll(t *testing.T) {
exp := &Expedition{
ZoneID: ZoneGoblinWarrens,
ZoneID: ZoneGoblinWarrens,
ThreatLevel: 0,
Camp: &CampState{Active: true, Type: CampTypeFortified},
Camp: &CampState{Active: true, Type: CampTypeFortified},
}
// roll 14 → -4 fortified → total 10 → Passage.
nc := resolveWanderingCheck(exp, ClassFighter, func() int { return 14 })
@@ -83,9 +83,9 @@ func TestResolveWanderingCheck_ThreatModAbove30(t *testing.T) {
func TestResolveWanderingCheck_RangerWildernessDiscount(t *testing.T) {
exp := &Expedition{
ZoneID: ZoneForestShadows,
ZoneID: ZoneForestShadows,
ThreatLevel: 0,
Camp: &CampState{Active: true, Type: CampTypeStandard},
Camp: &CampState{Active: true, Type: CampTypeStandard},
}
nc := resolveWanderingCheck(exp, ClassRanger, func() int { return 12 })
if nc.ClassMod != -2 {
@@ -124,7 +124,7 @@ func TestProcessNightCheck_LogsAndBumpsThreatOnPassage(t *testing.T) {
}
nc := NightCheck{
Outcome: NightOutcomePassage, Roll: 8, Total: 8,
Summary: "Signs of passage near camp; no encounter.",
Summary: "Signs of passage near camp; no encounter.",
ThreatBumped: true,
}
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,
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"},
},
},
@@ -87,7 +87,7 @@ var regionsByZone = map[ZoneID][]ExpeditionRegion{
{
ID: "dragons_lair_infernax_chamber", ZoneID: ZoneDragonsLair, Order: 4,
Name: "Infernax's Chamber", BaseCampSite: false,
RegionBoss: "Infernax (Zone Boss)", IsZoneBoss: true,
RegionBoss: "Infernax (Zone Boss)", IsZoneBoss: true,
EnemySubset: []string{"young_red_dragon"},
},
},
@@ -113,7 +113,7 @@ var regionsByZone = map[ZoneID][]ExpeditionRegion{
{
ID: "abyss_the_tear", ZoneID: ZoneAbyssPortal, Order: 4,
Name: "The Tear", BaseCampSite: false,
RegionBoss: "Belaxath (Zone Boss)", IsZoneBoss: true,
RegionBoss: "Belaxath (Zone Boss)", IsZoneBoss: true,
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
// where HarshMod is 1×) — the dungeon is actively starving you out.
// - otherwise, harshActive applies HarshMod (zone-tier scaled).
//
// phase5BDailyBurnRatePct is the shipped daily-burn multiplier from
// 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

View File

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

View File

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

View File

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

View File

@@ -39,14 +39,14 @@ const (
// currentBurn() (already wired); ShortRest disabled is checked by the
// rest plumbing once expedition combat is wired.
type ThreatBandInfo struct {
Band ThreatBand
Label string
Perception int // enemy passive perception bonus
AC int // enemy AC bonus
InitAdv bool // enemy initiative advantage
TrapsArm bool // re-arm cleared traps each day
SupplyMult float32 // multiplier on daily supply burn
NoShortRest bool // siege-mode only
Band ThreatBand
Label string
Perception int // enemy passive perception bonus
AC int // enemy AC bonus
InitAdv bool // enemy initiative advantage
TrapsArm bool // re-arm cleared traps each day
SupplyMult float32 // multiplier on daily supply burn
NoShortRest bool // siege-mode only
}
// 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.
func TestDnDFlavorPoolsNonEmpty(t *testing.T) {
pools := map[string][]string{
"CombatStart": flavor.CombatStart,
"CombatVictory": flavor.CombatVictory,
"BossDeath": flavor.BossDeath,
"PlayerDeath": flavor.PlayerDeath,
"ZoneComplete": flavor.ZoneComplete,
"Nat20": flavor.Nat20,
"Nat1": flavor.Nat1,
"LevelUp": flavor.LevelUp,
"ItemFound": flavor.ItemFound,
"RestShort": flavor.RestShort,
"RestLong": flavor.RestLong,
"HomeLongRest": flavor.HomeLongRest,
"MistyGreeting": flavor.MistyGreeting,
"CombatStart": flavor.CombatStart,
"CombatVictory": flavor.CombatVictory,
"BossDeath": flavor.BossDeath,
"PlayerDeath": flavor.PlayerDeath,
"ZoneComplete": flavor.ZoneComplete,
"Nat20": flavor.Nat20,
"Nat1": flavor.Nat1,
"LevelUp": flavor.LevelUp,
"ItemFound": flavor.ItemFound,
"RestShort": flavor.RestShort,
"RestLong": flavor.RestLong,
"HomeLongRest": flavor.HomeLongRest,
"MistyGreeting": flavor.MistyGreeting,
"MistyInsightSuccess": flavor.MistyInsightSuccess,
"MistySkillFail": flavor.MistySkillFail,
"ArinaGreeting": flavor.ArinaGreeting,
"ArinaArcanaSuccess": flavor.ArinaArcanaSuccess,
"ArinaSkillFail": flavor.ArinaSkillFail,
"ExpeditionStart": flavor.ExpeditionStart,
"MistySkillFail": flavor.MistySkillFail,
"ArinaGreeting": flavor.ArinaGreeting,
"ArinaArcanaSuccess": flavor.ArinaArcanaSuccess,
"ArinaSkillFail": flavor.ArinaSkillFail,
"ExpeditionStart": flavor.ExpeditionStart,
}
for name, pool := range pools {
if len(pool) == 0 {
@@ -47,18 +47,18 @@ func TestDnDFlavorPoolsNonEmpty(t *testing.T) {
// string when its underlying pool has entries.
func TestDnDFlavorHelpersReturnNonEmpty(t *testing.T) {
cases := map[string]func() string{
"dndCombatOpeningLine": dndCombatOpeningLine,
"dndZoneCompleteLine": dndZoneCompleteLine,
"dndNat20Line": dndNat20Line,
"dndNat1Line": dndNat1Line,
"dndLevelUpFlavorLine": dndLevelUpFlavorLine,
"dndItemFoundLine": dndItemFoundLine,
"dndRestShortFlavorLine": dndRestShortFlavorLine,
"dndCombatClosingLine_Win": func() string { return dndCombatClosingLine(true, false) },
"dndCombatClosingLine_Boss": func() string { return dndCombatClosingLine(true, true) },
"dndCombatOpeningLine": dndCombatOpeningLine,
"dndZoneCompleteLine": dndZoneCompleteLine,
"dndNat20Line": dndNat20Line,
"dndNat1Line": dndNat1Line,
"dndLevelUpFlavorLine": dndLevelUpFlavorLine,
"dndItemFoundLine": dndItemFoundLine,
"dndRestShortFlavorLine": dndRestShortFlavorLine,
"dndCombatClosingLine_Win": func() string { return dndCombatClosingLine(true, false) },
"dndCombatClosingLine_Boss": func() string { return dndCombatClosingLine(true, true) },
"dndCombatClosingLine_Death": func() string { return dndCombatClosingLine(false, false) },
"dndRestLongFlavorLine_Home": func() string { return dndRestLongFlavorLine(true) },
"dndRestLongFlavorLine_Inn": func() string { return dndRestLongFlavorLine(false) },
"dndRestLongFlavorLine_Home": func() string { return dndRestLongFlavorLine(true) },
"dndRestLongFlavorLine_Inn": func() string { return dndRestLongFlavorLine(false) },
}
for name, fn := range cases {
// 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).
//
// 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.
// Cleric's Divine Favor reuses HealItem, the under-50%-HP heal trigger.
//
// Rogue's Sneak Attack reuses AutoCritFirst (the consumable Crystal Berry
// field) — the engine already implements first-hit-auto-crit semantics.
// Cleric's Divine Favor reuses HealItem, the under-50%-HP heal trigger.
//
// This means:
// - 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) {
cases := []struct {
in string
count, sides, mod int
ok bool
in string
count, sides, mod int
ok bool
}{
{"d20", 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":
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 {
c, err := LoadDnDCharacter(ctx.Sender)
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.
@@ -125,7 +125,7 @@ func (p *AdventurePlugin) dndSetupStatus(ctx MessageContext) error {
b.WriteString("**Step 1 — Race.** Pick one of:\n")
b.WriteString(renderRaceMenu())
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
@@ -152,62 +152,62 @@ func (p *AdventurePlugin) dndSetupStatus(ctx MessageContext) error {
b.WriteString("**Next: Step 1 — Race.**\n")
b.WriteString(renderRaceMenu())
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)))
if c.Class == "" {
b.WriteString("\n**Next: Step 2 — Class.**\n")
b.WriteString(renderClassMenu())
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)))
if !statsAssigned(c) {
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("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",
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(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 {
r, ok := parseRace(raceArg)
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)
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
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)
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))
}
func (p *AdventurePlugin) dndSetupClass(ctx MessageContext, classArg string) error {
cl, ok := parseClass(classArg)
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)
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
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)
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"+
"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).",
@@ -227,29 +227,29 @@ func (p *AdventurePlugin) dndSetupStats(ctx MessageContext, statsArg string) err
}
c, err := loadOrInitDraft(ctx.Sender)
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]
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
b.WriteString("Stats saved (pre-racial bonuses).\n")
b.WriteString(renderConfirmPreview(c))
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 {
c, err := LoadDnDCharacter(ctx.Sender)
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 {
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) {
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.
@@ -283,28 +283,28 @@ func (p *AdventurePlugin) dndSetupConfirm(ctx MessageContext) error {
c.UpdatedAt = time.Now().UTC()
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)
// Phase 9: caster classes get a default known-spell list and slot pool.
// Idempotent — !setup confirm after a respec wipe will repopulate.
_ = ensureSpellsForCharacter(c)
return p.SendDM(ctx.Sender,renderSetupComplete(c))
return p.SendDM(ctx.Sender, renderSetupComplete(c))
}
func (p *AdventurePlugin) dndSetupCancel(ctx MessageContext) error {
c, err := LoadDnDCharacter(ctx.Sender)
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 {
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 {
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 ──────────────────────────────────────────────────────────────────
@@ -480,11 +480,11 @@ func dbExecCancel(userID id.UserID) (int64, error) {
// parseStatsArg accepts the six standard-array values in any of several
// 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
// of the standard array happens in the caller via isStandardArray.

View File

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

View File

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

View File

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

View File

@@ -295,7 +295,7 @@ func buildSpellList() []SpellDefinition {
{ID: "spirit_guardians", Name: "Spirit Guardians", Level: 3, School: "conjuration",
Classes: cleric, Effect: EffectDamageSave, CastTime: CastAction,
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.",
Upcast: "more guardians per slot above 3rd"},
{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",
Classes: mage, Effect: EffectDamageSave, CastTime: CastAction,
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."},
{ID: "greater_invisibility", Name: "Greater Invisibility", Level: 4, School: "illusion",
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."},
{ID: "clairvoyance", Name: "Clairvoyance", Level: 3, School: "divination",
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."},
{ID: "glyph_of_warding", Name: "Glyph of Warding", Level: 3, School: "abjuration",
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,
},
Mods: CombatModifiers{
DamageReduct: 1.0,
DamageReduct: 1.0,
BerserkerRage: true, RageMeleeDmg: 2,
PhysicalResistRage: true, FrenzyDmgBonus: 0.5,
},

View File

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

View File

@@ -25,26 +25,26 @@ import (
// dndXPTable[L] = cumulative XP needed to reach level L from level 1.
// L=0 is unused (D&D characters start at L1).
var dndXPTable = [...]int{
0, // L1 (start)
300, // L2
900, // L3
1700, // L4
2700, // L5 (anchor: doc)
4300, // L6
6500, // L7 (anchor: doc)
9000, // L8
11500, // L9
14000, // L10 (anchor: doc)
18000, // L11
22000, // L12
26000, // L13
30000, // L14
34000, // L15 (anchor: doc)
44000, // L16
54000, // L17
64000, // L18
74000, // L19
85000, // L20 (anchor: doc)
0, // L1 (start)
300, // L2
900, // L3
1700, // L4
2700, // L5 (anchor: doc)
4300, // L6
6500, // L7 (anchor: doc)
9000, // L8
11500, // L9
14000, // L10 (anchor: doc)
18000, // L11
22000, // L12
26000, // L13
30000, // L14
34000, // L15 (anchor: doc)
44000, // L16
54000, // L17
64000, // L18
74000, // L19
85000, // L20 (anchor: doc)
}
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
// and many more at high levels.
const (
dndArenaXPPerThreat = 12 // arena win XP = threat * this
dndArenaXPMin = 30 // arena win XP floor (low-threat fights still pay)
dndDungeonXPPerTier = 60 // dungeon win XP = tier^1.4 * this, roughly
dndLossXPFraction = 0.25
dndNearDeathXPBonus = 1.25 // multiplier for clutch wins
dndArenaXPPerThreat = 12 // arena win XP = threat * this
dndArenaXPMin = 30 // arena win XP floor (low-threat fights still pay)
dndDungeonXPPerTier = 60 // dungeon win XP = tier^1.4 * this, roughly
dndLossXPFraction = 0.25
dndNearDeathXPBonus = 1.25 // multiplier for clutch wins
)
// 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) {
cases := []struct{ level, want int }{
{1, 300}, // L1→L2: 300
{2, 600}, // L2→L3: 900-300
{4, 1000}, // L4→L5: 2700-1700
{6, 2200}, // L6→L7: 6500-4300
{1, 300}, // L1→L2: 300
{2, 600}, // L2→L3: 900-300
{4, 1000}, // L4→L5: 2700-1700
{6, 2200}, // L6→L7: 6500-4300
{19, 11000}, // L19→L20: 85000-74000
{20, 0}, // capped
{0, 300}, // clamp to 1
{20, 0}, // capped
{0, 300}, // clamp to 1
}
for _, c := range cases {
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,
// and renders the staged narration. Returns:
//
// intro — pre-combat block (TwinBee combat-start + monster stat block)
// phases — RenderCombatLog output, streamed with delays by the caller
// outcome — post-combat block: nat20/nat1 flavor, kill line, loot, d20 summary
// ended — true when the player went down (caller skips next-room teaser)
// intro — pre-combat block (TwinBee combat-start + monster stat block)
// phases — RenderCombatLog output, streamed with delays by the caller
// outcome — post-combat block: nat20/nat1 flavor, kill line, loot, d20 summary
// 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
// non-paced fallthrough.

View File

@@ -56,13 +56,13 @@ func TestPassiveDecayMood_DriftsTowardFifty(t *testing.T) {
hoursAgo int
want int
}{
{"high-score-decays-down", 80, 5, 70}, // -2/hr * 5
{"high-clamps-at-50", 80, 100, 50}, // would overshoot
{"low-score-decays-up", 10, 5, 20}, // +2/hr * 5
{"low-clamps-at-50", 10, 100, 50}, // would overshoot
{"already-50-no-change", 50, 100, 50}, // neutral stays neutral
{"fresh-no-decay", 80, 0, 80}, // no time elapsed
{"sub-hour-no-decay", 80, 0, 80}, // hours==0 → no drift
{"high-score-decays-down", 80, 5, 70}, // -2/hr * 5
{"high-clamps-at-50", 80, 100, 50}, // would overshoot
{"low-score-decays-up", 10, 5, 20}, // +2/hr * 5
{"low-clamps-at-50", 10, 100, 50}, // would overshoot
{"already-50-no-change", 50, 100, 50}, // neutral stays neutral
{"fresh-no-decay", 80, 0, 80}, // no time elapsed
{"sub-hour-no-decay", 80, 0, 80}, // hours==0 → no drift
}
for _, c := range cases {
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
// pending fork-prompt state (G5 surface) — populated when the player
// arrives at a fork with 2+ unlocked outgoing edges.
CurrentNode string
VisitedNodes []string
NodeChoices map[string]any
CurrentNode string
VisitedNodes []string
NodeChoices map[string]any
// Revisit R1 — monotonic count of node entries, including the entry
// 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)
return err
}

View File

@@ -37,18 +37,18 @@ const (
// (count, sides) form; AlertsEnemies flags traps that have no damage
// but raise the alarm. Weight biases the room-level random pick.
type zoneTrapDef struct {
Kind ZoneTrapKind
Display string
Trigger string // short flavor descriptor for the trip narration
DetectSkill DnDSkill
DetectDC int
DamageDiceN int // 0 if no direct damage
DamageDiceD int // sides
Kind ZoneTrapKind
Display string
Trigger string // short flavor descriptor for the trip narration
DetectSkill DnDSkill
DetectDC int
DamageDiceN int // 0 if no direct damage
DamageDiceD int // sides
DamageType string // "bludgeoning", "piercing", "poison", ...
AlertsEnemies bool // tripwire-class: 0 damage, raises alarm
SuppressesMagic bool // antimagic-class: 0 damage, narrative suppression
Tier ZoneTier
Weight int // 1..10, default 5
Weight int // 1..10, default 5
}
// 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,
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
// resolver normalizes over eligible events at pick time.
type ambientEvent struct {
Kind string
Pool []string
Weight int
Kind string
Pool []string
Weight int
Eligible func(*Expedition) bool
}
@@ -219,9 +219,9 @@ type ambientEvent struct {
// out so deliverAmbient can degrade to it if a chosen pool is empty.
func ambientEventMonologue() ambientEvent {
return ambientEvent{
Kind: "monologue",
Pool: flavor.AmbientMonologue,
Weight: 35,
Kind: "monologue",
Pool: flavor.AmbientMonologue,
Weight: 35,
Eligible: func(*Expedition) bool { return true },
}
}
@@ -233,15 +233,15 @@ func ambientEvents() []ambientEvent {
return []ambientEvent{
ambientEventMonologue(),
{
Kind: "small_find",
Pool: flavor.AmbientSmallFind,
Weight: 10,
Kind: "small_find",
Pool: flavor.AmbientSmallFind,
Weight: 10,
Eligible: always,
},
{
Kind: "noise",
Pool: flavor.AmbientNoise,
Weight: 15,
Kind: "noise",
Pool: flavor.AmbientNoise,
Weight: 15,
Eligible: always,
},
{
@@ -256,9 +256,9 @@ func ambientEvents() []ambientEvent {
},
},
{
Kind: "lucky_find",
Pool: flavor.AmbientLuckyFind,
Weight: 10,
Kind: "lucky_find",
Pool: flavor.AmbientLuckyFind,
Weight: 10,
Eligible: always,
},
{

View File

@@ -9,9 +9,9 @@ import (
func TestDecideAutopilotCamp_SkipsWhenCamped(t *testing.T) {
in := autoCampInputs{
Camp: &CampState{Active: true, Type: CampTypeRough},
Run: &DungeonRun{CurrentRoom: 0, RoomsCleared: []int{0}},
HPCur: 1, HPMax: 20, // very low — would otherwise camp
Camp: &CampState{Active: true, Type: CampTypeRough},
Run: &DungeonRun{CurrentRoom: 0, RoomsCleared: []int{0}},
HPCur: 1, HPMax: 20, // very low — would otherwise camp
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
}
if _, ok := decideAutopilotCamp(in); ok {
@@ -21,8 +21,8 @@ func TestDecideAutopilotCamp_SkipsWhenCamped(t *testing.T) {
func TestDecideAutopilotCamp_SkipsHealthyHP(t *testing.T) {
in := autoCampInputs{
Run: &DungeonRun{CurrentRoom: 0, RoomsCleared: []int{0}},
HPCur: 20, HPMax: 20,
Run: &DungeonRun{CurrentRoom: 0, RoomsCleared: []int{0}},
HPCur: 20, HPMax: 20,
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
}
if _, ok := decideAutopilotCamp(in); ok {
@@ -32,8 +32,8 @@ func TestDecideAutopilotCamp_SkipsHealthyHP(t *testing.T) {
func TestDecideAutopilotCamp_LowHPClearedPitchesStandard(t *testing.T) {
in := autoCampInputs{
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
HPCur: 5, HPMax: 20, // 25% — below 55% threshold
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
HPCur: 5, HPMax: 20, // 25% — below 55% threshold
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
}
d, ok := decideAutopilotCamp(in)
@@ -44,8 +44,8 @@ func TestDecideAutopilotCamp_LowHPClearedPitchesStandard(t *testing.T) {
func TestDecideAutopilotCamp_LowHPUnclearedPitchesRough(t *testing.T) {
in := autoCampInputs{
Run: &DungeonRun{CurrentRoom: 2, RoomsCleared: []int{0, 1}},
HPCur: 5, HPMax: 20,
Run: &DungeonRun{CurrentRoom: 2, RoomsCleared: []int{0, 1}},
HPCur: 5, HPMax: 20,
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
}
d, ok := decideAutopilotCamp(in)
@@ -56,8 +56,8 @@ func TestDecideAutopilotCamp_LowHPUnclearedPitchesRough(t *testing.T) {
func TestDecideAutopilotCamp_DowngradesWhenSuppliesTight(t *testing.T) {
in := autoCampInputs{
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
HPCur: 5, HPMax: 20,
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
HPCur: 5, HPMax: 20,
Supplies: ExpeditionSupplies{Current: 0.6, Max: 5, DailyBurn: 1}, // can't afford 1.0
}
d, ok := decideAutopilotCamp(in)
@@ -227,8 +227,8 @@ func TestDecideAutopilotCamp_NightTriggerOnEventAnchored(t *testing.T) {
now := time.Now().UTC()
stale := now.Add(-(nightCampWindow + time.Hour))
in := autoCampInputs{
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
HPCur: 20, HPMax: 20, // healthy — only the night window should pitch
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
HPCur: 20, HPMax: 20, // healthy — only the night window should pitch
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
Now: now,
EventAnchored: true,
@@ -250,8 +250,8 @@ func TestDecideAutopilotCamp_NightSkippedOnLegacy(t *testing.T) {
now := time.Now().UTC()
stale := now.Add(-24 * time.Hour)
in := autoCampInputs{
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
HPCur: 20, HPMax: 20,
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
HPCur: 20, HPMax: 20,
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
Now: now,
EventAnchored: false, // legacy — only HP-low triggers
@@ -266,8 +266,8 @@ func TestDecideAutopilotCamp_NightFlagSetEvenOnHPLow(t *testing.T) {
now := time.Now().UTC()
stale := now.Add(-(nightCampWindow + time.Hour))
in := autoCampInputs{
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
HPCur: 5, HPMax: 20, // also low
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
HPCur: 5, HPMax: 20, // also low
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
Now: now,
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).
// - r: walk result, including reason + accumulated stream.
// - 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
// digest variant. Zero-value when no pitch happened.
// digest variant. Zero-value when no pitch happened.
//
// Surface rules:
// - 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
// classBalanceResult — same shape, different lens.
type expeditionBalanceResult struct {
Profile expeditionBalanceProfile
Trials int
Completions int
Deaths int
StarvedOuts int
MedianDays int
MedianThreatEnd int
AvgEncounters float64
Profile expeditionBalanceProfile
Trials int
Completions int
Deaths int
StarvedOuts int
MedianDays int
MedianThreatEnd int
AvgEncounters float64
AvgHPRemainingPct float64 // mean across survivors
}
@@ -655,11 +655,11 @@ func buildHarnessZoneEnemy(monster DnDMonsterTemplate, tier int) Combatant {
// expeditionHarness threads per-trial mutable state (RNG, expedition,
// character HP carryover, encounter count) through the day loop.
type expeditionHarness struct {
exp *Expedition
char *DnDCharacter
rng *harnessRNG
encounters int
rollsPerDay int // resolved from profile + default; never zero
exp *Expedition
char *DnDCharacter
rng *harnessRNG
encounters int
rollsPerDay int // resolved from profile + default; never zero
// Lever overrides for the Phase 2 sweep. Zero on both fields ⇒
// live behavior (retreatThreatBump=5, /5 surprise-nick divisor).
retreatThreatBumpOverride int
@@ -743,12 +743,12 @@ func runExpeditionBalanceTrial(p expeditionBalanceProfile, seed uint64) expediti
rolls = harnessHarvestRollsPerDay
}
h := &expeditionHarness{
exp: exp,
char: char,
rng: newHarnessRNG(seed),
rollsPerDay: rolls,
retreatThreatBumpOverride: p.RetreatThreatBumpOverride,
surpriseNickDivisorOverride: p.SurpriseNickDivisorOverride,
exp: exp,
char: char,
rng: newHarnessRNG(seed),
rollsPerDay: rolls,
retreatThreatBumpOverride: p.RetreatThreatBumpOverride,
surpriseNickDivisorOverride: p.SurpriseNickDivisorOverride,
eliteInterruptThresholdOverride: p.EliteInterruptThresholdOverride,
threatDriftBaseOverride: p.ThreatDriftBaseOverride,
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
// (harsher) for each knob so the live point sits in the middle of
// the grid and we can read the slope in both directions.
bumps := []int{2, 5, 10} // threat-bump per retreat
divisors := []int{3, 5, 8, 12} // /N wounded-nick divisor; bigger = gentler
bumps := []int{2, 5, 10} // threat-bump per retreat
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)",
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
// "Phase 3 — global lever tuning" section:
//
// eliteInterruptThreshold (live=19, total roll cutoff for Elite
// bracket) — directly controls how often fresh-entry elite fights
// trigger during the daytime harvest pipeline. Sweep {17, 19, 23}:
// 17 = more elites (slope check below live), 19 = live baseline,
// 23 = elites only when roll+mod ≥ 23 (rare even at T5).
// eliteInterruptThreshold (live=19, total roll cutoff for Elite
// bracket) — directly controls how often fresh-entry elite fights
// trigger during the daytime harvest pipeline. Sweep {17, 19, 23}:
// 17 = more elites (slope check below live), 19 = live baseline,
// 23 = elites only when roll+mod ≥ 23 (rare even at T5).
//
// threatDriftBase (live=3, daily threat clock drift before mood-mod)
// — slows the multi-day AC/init/supply-burn creep that compounds
// over a 14-day expedition. Sweep {1, 3, 5}: 1 = nearly flat, 3 =
// live, 5 = harsher.
// threatDriftBase (live=3, daily threat clock drift before mood-mod)
// — slows the multi-day AC/init/supply-burn creep that compounds
// over a 14-day expedition. Sweep {1, 3, 5}: 1 = nearly flat, 3 =
// live, 5 = harsher.
//
// 3×3 = 9 combos × 10 zones × 200 trials/cell. Diagnostic-only — no
// 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,
// threatDriftBase=1) and walks two more global knobs:
//
// surpriseNickFloor (live=tier, i.e. 1..5 by zone tier) — the raw
// 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
// floor is the biggest single contributor to the wear-down curve
// that funnels survivors into starvation. Sweep {-1 (disable, =0),
// 1 (flat), 0-sentinel (live tier)}.
// surpriseNickFloor (live=tier, i.e. 1..5 by zone tier) — the raw
// 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
// floor is the biggest single contributor to the wear-down curve
// that funnels survivors into starvation. Sweep {-1 (disable, =0),
// 1 (flat), 0-sentinel (live tier)}.
//
// supplyBurnRatePct (live=100, %) — daily supply burn scalar. Lower
// = 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
// Phase 3-A into actual completions if survivability is the only
// remaining blocker, or leave them dying in combat if it isn't.
// Sweep {100 (live), 75, 50}.
// supplyBurnRatePct (live=100, %) — daily supply burn scalar. Lower
// = 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
// Phase 3-A into actual completions if survivability is the only
// remaining blocker, or leave them dying in combat if it isn't.
// Sweep {100 (live), 75, 50}.
//
// 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
@@ -925,14 +925,14 @@ func TestExpeditionBalance_Phase4A_OutlierDiagnostic(t *testing.T) {
killAttributed int // last fight of a died_combat trial
}
type zoneAgg struct {
profile expeditionBalanceProfile
completions int
deaths int
starves int
monsters map[string]*monsterStat
dayHist [16]int // 1..14 + overflow bucket; index 0 unused
eliteFights int
stdFights int
profile expeditionBalanceProfile
completions int
deaths int
starves int
monsters map[string]*monsterStat
dayHist [16]int // 1..14 + overflow bucket; index 0 unused
eliteFights int
stdFights int
// for the trial currently in flight (mutated during traceFightStruct)
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
// moves:
//
// 1. Gear-tier centerline remap (phase1TierCenterline) → axis L
// 2. Per-tier elite-bracket threshold (vs global) → axis E
// 3. Ship burn=75 globally (Phase 3-B's high-burn cell) → axis B
// 1. Gear-tier centerline remap (phase1TierCenterline) → axis L
// 2. Per-tier elite-bracket threshold (vs global) → axis E
// 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-
// 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
// the lever, non-monotone means a confound.
//
// L: player level = {centerline-2, centerline, centerline+2}
// E: elite threshold = {18, 23, 28}
// B: supply burn pct = {40, 50, 60}
// L: player level = {centerline-2, centerline, centerline+2}
// E: elite threshold = {18, 23, 28}
// B: supply burn pct = {40, 50, 60}
//
// 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
@@ -1145,7 +1145,7 @@ func TestExpeditionBalance_Phase5A_TierWideSensitivity(t *testing.T) {
// Returns comp/death/starve counts plus elite/standard fight share
// so the caller can attribute the slope across axis values.
type cellOut struct {
comp, death, starve int
comp, death, starve int
eliteFights, stdFights int
}
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.
//
// 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.
// 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.
func redirectToGamesRoom(sender id.UserID) string {
_ = sender

View File

@@ -24,8 +24,8 @@ func BenchmarkComputeSignalFromRates_CrossPairSize(b *testing.B) {
baseRates := make([]float64, 260)
quoteRates := make([]float64, 260)
for i := range baseRates {
baseRates[i] = 0.90 + rand.Float64()*0.10 // EUR/USD ~0.90-1.00
quoteRates[i] = 145 + rand.Float64()*10 // JPY/USD ~145-155
baseRates[i] = 0.90 + rand.Float64()*0.10 // EUR/USD ~0.90-1.00
quoteRates[i] = 145 + rand.Float64()*10 // JPY/USD ~145-155
}
// 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", "JPY", "USD"}, 1500, "JPY", "USD", false},
{[]string{"USD", "EUR"}, 0, "", "", true}, // no amount
{[]string{"1500", "USD"}, 0, "", "", true}, // missing quote
{[]string{"1500", "USD", "USD"}, 0, "", "", true}, // same currency
{[]string{"1500", "ZZZ", "EUR"}, 0, "", "", true}, // unknown currency
{[]string{"1500", "USD", "EUR", "JPY"}, 0, "", "", true}, // too many currencies
{[]string{"1500", "100", "USD", "EUR"}, 0, "", "", true}, // multiple amounts
{[]string{"-100", "USD", "EUR"}, 0, "", "", true}, // negative
{[]string{"USD", "EUR"}, 0, "", "", true}, // no amount
{[]string{"1500", "USD"}, 0, "", "", true}, // missing quote
{[]string{"1500", "USD", "USD"}, 0, "", "", true}, // same currency
{[]string{"1500", "ZZZ", "EUR"}, 0, "", "", true}, // unknown currency
{[]string{"1500", "USD", "EUR", "JPY"}, 0, "", "", true}, // too many currencies
{[]string{"1500", "100", "USD", "EUR"}, 0, "", "", true}, // multiple amounts
{[]string{"-100", "USD", "EUR"}, 0, "", "", true}, // negative
}
for _, c := range cases {

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,10 @@ import (
// ── formatNumber (xp.go) ───────────────────────────────────────────────────
func TestFormatNumber_Small(t *testing.T) {
cases := []struct{ in int; want string }{
cases := []struct {
in int
want string
}{
{0, "0"},
{1, "1"},
{999, "999"},
@@ -21,7 +24,10 @@ func TestFormatNumber_Small(t *testing.T) {
}
func TestFormatNumber_WithCommas(t *testing.T) {
cases := []struct{ in int; want string }{
cases := []struct {
in int
want string
}{
{1000, "1,000"},
{10000, "10,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 {
d := db.Get()
var balance int64

View File

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

View File

@@ -355,9 +355,9 @@ type CFRData struct {
func packInfoSetKey(street Street, posIP bool, eqBucket, sprBkt, boardTex int, history string) InfoSetKey {
var key uint64
key |= uint64(street) & 0x7 // bits 0-2
key |= uint64(street) & 0x7 // bits 0-2
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(sprBkt) & 0x7) << 8 // bits 8-10

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -53,13 +53,13 @@ type modConfig struct {
RepeatSimilarityThreshold float64
// Mention flood
MentionMax int
MentionFloodCount int
MentionMax int
MentionFloodCount int
MentionFloodWindowSeconds int
// Link rate (new members)
LinkRateNewMember int
LinkRateWindowSeconds int
LinkRateNewMember int
LinkRateWindowSeconds int
// Invite flooding
InviteMaxPerHour int
@@ -123,16 +123,16 @@ func loadModConfig() modConfig {
// ---------------------------------------------------------------------------
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_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_repeat": "Hey — repeating the same message isn't cool. Your messages have been removed and this has been noted.",
"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_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_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_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.",
"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.",
"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.",
"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.",
"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
// the whole struct rather than splitting per-field.
type HouseState struct {
Tier int
LoanBalance int
LoanFrozen bool
MissedPayments int
Autopay bool
CurrentRate float64
Tier int
LoanBalance int
LoanFrozen bool
MissedPayments int
Autopay bool
CurrentRate float64
}
// 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.
func loadLifecycleState(userID id.UserID) (LifecycleState, error) {
var (
s LifecycleState
s LifecycleState
streakDecayed, actionTaken, holidayTaken int
createdAt, lastActiveAt sql.NullTime
createdAt, lastActiveAt sql.NullTime
)
err := db.Get().QueryRow(
`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
// either rarely mutate or already mutate at every save site.
type MiscState struct {
Title string
TreasuresLocked bool
CraftsSucceeded int
Title string
TreasuresLocked bool
CraftsSucceeded int
}
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
// data file by the importer's classifier; the lookup is a fixed table.
type slotCheck struct {
id string
wantSlot DnDSlot
id string
wantSlot DnDSlot
mustNotBeRingForSubstring string // sanity vs word-boundary regressions
}
checks := []slotCheck{
@@ -399,8 +399,8 @@ func TestScenario_SpellJargonRegression(t *testing.T) {
bannedRegexes := []*regexp.Regexp{
regexp.MustCompile(`\bd(4|6|8|10|12|20|100)\b`),
regexp.MustCompile(`\b\d+d\d+\b`),
regexp.MustCompile(`\bWhatever [a-z]`), // placeholder signature
regexp.MustCompile(`(?:\.\.\.|…)\s*$`), // trailing ellipsis
regexp.MustCompile(`\bWhatever [a-z]`), // placeholder signature
regexp.MustCompile(`(?:\.\.\.|…)\s*$`), // trailing ellipsis
regexp.MustCompile(`\b[a-z]{1,3}(?:\.\.\.|…)\s*$`), // truncated word
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) Commands() []CommandDef { return nil }
func (p *SpaceInviterPlugin) Name() string { return "space_inviter" }
func (p *SpaceInviterPlugin) Commands() []CommandDef { return nil }
func (p *SpaceInviterPlugin) OnReaction(ReactionContext) error { return nil }
func (p *SpaceInviterPlugin) Init() error {

View File

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

View File

@@ -68,15 +68,15 @@ type openTDBResult struct {
}
type activeSession struct {
RoomID id.RoomID
RoomID id.RoomID
QuestionEventID id.EventID
Question string
CorrectAnswer string
AllAnswers []string
CorrectIndex int
StartedAt time.Time
Difficulty string
Category string
Question string
CorrectAnswer string
AllAnswers []string
CorrectIndex int
StartedAt time.Time
Difficulty string
Category string
}
// 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"),
)
}

View File

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

View File

@@ -658,4 +658,3 @@ func parseNoMercyFlags(args string) (noMercy bool, sevenZeroRule bool, amountStr
return true, false, rest
}

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