diff --git a/cmd/expedition-sim/main.go b/cmd/expedition-sim/main.go index 1080b75..da6a0cb 100644 --- a/cmd/expedition-sim/main.go +++ b/cmd/expedition-sim/main.go @@ -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 { diff --git a/cmd/open5e-import/desc_overrides.go b/cmd/open5e-import/desc_overrides.go index 83c6f42..54285cd 100644 --- a/cmd/open5e-import/desc_overrides.go +++ b/cmd/open5e-import/desc_overrides.go @@ -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 ]+?)?`), diff --git a/cmd/open5e-import/desc_overrides_test.go b/cmd/open5e-import/desc_overrides_test.go index e8e8706..fded62e 100644 --- a/cmd/open5e-import/desc_overrides_test.go +++ b/cmd/open5e-import/desc_overrides_test.go @@ -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 { diff --git a/cmd/open5e-import/main.go b/cmd/open5e-import/main.go index c477831..62fbf0e 100644 --- a/cmd/open5e-import/main.go +++ b/cmd/open5e-import/main.go @@ -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 diff --git a/internal/plugin/adventure.go b/internal/plugin/adventure.go index 774966b..aa7255d 100644 --- a/internal/plugin/adventure.go +++ b/internal/plugin/adventure.go @@ -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.") } - diff --git a/internal/plugin/adventure_activities.go b/internal/plugin/adventure_activities.go index b2317c1..4aa8520 100644 --- a/internal/plugin/adventure_activities.go +++ b/internal/plugin/adventure_activities.go @@ -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 diff --git a/internal/plugin/adventure_activity_unified.go b/internal/plugin/adventure_activity_unified.go index 5061fb2..1b851bd 100644 --- a/internal/plugin/adventure_activity_unified.go +++ b/internal/plugin/adventure_activity_unified.go @@ -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 { diff --git a/internal/plugin/adventure_arena.go b/internal/plugin/adventure_arena.go index c1c5740..7d20046 100644 --- a/internal/plugin/adventure_arena.go +++ b/internal/plugin/adventure_arena.go @@ -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 diff --git a/internal/plugin/adventure_arena_bossflow_test.go b/internal/plugin/adventure_arena_bossflow_test.go index fe3dfa2..b00cdda 100644 --- a/internal/plugin/adventure_arena_bossflow_test.go +++ b/internal/plugin/adventure_arena_bossflow_test.go @@ -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, diff --git a/internal/plugin/adventure_arena_render.go b/internal/plugin/adventure_arena_render.go index 08099a6..2a75ace 100644 --- a/internal/plugin/adventure_arena_render.go +++ b/internal/plugin/adventure_arena_render.go @@ -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 { diff --git a/internal/plugin/adventure_babysit.go b/internal/plugin/adventure_babysit.go index 1f86e57..32d1e5f 100644 --- a/internal/plugin/adventure_babysit.go +++ b/internal/plugin/adventure_babysit.go @@ -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) { diff --git a/internal/plugin/adventure_babysit_test.go b/internal/plugin/adventure_babysit_test.go index 96d8a1c..a1641ae 100644 --- a/internal/plugin/adventure_babysit_test.go +++ b/internal/plugin/adventure_babysit_test.go @@ -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) diff --git a/internal/plugin/adventure_character.go b/internal/plugin/adventure_character.go index dbf6a22..6ef006b 100644 --- a/internal/plugin/adventure_character.go +++ b/internal/plugin/adventure_character.go @@ -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 { diff --git a/internal/plugin/adventure_consumables.go b/internal/plugin/adventure_consumables.go index 02a2a8c..955e371 100644 --- a/internal/plugin/adventure_consumables.go +++ b/internal/plugin/adventure_consumables.go @@ -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. diff --git a/internal/plugin/adventure_flavor_blacksmith.go b/internal/plugin/adventure_flavor_blacksmith.go index 911ae82..99a1261 100644 --- a/internal/plugin/adventure_flavor_blacksmith.go +++ b/internal/plugin/adventure_flavor_blacksmith.go @@ -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_", } - diff --git a/internal/plugin/adventure_flavor_dungeon.go b/internal/plugin/adventure_flavor_dungeon.go index 7e043e6..0e300c5 100644 --- a/internal/plugin/adventure_flavor_dungeon.go +++ b/internal/plugin/adventure_flavor_dungeon.go @@ -924,4 +924,4 @@ var DungeonExceptional = map[int][]string{ "in a dungeon. Write it down. " + "The dungeon will tell this story differently.", }, -} \ No newline at end of file +} diff --git a/internal/plugin/adventure_flavor_pets.go b/internal/plugin/adventure_flavor_pets.go index 5e7484c..6ba518f 100644 --- a/internal/plugin/adventure_flavor_pets.go +++ b/internal/plugin/adventure_flavor_pets.go @@ -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. diff --git a/internal/plugin/adventure_followups_test.go b/internal/plugin/adventure_followups_test.go index 01232d3..237111f 100644 --- a/internal/plugin/adventure_followups_test.go +++ b/internal/plugin/adventure_followups_test.go @@ -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") diff --git a/internal/plugin/adventure_hospital.go b/internal/plugin/adventure_hospital.go index 3b20d04..3e87c67 100644 --- a/internal/plugin/adventure_hospital.go +++ b/internal/plugin/adventure_hospital.go @@ -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 } diff --git a/internal/plugin/adventure_housing_test.go b/internal/plugin/adventure_housing_test.go index ebf56f2..f3a5d49 100644 --- a/internal/plugin/adventure_housing_test.go +++ b/internal/plugin/adventure_housing_test.go @@ -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} diff --git a/internal/plugin/adventure_masterwork.go b/internal/plugin/adventure_masterwork.go index 669f77c..fc9bc43 100644 --- a/internal/plugin/adventure_masterwork.go +++ b/internal/plugin/adventure_masterwork.go @@ -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 diff --git a/internal/plugin/adventure_npcs.go b/internal/plugin/adventure_npcs.go index 68946a1..49a285e 100644 --- a/internal/plugin/adventure_npcs.go +++ b/internal/plugin/adventure_npcs.go @@ -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 { diff --git a/internal/plugin/adventure_render.go b/internal/plugin/adventure_render.go index a5a97b1..c265044 100644 --- a/internal/plugin/adventure_render.go +++ b/internal/plugin/adventure_render.go @@ -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 } diff --git a/internal/plugin/adventure_shop_test.go b/internal/plugin/adventure_shop_test.go index 79f327a..30548f9 100644 --- a/internal/plugin/adventure_shop_test.go +++ b/internal/plugin/adventure_shop_test.go @@ -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 { diff --git a/internal/plugin/adventure_test.go b/internal/plugin/adventure_test.go index 81c3527..29f7ab6 100644 --- a/internal/plugin/adventure_test.go +++ b/internal/plugin/adventure_test.go @@ -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, } diff --git a/internal/plugin/adventure_twinbee.go b/internal/plugin/adventure_twinbee.go index 2e13359..b9dc8ed 100644 --- a/internal/plugin/adventure_twinbee.go +++ b/internal/plugin/adventure_twinbee.go @@ -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{ diff --git a/internal/plugin/anime.go b/internal/plugin/anime.go index 3b2eb01..0941a0b 100644 --- a/internal/plugin/anime.go +++ b/internal/plugin/anime.go @@ -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"` diff --git a/internal/plugin/archetype.go b/internal/plugin/archetype.go index 7260beb..a588ced 100644 --- a/internal/plugin/archetype.go +++ b/internal/plugin/archetype.go @@ -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 } diff --git a/internal/plugin/bestiary_tuned_data.go b/internal/plugin/bestiary_tuned_data.go index a74301b..2603f8b 100644 --- a/internal/plugin/bestiary_tuned_data.go +++ b/internal/plugin/bestiary_tuned_data.go @@ -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"}, } } diff --git a/internal/plugin/blackjack.go b/internal/plugin/blackjack.go index 7c6596e..901d19c 100644 --- a/internal/plugin/blackjack.go +++ b/internal/plugin/blackjack.go @@ -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") } } - - diff --git a/internal/plugin/combat_bridge.go b/internal/plugin/combat_bridge.go index a5e82dd..537fc3f 100644 --- a/internal/plugin/combat_bridge.go +++ b/internal/plugin/combat_bridge.go @@ -592,4 +592,3 @@ func (p *AdventurePlugin) resolveDungeonAction( return result } - diff --git a/internal/plugin/combat_engine.go b/internal/plugin/combat_engine.go index dadd6e5..14db07a 100644 --- a/internal/plugin/combat_engine.go +++ b/internal/plugin/combat_engine.go @@ -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 } - diff --git a/internal/plugin/combat_stats.go b/internal/plugin/combat_stats.go index f4e9a26..7a141fb 100644 --- a/internal/plugin/combat_stats.go +++ b/internal/plugin/combat_stats.go @@ -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 13–80 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 13–80 HP, not 100+) Defense: 2 + int(t*1.2), Speed: 4 + int(t*1.5), CritRate: 0.03 + death*0.003, diff --git a/internal/plugin/combat_stats_test.go b/internal/plugin/combat_stats_test.go index d247a01..374e76f 100644 --- a/internal/plugin/combat_stats_test.go +++ b/internal/plugin/combat_stats_test.go @@ -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 { diff --git a/internal/plugin/concerts.go b/internal/plugin/concerts.go index 708ac26..d100921 100644 --- a/internal/plugin/concerts.go +++ b/internal/plugin/concerts.go @@ -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"` diff --git a/internal/plugin/dnd.go b/internal/plugin/dnd.go index e9a0cf1..6cecd3a 100644 --- a/internal/plugin/dnd.go +++ b/internal/plugin/dnd.go @@ -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 ` 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. diff --git a/internal/plugin/dnd_abilities.go b/internal/plugin/dnd_abilities.go index 53f4e2a..4b3a6b4 100644 --- a/internal/plugin/dnd_abilities.go +++ b/internal/plugin/dnd_abilities.go @@ -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 diff --git a/internal/plugin/dnd_abilities_test.go b/internal/plugin/dnd_abilities_test.go index 44bdfe0..f7d5245 100644 --- a/internal/plugin/dnd_abilities_test.go +++ b/internal/plugin/dnd_abilities_test.go @@ -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}, diff --git a/internal/plugin/dnd_audit_fixes_test.go b/internal/plugin/dnd_audit_fixes_test.go index 23ff011..6c9b1f7 100644 --- a/internal/plugin/dnd_audit_fixes_test.go +++ b/internal/plugin/dnd_audit_fixes_test.go @@ -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 { diff --git a/internal/plugin/dnd_bestiary.go b/internal/plugin/dnd_bestiary.go index cd3cf3b..76fd0d2 100644 --- a/internal/plugin/dnd_bestiary.go +++ b/internal/plugin/dnd_bestiary.go @@ -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 4–6.", + 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 4–6.", 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} diff --git a/internal/plugin/dnd_boss_consumables.go b/internal/plugin/dnd_boss_consumables.go index 0f57918..354f980 100644 --- a/internal/plugin/dnd_boss_consumables.go +++ b/internal/plugin/dnd_boss_consumables.go @@ -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 { diff --git a/internal/plugin/dnd_combat.go b/internal/plugin/dnd_combat.go index 074d23a..04471d1 100644 --- a/internal/plugin/dnd_combat.go +++ b/internal/plugin/dnd_combat.go @@ -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(), diff --git a/internal/plugin/dnd_equipment_profiles.go b/internal/plugin/dnd_equipment_profiles.go index 0e04fdf..af7b8fa 100644 --- a/internal/plugin/dnd_equipment_profiles.go +++ b/internal/plugin/dnd_equipment_profiles.go @@ -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 diff --git a/internal/plugin/dnd_equipment_profiles_test.go b/internal/plugin/dnd_equipment_profiles_test.go index c3d7ffc..ec8903c 100644 --- a/internal/plugin/dnd_equipment_profiles_test.go +++ b/internal/plugin/dnd_equipment_profiles_test.go @@ -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 { diff --git a/internal/plugin/dnd_expedition.go b/internal/plugin/dnd_expedition.go index 19b64b4..aa875ce 100644 --- a/internal/plugin/dnd_expedition.go +++ b/internal/plugin/dnd_expedition.go @@ -535,4 +535,3 @@ func nullableString(s string) any { } return s } - diff --git a/internal/plugin/dnd_expedition_combat_test.go b/internal/plugin/dnd_expedition_combat_test.go index 5ff3dd2..e2cac56 100644 --- a/internal/plugin/dnd_expedition_combat_test.go +++ b/internal/plugin/dnd_expedition_combat_test.go @@ -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) diff --git a/internal/plugin/dnd_expedition_harvest_test.go b/internal/plugin/dnd_expedition_harvest_test.go index 5e7fa30..3733e7f 100644 --- a/internal/plugin/dnd_expedition_harvest_test.go +++ b/internal/plugin/dnd_expedition_harvest_test.go @@ -96,7 +96,6 @@ func TestSaveHarvestNodes_DropsLegacyRoomKey(t *testing.T) { } } - func TestResolveHarvestOutcome_Brackets(t *testing.T) { cases := []struct { roll, dc int diff --git a/internal/plugin/dnd_expedition_map_test.go b/internal/plugin/dnd_expedition_map_test.go index 18ee5db..9dfce17 100644 --- a/internal/plugin/dnd_expedition_map_test.go +++ b/internal/plugin/dnd_expedition_map_test.go @@ -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 { diff --git a/internal/plugin/dnd_expedition_night_test.go b/internal/plugin/dnd_expedition_night_test.go index 6159c08..2e2dddd 100644 --- a/internal/plugin/dnd_expedition_night_test.go +++ b/internal/plugin/dnd_expedition_night_test.go @@ -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 { diff --git a/internal/plugin/dnd_expedition_region.go b/internal/plugin/dnd_expedition_region.go index 47c88e1..c0dc638 100644 --- a/internal/plugin/dnd_expedition_region.go +++ b/internal/plugin/dnd_expedition_region.go @@ -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"}, }, }, diff --git a/internal/plugin/dnd_expedition_supplies.go b/internal/plugin/dnd_expedition_supplies.go index 3993543..1333cab 100644 --- a/internal/plugin/dnd_expedition_supplies.go +++ b/internal/plugin/dnd_expedition_supplies.go @@ -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 diff --git a/internal/plugin/dnd_expedition_supplies_test.go b/internal/plugin/dnd_expedition_supplies_test.go index 15f27fd..2bd7a7d 100644 --- a/internal/plugin/dnd_expedition_supplies_test.go +++ b/internal/plugin/dnd_expedition_supplies_test.go @@ -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}, diff --git a/internal/plugin/dnd_expedition_temporal.go b/internal/plugin/dnd_expedition_temporal.go index eb764b1..4cf084a 100644 --- a/internal/plugin/dnd_expedition_temporal.go +++ b/internal/plugin/dnd_expedition_temporal.go @@ -455,12 +455,13 @@ const AbyssInstabilityPerDay = 5 const AbyssMaxInstability = 100 // AbyssInstabilityBandFor classifies the §7.6 instability tiers. -// normal: 0–20 -// mild: 21–40 (-1 WIS) -// warp: 41–60 (rooms shift order) -// surges: 61–80 (12h wandering checks) -// unravel: 81–99 (supply ×2, -2 all rolls) -// collapse: 100 +// +// normal: 0–20 +// mild: 21–40 (-1 WIS) +// warp: 41–60 (rooms shift order) +// surges: 61–80 (12h wandering checks) +// unravel: 81–99 (supply ×2, -2 all rolls) +// collapse: 100 func AbyssInstabilityBandFor(stack int) string { switch { case stack <= 20: diff --git a/internal/plugin/dnd_expedition_temporal_test.go b/internal/plugin/dnd_expedition_temporal_test.go index 9aff30c..1bb525b 100644 --- a/internal/plugin/dnd_expedition_temporal_test.go +++ b/internal/plugin/dnd_expedition_temporal_test.go @@ -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. diff --git a/internal/plugin/dnd_expedition_threat.go b/internal/plugin/dnd_expedition_threat.go index bf97766..3572973 100644 --- a/internal/plugin/dnd_expedition_threat.go +++ b/internal/plugin/dnd_expedition_threat.go @@ -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. diff --git a/internal/plugin/dnd_flavor_test.go b/internal/plugin/dnd_flavor_test.go index 3e315d5..d509944 100644 --- a/internal/plugin/dnd_flavor_test.go +++ b/internal/plugin/dnd_flavor_test.go @@ -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 diff --git a/internal/plugin/dnd_passives.go b/internal/plugin/dnd_passives.go index 81d90aa..624b3ec 100644 --- a/internal/plugin/dnd_passives.go +++ b/internal/plugin/dnd_passives.go @@ -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; diff --git a/internal/plugin/dnd_plugs_test.go b/internal/plugin/dnd_plugs_test.go index c53bfc2..6bc77e0 100644 --- a/internal/plugin/dnd_plugs_test.go +++ b/internal/plugin/dnd_plugs_test.go @@ -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}, diff --git a/internal/plugin/dnd_setup.go b/internal/plugin/dnd_setup.go index ad79f73..25e5c20 100644 --- a/internal/plugin/dnd_setup.go +++ b/internal/plugin/dnd_setup.go @@ -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 ` (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 `\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 `\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 ` — see options with `!setup`.", + return p.SendDM(ctx.Sender, fmt.Sprintf("Race set: **%s**. _%s_\n\nNext: `!setup class ` — 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. diff --git a/internal/plugin/dnd_skills.go b/internal/plugin/dnd_skills.go index 5d95aeb..dacc47e 100644 --- a/internal/plugin/dnd_skills.go +++ b/internal/plugin/dnd_skills.go @@ -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 { diff --git a/internal/plugin/dnd_spell_combat.go b/internal/plugin/dnd_spell_combat.go index 4b36172..1d21b5a 100644 --- a/internal/plugin/dnd_spell_combat.go +++ b/internal/plugin/dnd_spell_combat.go @@ -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, } diff --git a/internal/plugin/dnd_spell_save_rounding_test.go b/internal/plugin/dnd_spell_save_rounding_test.go index 6304dd4..4cfd210 100644 --- a/internal/plugin/dnd_spell_save_rounding_test.go +++ b/internal/plugin/dnd_spell_save_rounding_test.go @@ -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} diff --git a/internal/plugin/dnd_spells_data.go b/internal/plugin/dnd_spells_data.go index 2b252c6..05949b5 100644 --- a/internal/plugin/dnd_spells_data.go +++ b/internal/plugin/dnd_spells_data.go @@ -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, diff --git a/internal/plugin/dnd_subclass_combat_test.go b/internal/plugin/dnd_subclass_combat_test.go index 207dee9..bb2ee71 100644 --- a/internal/plugin/dnd_subclass_combat_test.go +++ b/internal/plugin/dnd_subclass_combat_test.go @@ -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, }, diff --git a/internal/plugin/dnd_test.go b/internal/plugin/dnd_test.go index 3fc8efe..ddf2f38 100644 --- a/internal/plugin/dnd_test.go +++ b/internal/plugin/dnd_test.go @@ -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 diff --git a/internal/plugin/dnd_xp.go b/internal/plugin/dnd_xp.go index d65118f..bc104ae 100644 --- a/internal/plugin/dnd_xp.go +++ b/internal/plugin/dnd_xp.go @@ -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. diff --git a/internal/plugin/dnd_xp_test.go b/internal/plugin/dnd_xp_test.go index f760156..44cd4b8 100644 --- a/internal/plugin/dnd_xp_test.go +++ b/internal/plugin/dnd_xp_test.go @@ -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 { diff --git a/internal/plugin/dnd_zone_cmd.go b/internal/plugin/dnd_zone_cmd.go index 55fef73..333d824 100644 --- a/internal/plugin/dnd_zone_cmd.go +++ b/internal/plugin/dnd_zone_cmd.go @@ -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. diff --git a/internal/plugin/dnd_zone_narration_test.go b/internal/plugin/dnd_zone_narration_test.go index 2386cd6..1170b25 100644 --- a/internal/plugin/dnd_zone_narration_test.go +++ b/internal/plugin/dnd_zone_narration_test.go @@ -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) { diff --git a/internal/plugin/dnd_zone_run.go b/internal/plugin/dnd_zone_run.go index 0e5926f..8dd846a 100644 --- a/internal/plugin/dnd_zone_run.go +++ b/internal/plugin/dnd_zone_run.go @@ -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 } - diff --git a/internal/plugin/dnd_zone_traps.go b/internal/plugin/dnd_zone_traps.go index 688e212..58fe3d8 100644 --- a/internal/plugin/dnd_zone_traps.go +++ b/internal/plugin/dnd_zone_traps.go @@ -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. diff --git a/internal/plugin/euro.go b/internal/plugin/euro.go index db2e507..455b55b 100644 --- a/internal/plugin/euro.go +++ b/internal/plugin/euro.go @@ -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))) } - diff --git a/internal/plugin/expedition_ambient.go b/internal/plugin/expedition_ambient.go index 2e28e64..aa0c656 100644 --- a/internal/plugin/expedition_ambient.go +++ b/internal/plugin/expedition_ambient.go @@ -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, }, { diff --git a/internal/plugin/expedition_autocamp_test.go b/internal/plugin/expedition_autocamp_test.go index 5299b0d..94a9ab6 100644 --- a/internal/plugin/expedition_autocamp_test.go +++ b/internal/plugin/expedition_autocamp_test.go @@ -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, diff --git a/internal/plugin/expedition_autorun.go b/internal/plugin/expedition_autorun.go index b2e5c07..d043722 100644 --- a/internal/plugin/expedition_autorun.go +++ b/internal/plugin/expedition_autorun.go @@ -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 diff --git a/internal/plugin/expedition_balance.go b/internal/plugin/expedition_balance.go index 66b0ed1..93720d6 100644 --- a/internal/plugin/expedition_balance.go +++ b/internal/plugin/expedition_balance.go @@ -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, diff --git a/internal/plugin/expedition_balance_test.go b/internal/plugin/expedition_balance_test.go index 00194a6..82247c9 100644 --- a/internal/plugin/expedition_balance_test.go +++ b/internal/plugin/expedition_balance_test.go @@ -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 diff --git a/internal/plugin/flip.go b/internal/plugin/flip.go index 4372956..a2ed511 100644 --- a/internal/plugin/flip.go +++ b/internal/plugin/flip.go @@ -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 diff --git a/internal/plugin/forex_analysis_test.go b/internal/plugin/forex_analysis_test.go index 80374c2..f722488 100644 --- a/internal/plugin/forex_analysis_test.go +++ b/internal/plugin/forex_analysis_test.go @@ -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) diff --git a/internal/plugin/forex_convert_test.go b/internal/plugin/forex_convert_test.go index 80d2910..1bdf227 100644 --- a/internal/plugin/forex_convert_test.go +++ b/internal/plugin/forex_convert_test.go @@ -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 { diff --git a/internal/plugin/fun.go b/internal/plugin/fun.go index 43393f8..cf4eea2 100644 --- a/internal/plugin/fun.go +++ b/internal/plugin/fun.go @@ -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, diff --git a/internal/plugin/gaming.go b/internal/plugin/gaming.go index 1ddce91..c549c4c 100644 --- a/internal/plugin/gaming.go +++ b/internal/plugin/gaming.go @@ -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"` diff --git a/internal/plugin/hangman.go b/internal/plugin/hangman.go index 40d53a0..b82f857 100644 --- a/internal/plugin/hangman.go +++ b/internal/plugin/hangman.go @@ -973,4 +973,3 @@ func (p *HangmanPlugin) recordHangmanScore(userID id.UserID, earned float64) { string(userID), earned, earned, ) } - diff --git a/internal/plugin/helpers_test.go b/internal/plugin/helpers_test.go index 682dcef..0ceec61 100644 --- a/internal/plugin/helpers_test.go +++ b/internal/plugin/helpers_test.go @@ -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"}, diff --git a/internal/plugin/holdem.go b/internal/plugin/holdem.go index 52a108c..ecca03f 100644 --- a/internal/plugin/holdem.go +++ b/internal/plugin/holdem.go @@ -1249,7 +1249,6 @@ func (p *HoldemPlugin) removePlayer(game *HoldemGame, uid id.UserID) { } } - func (p *HoldemPlugin) getNPCBalance() int64 { d := db.Get() var balance int64 diff --git a/internal/plugin/holdem_betting.go b/internal/plugin/holdem_betting.go index 268021d..7e32a51 100644 --- a/internal/plugin/holdem_betting.go +++ b/internal/plugin/holdem_betting.go @@ -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 } - diff --git a/internal/plugin/holdem_cfr.go b/internal/plugin/holdem_cfr.go index f864a37..6902bae 100644 --- a/internal/plugin/holdem_cfr.go +++ b/internal/plugin/holdem_cfr.go @@ -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 diff --git a/internal/plugin/holdem_tips.go b/internal/plugin/holdem_tips.go index d3ba330..62c760d 100644 --- a/internal/plugin/holdem_tips.go +++ b/internal/plugin/holdem_tips.go @@ -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. diff --git a/internal/plugin/holdem_tips_test.go b/internal/plugin/holdem_tips_test.go index 0afa934..5ec1186 100644 --- a/internal/plugin/holdem_tips_test.go +++ b/internal/plugin/holdem_tips_test.go @@ -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", } diff --git a/internal/plugin/holidays.go b/internal/plugin/holidays.go index 3b42616..2a0deef 100644 --- a/internal/plugin/holidays.go +++ b/internal/plugin/holidays.go @@ -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, } diff --git a/internal/plugin/llm_passive.go b/internal/plugin/llm_passive.go index 21851a2..b0c94d3 100644 --- a/internal/plugin/llm_passive.go +++ b/internal/plugin/llm_passive.go @@ -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 diff --git a/internal/plugin/milkcarton.go b/internal/plugin/milkcarton.go index f23d543..18e917b 100644 --- a/internal/plugin/milkcarton.go +++ b/internal/plugin/milkcarton.go @@ -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. diff --git a/internal/plugin/moderation.go b/internal/plugin/moderation.go index 22adf90..cfd49c0 100644 --- a/internal/plugin/moderation.go +++ b/internal/plugin/moderation.go @@ -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.", } // --------------------------------------------------------------------------- diff --git a/internal/plugin/player_meta.go b/internal/plugin/player_meta.go index 2304eef..35ca210 100644 --- a/internal/plugin/player_meta.go +++ b/internal/plugin/player_meta.go @@ -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 { diff --git a/internal/plugin/scenario_proddb_test.go b/internal/plugin/scenario_proddb_test.go index 024584d..c15d993 100644 --- a/internal/plugin/scenario_proddb_test.go +++ b/internal/plugin/scenario_proddb_test.go @@ -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`), } diff --git a/internal/plugin/space_inviter.go b/internal/plugin/space_inviter.go index 8df9138..03cb49f 100644 --- a/internal/plugin/space_inviter.go +++ b/internal/plugin/space_inviter.go @@ -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 { diff --git a/internal/plugin/stocks.go b/internal/plugin/stocks.go index e5a3936..61dc88e 100644 --- a/internal/plugin/stocks.go +++ b/internal/plugin/stocks.go @@ -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. diff --git a/internal/plugin/trivia.go b/internal/plugin/trivia.go index a15b86c..55b3ea4 100644 --- a/internal/plugin/trivia.go +++ b/internal/plugin/trivia.go @@ -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. diff --git a/internal/plugin/uno.go b/internal/plugin/uno.go index 305cc17..737ca47 100644 --- a/internal/plugin/uno.go +++ b/internal/plugin/uno.go @@ -2039,5 +2039,3 @@ func (p *UnoPlugin) recordGame(game *unoGame, result string, potBefore float64) time.Now().UTC().Format("2006-01-02 15:04:05"), ) } - - diff --git a/internal/plugin/uno_multi.go b/internal/plugin/uno_multi.go index e309299..22b9d5f 100644 --- a/internal/plugin/uno_multi.go +++ b/internal/plugin/uno_multi.go @@ -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 diff --git a/internal/plugin/uno_nomercy.go b/internal/plugin/uno_nomercy.go index 9e902d6..aac32ff 100644 --- a/internal/plugin/uno_nomercy.go +++ b/internal/plugin/uno_nomercy.go @@ -658,4 +658,3 @@ func parseNoMercyFlags(args string) (noMercy bool, sevenZeroRule bool, amountStr return true, false, rest } - diff --git a/internal/plugin/uno_test.go b/internal/plugin/uno_test.go index f073b9a..5aa665e 100644 --- a/internal/plugin/uno_test.go +++ b/internal/plugin/uno_test.go @@ -326,10 +326,10 @@ func TestCardPointValue(t *testing.T) { func TestScoreHand(t *testing.T) { hand := []unoCard{ - {unoRed, unoFive}, // 5 - {unoBlue, unoNine}, // 9 - {unoGreen, unoSkip}, // 20 - {unoWild, unoWildCard}, // 50 + {unoRed, unoFive}, // 5 + {unoBlue, unoNine}, // 9 + {unoGreen, unoSkip}, // 20 + {unoWild, unoWildCard}, // 50 } got := scoreHand(hand) want := 84 diff --git a/internal/plugin/wordle.go b/internal/plugin/wordle.go index cc938a1..be66e3c 100644 --- a/internal/plugin/wordle.go +++ b/internal/plugin/wordle.go @@ -484,8 +484,6 @@ func wordleCategoryHint(category WordleCategory) string { return "" } - - // isValidWord checks if a word is valid, using the in-memory cache first, // then the custom allow-list, then DreamDict. // Returns (valid, apiError). apiError is true when the service is unreachable. @@ -545,7 +543,6 @@ func (p *WordlePlugin) fetchDefinition(answer string) string { return dictFetchDefinitionText(p.dict, answer, lang) } - func (p *WordlePlugin) nextPuzzleNumber() int { d := db.Get() var count int @@ -842,4 +839,3 @@ func (p *WordlePlugin) rehydratePuzzles() { slog.Info("wordle: rehydrated puzzle", "room", roomID, "answer_len", pr.wordLength, "guesses", len(puzzle.Guesses)) } } - diff --git a/internal/plugin/wordle_game.go b/internal/plugin/wordle_game.go index 14aa22a..ceff9e7 100644 --- a/internal/plugin/wordle_game.go +++ b/internal/plugin/wordle_game.go @@ -28,9 +28,9 @@ type WordleGuess struct { type WordleCategory string const ( - WordleCategoryEN WordleCategory = "" // English (default) - WordleCategoryPT WordleCategory = "pt" // European Portuguese - WordleCategoryFR WordleCategory = "fr" // French + WordleCategoryEN WordleCategory = "" // English (default) + WordleCategoryPT WordleCategory = "pt" // European Portuguese + WordleCategoryFR WordleCategory = "fr" // French WordleCategoryGames WordleCategory = "games" // Video game words (English) ) diff --git a/internal/plugin/zone_graph_crypt_valdris_test.go b/internal/plugin/zone_graph_crypt_valdris_test.go index ffec2e5..3942852 100644 --- a/internal/plugin/zone_graph_crypt_valdris_test.go +++ b/internal/plugin/zone_graph_crypt_valdris_test.go @@ -120,4 +120,3 @@ func TestCurrentRoomType_GraphAuthored(t *testing.T) { t.Errorf("graph mode + secret node: got %s, want %s", got, RoomExploration) } } - diff --git a/internal/plugin/zone_graph_nav.go b/internal/plugin/zone_graph_nav.go index e58f513..7714465 100644 --- a/internal/plugin/zone_graph_nav.go +++ b/internal/plugin/zone_graph_nav.go @@ -427,4 +427,3 @@ func resolveForkChoice(pf *pendingFork, choice int) (pendingChoice, error) { } return c, nil } - diff --git a/internal/plugin/zone_graph_nav_test.go b/internal/plugin/zone_graph_nav_test.go index 2bf5fd6..74c2f79 100644 --- a/internal/plugin/zone_graph_nav_test.go +++ b/internal/plugin/zone_graph_nav_test.go @@ -240,4 +240,3 @@ func TestNodeKindToRoomType(t *testing.T) { } } } - diff --git a/internal/util/parser.go b/internal/util/parser.go index ec9c57d..62f700d 100644 --- a/internal/util/parser.go +++ b/internal/util/parser.go @@ -10,13 +10,13 @@ import ( // MessageStats holds parsed message metrics. type MessageStats struct { - Words int - Chars int - Links int - Images int - Questions int + Words int + Chars int + Links int + Images int + Questions int Exclamations int - Emojis int + Emojis int } var (