mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
gofmt: bring internal/ and cmd/ back to gofmt -l clean
Mechanical `gofmt -w ./internal ./cmd`. Mostly struct-field realignment that had drifted, plus a few trailing-newline fixes. No behaviour change — gofmt is semantics-preserving, and build/vet/test are green either side. Split out from the code-review fixes that follow so those stay reviewable instead of hiding inside a wall of realignment.
This commit is contained in:
@@ -3,17 +3,20 @@
|
|||||||
// outcomes mirror what live players hit.
|
// outcomes mirror what live players hit.
|
||||||
//
|
//
|
||||||
// Single run:
|
// Single run:
|
||||||
// expedition-sim [-class fighter] [-level 5] [-zone goblin_warrens]
|
//
|
||||||
// [-bank 1000] [-cap 50] [-log] [-data DIR]
|
// expedition-sim [-class fighter] [-level 5] [-zone goblin_warrens]
|
||||||
|
// [-bank 1000] [-cap 50] [-log] [-data DIR]
|
||||||
//
|
//
|
||||||
// Party run (N3/P7 — the leader is -class; followers clone it unless named):
|
// Party run (N3/P7 — the leader is -class; followers clone it unless named):
|
||||||
// expedition-sim -party 3 -level 15 -zone dragons_lair
|
//
|
||||||
// expedition-sim -party 2 -class cleric -party-classes fighter -level 16 ...
|
// expedition-sim -party 3 -level 15 -zone dragons_lair
|
||||||
|
// expedition-sim -party 2 -class cleric -party-classes fighter -level 16 ...
|
||||||
//
|
//
|
||||||
// Matrix mode (cartesian sweep over classes × levels × zones × N runs,
|
// Matrix mode (cartesian sweep over classes × levels × zones × N runs,
|
||||||
// one JSON object per stdout line, log suppressed by default):
|
// one JSON object per stdout line, log suppressed by default):
|
||||||
// expedition-sim -matrix -classes fighter,mage -levels 5,10 \
|
//
|
||||||
// -zones goblin_warrens,wolf_den -runs 20
|
// expedition-sim -matrix -classes fighter,mage -levels 5,10 \
|
||||||
|
// -zones goblin_warrens,wolf_den -runs 20
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -269,7 +272,6 @@ func matrixWorker(exe string, in <-chan matrixJob, out chan<- *plugin.SimResult,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func runOne(dataDir string, uid id.UserID, class plugin.DnDClass, level int, zone plugin.ZoneID, bank float64, cap, days int, followers []string) (*plugin.SimResult, error) {
|
func runOne(dataDir string, uid id.UserID, class plugin.DnDClass, level int, zone plugin.ZoneID, bank float64, cap, days int, followers []string) (*plugin.SimResult, error) {
|
||||||
runner, err := plugin.NewSimRunner(dataDir)
|
runner, err := plugin.NewSimRunner(dataDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ var (
|
|||||||
reSpaces = regexp.MustCompile(`\s+`)
|
reSpaces = regexp.MustCompile(`\s+`)
|
||||||
reOrphanMustMake = regexp.MustCompile(`(?i)\s+must\s+(?:make|succeed on|attempt)([.,;])`)
|
reOrphanMustMake = regexp.MustCompile(`(?i)\s+must\s+(?:make|succeed on|attempt)([.,;])`)
|
||||||
reOrphanTrailingAnd = regexp.MustCompile(`(?i),?\s+and\.`)
|
reOrphanTrailingAnd = regexp.MustCompile(`(?i),?\s+and\.`)
|
||||||
descScrub = []*regexp.Regexp{
|
descScrub = []*regexp.Regexp{
|
||||||
// Range / distance jargon.
|
// Range / distance jargon.
|
||||||
regexp.MustCompile(`(?i)\s*\bwithin range\b`),
|
regexp.MustCompile(`(?i)\s*\bwithin range\b`),
|
||||||
regexp.MustCompile(`(?i)\s*\bwithin \d+ feet(?: of [a-z ]+?)?`),
|
regexp.MustCompile(`(?i)\s*\bwithin \d+ feet(?: of [a-z ]+?)?`),
|
||||||
|
|||||||
@@ -93,11 +93,11 @@ func TestCleanDescRepairsOrphans(t *testing.T) {
|
|||||||
// TestStripNameParenthetical — bestiary R21 / magic-item alias names.
|
// TestStripNameParenthetical — bestiary R21 / magic-item alias names.
|
||||||
func TestStripNameParenthetical(t *testing.T) {
|
func TestStripNameParenthetical(t *testing.T) {
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
"Giant Rat (Diseased)": "Giant Rat",
|
"Giant Rat (Diseased)": "Giant Rat",
|
||||||
"Deep Gnome (Svirfneblin)": "Deep Gnome",
|
"Deep Gnome (Svirfneblin)": "Deep Gnome",
|
||||||
"Stone of Good Luck (Luckstone)": "Stone of Good Luck",
|
"Stone of Good Luck (Luckstone)": "Stone of Good Luck",
|
||||||
"Ordinary Name": "Ordinary Name",
|
"Ordinary Name": "Ordinary Name",
|
||||||
"": "",
|
"": "",
|
||||||
}
|
}
|
||||||
for in, want := range cases {
|
for in, want := range cases {
|
||||||
if got := stripNameParenthetical(in); got != want {
|
if got := stripNameParenthetical(in); got != want {
|
||||||
|
|||||||
@@ -88,18 +88,18 @@ func must(err error) {
|
|||||||
// open5eSpell is the subset of the Open5e v1 spell schema we consume. Unknown
|
// open5eSpell is the subset of the Open5e v1 spell schema we consume. Unknown
|
||||||
// fields are ignored; the vendored JSON keeps the full payload.
|
// fields are ignored; the vendored JSON keeps the full payload.
|
||||||
type open5eSpell struct {
|
type open5eSpell struct {
|
||||||
Slug string `json:"slug"`
|
Slug string `json:"slug"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Desc string `json:"desc"`
|
Desc string `json:"desc"`
|
||||||
HigherLevel string `json:"higher_level"`
|
HigherLevel string `json:"higher_level"`
|
||||||
Range string `json:"range"`
|
Range string `json:"range"`
|
||||||
Material string `json:"material"`
|
Material string `json:"material"`
|
||||||
CanBeCastAsRitual bool `json:"can_be_cast_as_ritual"`
|
CanBeCastAsRitual bool `json:"can_be_cast_as_ritual"`
|
||||||
Duration string `json:"duration"`
|
Duration string `json:"duration"`
|
||||||
RequiresConcentration bool `json:"requires_concentration"`
|
RequiresConcentration bool `json:"requires_concentration"`
|
||||||
CastingTime string `json:"casting_time"`
|
CastingTime string `json:"casting_time"`
|
||||||
LevelInt int `json:"level_int"`
|
LevelInt int `json:"level_int"`
|
||||||
School string `json:"school"`
|
School string `json:"school"`
|
||||||
// SpellLists is the structured class list, but the SRD dump leaves it
|
// SpellLists is the structured class list, but the SRD dump leaves it
|
||||||
// incomplete (no paladin entries at all). DndClass is the free-text
|
// incomplete (no paladin entries at all). DndClass is the free-text
|
||||||
// "Druid, Wizard" field and is the more complete source — mapClasses
|
// "Druid, Wizard" field and is the more complete source — mapClasses
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ import (
|
|||||||
|
|
||||||
type AdventurePlugin struct {
|
type AdventurePlugin struct {
|
||||||
Base
|
Base
|
||||||
euro *EuroPlugin
|
euro *EuroPlugin
|
||||||
xp *XPPlugin
|
xp *XPPlugin
|
||||||
achievements *AchievementsPlugin
|
achievements *AchievementsPlugin
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
dmToPlayer map[id.RoomID]id.UserID
|
dmToPlayer map[id.RoomID]id.UserID
|
||||||
pending sync.Map // userID string -> *advPendingInteraction
|
pending sync.Map // userID string -> *advPendingInteraction
|
||||||
userLocks sync.Map // userID string -> *sync.Mutex
|
userLocks sync.Map // userID string -> *sync.Mutex
|
||||||
@@ -37,8 +37,8 @@ type AdventurePlugin struct {
|
|||||||
hospitalNudges sync.Map // userID string -> time.Time (when to send nudge)
|
hospitalNudges sync.Map // userID string -> time.Time (when to send nudge)
|
||||||
craftResults sync.Map // userID string -> []CraftResult (pending craft results for narrative)
|
craftResults sync.Map // userID string -> []CraftResult (pending craft results for narrative)
|
||||||
treasureUndo sync.Map // userID string -> *advTreasureUndoToken (10-min auto-swap reversal)
|
treasureUndo sync.Map // userID string -> *advTreasureUndoToken (10-min auto-swap reversal)
|
||||||
morningHour int
|
morningHour int
|
||||||
summaryHour int
|
summaryHour int
|
||||||
|
|
||||||
// stopCh is closed by Stop() to signal all background tickers
|
// stopCh is closed by Stop() to signal all background tickers
|
||||||
// (morning/summary/midnight/event/rival/robbie/hospital/mortgage/
|
// (morning/summary/midnight/event/rival/robbie/hospital/mortgage/
|
||||||
@@ -1558,4 +1558,3 @@ func (p *AdventurePlugin) handleBoostCmd(ctx MessageContext) error {
|
|||||||
advSetBoost(true)
|
advSetBoost(true)
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "⚡ Double XP/money boost **enabled**! All adventure XP and loot values are doubled.")
|
return p.SendReply(ctx.RoomID, ctx.EventID, "⚡ Double XP/money boost **enabled**! All adventure XP and loot values are doubled.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -642,7 +642,7 @@ type AdvActionResult struct {
|
|||||||
FlavorKey string
|
FlavorKey string
|
||||||
EquipBroken []EquipmentSlot
|
EquipBroken []EquipmentSlot
|
||||||
NearDeath bool
|
NearDeath bool
|
||||||
StreakBonus int
|
StreakBonus int
|
||||||
CombatLog *CombatResult
|
CombatLog *CombatResult
|
||||||
// MasteryCrossings records equipment slots whose ActionsUsed crossed a
|
// MasteryCrossings records equipment slots whose ActionsUsed crossed a
|
||||||
// mastery threshold (advMasteryThresholds) on this action. The caller
|
// mastery threshold (advMasteryThresholds) on this action. The caller
|
||||||
|
|||||||
@@ -116,12 +116,12 @@ func loadAdvDailyActivity(date string) (map[id.UserID][]AdvDailyActivity, error)
|
|||||||
}
|
}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var (
|
var (
|
||||||
uid, zoneID string
|
uid, zoneID string
|
||||||
visitedJSON string
|
visitedJSON string
|
||||||
totalRooms int
|
totalRooms int
|
||||||
abandoned, bossDefeated int
|
abandoned, bossDefeated int
|
||||||
completedAt *time.Time
|
completedAt *time.Time
|
||||||
lastAction time.Time
|
lastAction time.Time
|
||||||
)
|
)
|
||||||
if err := rows.Scan(&uid, &zoneID, &visitedJSON, &totalRooms,
|
if err := rows.Scan(&uid, &zoneID, &visitedJSON, &totalRooms,
|
||||||
&abandoned, &bossDefeated, &completedAt, &lastAction); err != nil {
|
&abandoned, &bossDefeated, &completedAt, &lastAction); err != nil {
|
||||||
|
|||||||
@@ -16,20 +16,20 @@ import (
|
|||||||
// ── Arena Run State ─────────────────────────────────────────────────────────
|
// ── Arena Run State ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type ArenaRun struct {
|
type ArenaRun struct {
|
||||||
ID int64
|
ID int64
|
||||||
UserID id.UserID
|
UserID id.UserID
|
||||||
RoomID id.RoomID
|
RoomID id.RoomID
|
||||||
StartTier int
|
StartTier int
|
||||||
Tier int
|
Tier int
|
||||||
Round int
|
Round int
|
||||||
Status string // "active", "awaiting", "completed", "dead", "cashed_out"
|
Status string // "active", "awaiting", "completed", "dead", "cashed_out"
|
||||||
Earnings int64 // session total (multiplied euros accumulated across completed tiers)
|
Earnings int64 // session total (multiplied euros accumulated across completed tiers)
|
||||||
TierEarnings int64 // current tier's raw earnings (reset each tier)
|
TierEarnings int64 // current tier's raw earnings (reset each tier)
|
||||||
XPAccumulated int // session XP accumulator (raw, multiplied at payout)
|
XPAccumulated int // session XP accumulator (raw, multiplied at payout)
|
||||||
RoundsSurvived int
|
RoundsSurvived int
|
||||||
LastMonster string
|
LastMonster string
|
||||||
StartedAt time.Time
|
StartedAt time.Time
|
||||||
EndedAt *time.Time
|
EndedAt *time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Command Dispatch ────────────────────────────────────────────────────────
|
// ── Command Dispatch ────────────────────────────────────────────────────────
|
||||||
@@ -1015,8 +1015,8 @@ func loadArenaPersonalStats(userID id.UserID) *ArenaPersonalStats {
|
|||||||
|
|
||||||
type ArenaGearSet struct {
|
type ArenaGearSet struct {
|
||||||
Tier int
|
Tier int
|
||||||
SetKey string // DB key: "bloodied", "ironclad", etc.
|
SetKey string // DB key: "bloodied", "ironclad", etc.
|
||||||
SetName string // Display: "Bloodied", "Ironclad", etc.
|
SetName string // Display: "Bloodied", "Ironclad", etc.
|
||||||
HelmetName string
|
HelmetName string
|
||||||
Description string
|
Description string
|
||||||
DropRate float64
|
DropRate float64
|
||||||
|
|||||||
@@ -138,12 +138,12 @@ func TestArenaBossOutcome_WinSurfacesStagedLogAndBossDeath(t *testing.T) {
|
|||||||
|
|
||||||
victoryHeadline := "🏆 **" + monster.Name + "** falls (HP " + "60" + "→" + "42" + " / 60)."
|
victoryHeadline := "🏆 **" + monster.Name + "** falls (HP " + "60" + "→" + "42" + " / 60)."
|
||||||
outcome := renderBossOutcome(BossOutcomeInputs{
|
outcome := renderBossOutcome(BossOutcomeInputs{
|
||||||
ZoneID: ZoneArena,
|
ZoneID: ZoneArena,
|
||||||
RunID: "arena-stagedlog-test",
|
RunID: "arena-stagedlog-test",
|
||||||
RoomIdx: 11,
|
RoomIdx: 11,
|
||||||
Monster: monster,
|
Monster: monster,
|
||||||
Result: result,
|
Result: result,
|
||||||
PreHP: 60, PostHP: 42, MaxHP: 60,
|
PreHP: 60, PostHP: 42, MaxHP: 60,
|
||||||
PhaseTwoAt: arenaBossPhaseTwoAt(1),
|
PhaseTwoAt: arenaBossPhaseTwoAt(1),
|
||||||
Nat20s: 1,
|
Nat20s: 1,
|
||||||
Nat1s: 0,
|
Nat1s: 0,
|
||||||
|
|||||||
@@ -126,12 +126,12 @@ func renderArenaStatus(run *ArenaRun) string {
|
|||||||
// ── Leaderboard ─────────────────────────────────────────────────────────────
|
// ── Leaderboard ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type ArenaLeaderboardEntry struct {
|
type ArenaLeaderboardEntry struct {
|
||||||
DisplayName string
|
DisplayName string
|
||||||
TotalEarnings int64
|
TotalEarnings int64
|
||||||
HighestTier int
|
HighestTier int
|
||||||
Tier5Completions int
|
Tier5Completions int
|
||||||
TotalRuns int
|
TotalRuns int
|
||||||
TotalDeaths int
|
TotalDeaths int
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderArenaLeaderboard(season string, entries []ArenaLeaderboardEntry) string {
|
func renderArenaLeaderboard(season string, entries []ArenaLeaderboardEntry) string {
|
||||||
|
|||||||
@@ -318,15 +318,15 @@ func renderBabysitSummary(char *AdventureCharacter, logs []babysitLogEntry) stri
|
|||||||
// ── Babysit Log CRUD ────────────────────────────────────────────────────────
|
// ── Babysit Log CRUD ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type babysitLogEntry struct {
|
type babysitLogEntry struct {
|
||||||
ID int64
|
ID int64
|
||||||
UserID id.UserID
|
UserID id.UserID
|
||||||
LogDate string
|
LogDate string
|
||||||
Activity string
|
Activity string
|
||||||
Outcome string
|
Outcome string
|
||||||
GoldEarned int
|
GoldEarned int
|
||||||
XPGained int
|
XPGained int
|
||||||
ItemsDropped string
|
ItemsDropped string
|
||||||
RivalRefused string
|
RivalRefused string
|
||||||
}
|
}
|
||||||
|
|
||||||
func logBabysitActivity(userID id.UserID, activity, outcome string, gold, xp int, items string) {
|
func logBabysitActivity(userID id.UserID, activity, outcome string, gold, xp int, items string) {
|
||||||
|
|||||||
@@ -66,11 +66,11 @@ func TestRunBabysitDailyTrickle_AccumulatesAndLevels(t *testing.T) {
|
|||||||
defer func() { _ = recover() }()
|
defer func() { _ = recover() }()
|
||||||
|
|
||||||
char := &AdventureCharacter{
|
char := &AdventureCharacter{
|
||||||
UserID: id.UserID("@accum:example"),
|
UserID: id.UserID("@accum:example"),
|
||||||
PetType: "dog",
|
PetType: "dog",
|
||||||
PetName: "Rex",
|
PetName: "Rex",
|
||||||
PetLevel: 1, // needs 10 XP = 1000 centi-XP to L2
|
PetLevel: 1, // needs 10 XP = 1000 centi-XP to L2
|
||||||
PetXP: 900,
|
PetXP: 900,
|
||||||
BabysitActive: true,
|
BabysitActive: true,
|
||||||
}
|
}
|
||||||
expires := time.Now().UTC().Add(48 * time.Hour)
|
expires := time.Now().UTC().Add(48 * time.Hour)
|
||||||
|
|||||||
@@ -24,32 +24,32 @@ var allSlots = []EquipmentSlot{SlotWeapon, SlotArmor, SlotHelmet, SlotBoots, Slo
|
|||||||
// ── Core Types ───────────────────────────────────────────────────────────────
|
// ── Core Types ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type AdventureCharacter struct {
|
type AdventureCharacter struct {
|
||||||
UserID id.UserID
|
UserID id.UserID
|
||||||
DisplayName string
|
DisplayName string
|
||||||
CombatLevel int
|
CombatLevel int
|
||||||
MiningSkill int
|
MiningSkill int
|
||||||
ForagingSkill int
|
ForagingSkill int
|
||||||
FishingSkill int // v2
|
FishingSkill int // v2
|
||||||
CombatXP int
|
CombatXP int
|
||||||
MiningXP int
|
MiningXP int
|
||||||
ForagingXP int
|
ForagingXP int
|
||||||
FishingXP int // v2
|
FishingXP int // v2
|
||||||
Alive bool
|
Alive bool
|
||||||
DeadUntil *time.Time
|
DeadUntil *time.Time
|
||||||
ActionTakenToday bool
|
ActionTakenToday bool
|
||||||
HolidayActionTaken bool
|
HolidayActionTaken bool
|
||||||
CombatActionsUsed int
|
CombatActionsUsed int
|
||||||
HarvestActionsUsed int
|
HarvestActionsUsed int
|
||||||
ArenaWins int // v2
|
ArenaWins int // v2
|
||||||
ArenaLosses int // v2
|
ArenaLosses int // v2
|
||||||
InvasionScore int // v2
|
InvasionScore int // v2
|
||||||
Title string // v2
|
Title string // v2
|
||||||
CurrentStreak int
|
CurrentStreak int
|
||||||
BestStreak int
|
BestStreak int
|
||||||
LastActionDate string
|
LastActionDate string
|
||||||
GrudgeLocation string
|
GrudgeLocation string
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
LastActiveAt time.Time
|
LastActiveAt time.Time
|
||||||
DeathReprieveLast *time.Time
|
DeathReprieveLast *time.Time
|
||||||
MasterworkDropsReceived int
|
MasterworkDropsReceived int
|
||||||
RivalPool int
|
RivalPool int
|
||||||
@@ -78,27 +78,27 @@ type AdventureCharacter struct {
|
|||||||
HouseAutopay bool
|
HouseAutopay bool
|
||||||
HouseCurrentRate float64
|
HouseCurrentRate float64
|
||||||
// Pets
|
// Pets
|
||||||
PetType string
|
PetType string
|
||||||
PetName string
|
PetName string
|
||||||
PetXP int
|
PetXP int
|
||||||
PetLevel int
|
PetLevel int
|
||||||
PetArmorTier int
|
PetArmorTier int
|
||||||
PetChasedAway bool
|
PetChasedAway bool
|
||||||
PetReactivated bool
|
PetReactivated bool
|
||||||
PetArrived bool
|
PetArrived bool
|
||||||
MistyEncounterCount int
|
MistyEncounterCount int
|
||||||
MistyDonatedCount int
|
MistyDonatedCount int
|
||||||
ThomAnimalLineFired bool
|
ThomAnimalLineFired bool
|
||||||
PetSupplyShopUnlocked bool
|
PetSupplyShopUnlocked bool
|
||||||
PetLevel10Date string
|
PetLevel10Date string
|
||||||
PetMorningDefense bool
|
PetMorningDefense bool
|
||||||
AutoBabysit bool
|
AutoBabysit bool
|
||||||
AutoBabysitFocus string // mining|fishing|foraging — preferred skill for auto-babysit; "" defaults to weakest
|
AutoBabysitFocus string // mining|fishing|foraging — preferred skill for auto-babysit; "" defaults to weakest
|
||||||
TreasuresLocked bool // when true, treasure drops at cap are refused instead of auto-swapping
|
TreasuresLocked bool // when true, treasure drops at cap are refused instead of auto-swapping
|
||||||
StreakDecayed bool
|
StreakDecayed bool
|
||||||
CraftsSucceeded int
|
CraftsSucceeded int
|
||||||
DeathSource string // "adventure" | "arena" | "" (legacy/unknown — treated as adventure)
|
DeathSource string // "adventure" | "arena" | "" (legacy/unknown — treated as adventure)
|
||||||
DeathLocation string // human-readable location of last death; cleared on revive is not required
|
DeathLocation string // human-readable location of last death; cleared on revive is not required
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdvEquipment struct {
|
type AdvEquipment struct {
|
||||||
|
|||||||
@@ -17,26 +17,26 @@ import (
|
|||||||
type ConsumableEffect string
|
type ConsumableEffect string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
EffectHeal ConsumableEffect = "heal"
|
EffectHeal ConsumableEffect = "heal"
|
||||||
EffectDefBoost ConsumableEffect = "def_boost"
|
EffectDefBoost ConsumableEffect = "def_boost"
|
||||||
EffectAtkBoost ConsumableEffect = "atk_boost"
|
EffectAtkBoost ConsumableEffect = "atk_boost"
|
||||||
EffectWard ConsumableEffect = "ward"
|
EffectWard ConsumableEffect = "ward"
|
||||||
EffectSpeedBoost ConsumableEffect = "speed_boost"
|
EffectSpeedBoost ConsumableEffect = "speed_boost"
|
||||||
EffectCritBoost ConsumableEffect = "crit_boost"
|
EffectCritBoost ConsumableEffect = "crit_boost"
|
||||||
EffectFlatDmg ConsumableEffect = "flat_dmg"
|
EffectFlatDmg ConsumableEffect = "flat_dmg"
|
||||||
EffectSpore ConsumableEffect = "spore"
|
EffectSpore ConsumableEffect = "spore"
|
||||||
EffectReflect ConsumableEffect = "reflect"
|
EffectReflect ConsumableEffect = "reflect"
|
||||||
EffectAutoCrit ConsumableEffect = "auto_crit"
|
EffectAutoCrit ConsumableEffect = "auto_crit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConsumableDef struct {
|
type ConsumableDef struct {
|
||||||
Name string
|
Name string
|
||||||
Effect ConsumableEffect
|
Effect ConsumableEffect
|
||||||
Value float64 // meaning depends on effect
|
Value float64 // meaning depends on effect
|
||||||
Tier int
|
Tier int
|
||||||
Buyable bool
|
Buyable bool
|
||||||
Price int64
|
Price int64
|
||||||
Slot string // "offensive" or "defensive"
|
Slot string // "offensive" or "defensive"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConsumableItem is an AdvItem that has consumable properties.
|
// ConsumableItem is an AdvItem that has consumable properties.
|
||||||
|
|||||||
@@ -79,4 +79,3 @@ var blacksmithBrokenCondition = []string{
|
|||||||
"_looks at the condition, looks at you, looks back at the condition_ You know it costs more when you let it get like this. Of course you know. You just didn't care. That's fine. I care enough for both of us. It'll cost you.",
|
"_looks at the condition, looks at you, looks back at the condition_ You know it costs more when you let it get like this. Of course you know. You just didn't care. That's fine. I care enough for both of us. It'll cost you.",
|
||||||
"This could have been avoided with regular visits. _slides the cost estimate across the counter without breaking eye contact_",
|
"This could have been avoided with regular visits. _slides the cost estimate across the counter without breaking eye contact_",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -924,4 +924,4 @@ var DungeonExceptional = map[int][]string{
|
|||||||
"in a dungeon. Write it down. " +
|
"in a dungeon. Write it down. " +
|
||||||
"The dungeon will tell this story differently.",
|
"The dungeon will tell this story differently.",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,6 +275,7 @@ var PetCatDeflect = []string{
|
|||||||
"Your cat is already somewhere else.\n\nIt blinks.\n\nNot at you.\n\nAt nothing.\n\n" +
|
"Your cat is already somewhere else.\n\nIt blinks.\n\nNot at you.\n\nAt nothing.\n\n" +
|
||||||
"The way cats do.",
|
"The way cats do.",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fires randomly in the morning DM. Cat has left something.
|
// Fires randomly in the morning DM. Cat has left something.
|
||||||
// Results in a defense boost for the day.
|
// Results in a defense boost for the day.
|
||||||
// The cat is proud. The cat will never stop doing this.
|
// The cat is proud. The cat will never stop doing this.
|
||||||
|
|||||||
@@ -97,12 +97,12 @@ func TestAdvMasteryRowSegment(t *testing.T) {
|
|||||||
|
|
||||||
func TestAdvMasteryRollup(t *testing.T) {
|
func TestAdvMasteryRollup(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
used map[EquipmentSlot]int
|
used map[EquipmentSlot]int
|
||||||
wantCrossed int
|
wantCrossed int
|
||||||
wantMaxed int
|
wantMaxed int
|
||||||
wantBonus float64
|
wantBonus float64
|
||||||
wantAnyProgress bool
|
wantAnyProgress bool
|
||||||
}{
|
}{
|
||||||
{"empty", nil, 0, 0, 0, false},
|
{"empty", nil, 0, 0, 0, false},
|
||||||
{"sub-50 only", map[EquipmentSlot]int{SlotWeapon: 30}, 0, 0, 0, true},
|
{"sub-50 only", map[EquipmentSlot]int{SlotWeapon: 30}, 0, 0, 0, true},
|
||||||
@@ -240,12 +240,12 @@ func TestAdvTreasureIrreplaceable(t *testing.T) {
|
|||||||
|
|
||||||
func TestRenderCraftingTeaser_BracketBoundaries(t *testing.T) {
|
func TestRenderCraftingTeaser_BracketBoundaries(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
foraging int
|
foraging int
|
||||||
craftsSucceeded int
|
craftsSucceeded int
|
||||||
wantEmpty bool
|
wantEmpty bool
|
||||||
wantContainsAny []string // any of these (OR)
|
wantContainsAny []string // any of these (OR)
|
||||||
wantNotContains []string
|
wantNotContains []string
|
||||||
}{
|
}{
|
||||||
{"foraging 6 — out of pre-window", 6, 0, true, nil, nil},
|
{"foraging 6 — out of pre-window", 6, 0, true, nil, nil},
|
||||||
{"foraging 7 — pre-unlock with 3 levels left, plural", 7, 0, false, []string{"3 Foraging level"}, []string{"unlocks in 1 ", "unlocks in 0"}},
|
{"foraging 7 — pre-unlock with 3 levels left, plural", 7, 0, false, []string{"3 Foraging level"}, []string{"unlocks in 1 ", "unlocks in 0"}},
|
||||||
@@ -297,7 +297,7 @@ func TestCraftingReminderWeekday_Spread(t *testing.T) {
|
|||||||
|
|
||||||
func TestCraftingReminderWeekday_StableWithinWeek(t *testing.T) {
|
func TestCraftingReminderWeekday_StableWithinWeek(t *testing.T) {
|
||||||
uid := id.UserID("@stable:test")
|
uid := id.UserID("@stable:test")
|
||||||
mon := time.Date(2026, 5, 4, 0, 0, 0, 0, time.UTC) // Monday
|
mon := time.Date(2026, 5, 4, 0, 0, 0, 0, time.UTC) // Monday
|
||||||
sun := time.Date(2026, 5, 10, 23, 0, 0, 0, time.UTC) // Sunday same ISO week
|
sun := time.Date(2026, 5, 10, 23, 0, 0, 0, time.UTC) // Sunday same ISO week
|
||||||
if craftingReminderWeekday(uid, mon) != craftingReminderWeekday(uid, sun) {
|
if craftingReminderWeekday(uid, mon) != craftingReminderWeekday(uid, sun) {
|
||||||
t.Error("weekday should be stable within an ISO week")
|
t.Error("weekday should be stable within an ISO week")
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ func (p *AdventurePlugin) handleHospitalCmd(ctx MessageContext) error {
|
|||||||
|
|
||||||
// Store pending interaction
|
// Store pending interaction
|
||||||
p.pending.Store(string(ctx.Sender), &advPendingInteraction{
|
p.pending.Store(string(ctx.Sender), &advPendingInteraction{
|
||||||
Type: "hospital_pay",
|
Type: "hospital_pay",
|
||||||
Data: &advPendingHospitalPay{
|
Data: &advPendingHospitalPay{
|
||||||
Cost: afterInsurance,
|
Cost: afterInsurance,
|
||||||
Discounted: haggleOutcome == haggleWin,
|
Discounted: haggleOutcome == haggleWin,
|
||||||
@@ -277,7 +277,7 @@ func (p *AdventurePlugin) resolveHospitalPay(ctx MessageContext, interaction *ad
|
|||||||
gr := gamesRoom()
|
gr := gamesRoom()
|
||||||
if gr != "" {
|
if gr != "" {
|
||||||
name, _ := loadDisplayName(char.UserID)
|
name, _ := loadDisplayName(char.UserID)
|
||||||
p.SendMessage(gr, fmt.Sprintf(hospitalDitchAnnounce, name))
|
p.SendMessage(gr, fmt.Sprintf(hospitalDitchAnnounce, name))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,10 +144,10 @@ func TestHouseHPBonus(t *testing.T) {
|
|||||||
expected float64
|
expected float64
|
||||||
}{
|
}{
|
||||||
{0, 0},
|
{0, 0},
|
||||||
{1, 0}, // Base house — no bonus
|
{1, 0}, // Base house — no bonus
|
||||||
{2, 0.05}, // Livable
|
{2, 0.05}, // Livable
|
||||||
{3, 0.12}, // Comfortable
|
{3, 0.12}, // Comfortable
|
||||||
{4, 0.20}, // Established
|
{4, 0.20}, // Established
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
char := &AdventureCharacter{HouseTier: tt.tier}
|
char := &AdventureCharacter{HouseTier: tt.tier}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import (
|
|||||||
type MasterworkDef struct {
|
type MasterworkDef struct {
|
||||||
Slot EquipmentSlot
|
Slot EquipmentSlot
|
||||||
Activity AdvActivityType
|
Activity AdvActivityType
|
||||||
SkillSource string // "mining", "fishing", "foraging"
|
SkillSource string // "mining", "fishing", "foraging"
|
||||||
Tier int // 1-5, matches location tier
|
Tier int // 1-5, matches location tier
|
||||||
Name string
|
Name string
|
||||||
Description string // character sheet / trade listing
|
Description string // character sheet / trade listing
|
||||||
DropRate float64 // per-tier: 0.05, 0.04, 0.03, 0.02, 0.005
|
DropRate float64 // per-tier: 0.05, 0.04, 0.03, 0.02, 0.005
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ import (
|
|||||||
// ── NPC Constants ──────────────────────────────────────────────────────────
|
// ── NPC Constants ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const (
|
const (
|
||||||
mistyCost = 100
|
mistyCost = 100
|
||||||
arinaCost = 5000
|
arinaCost = 5000
|
||||||
npcCooldownDays = 7
|
npcCooldownDays = 7
|
||||||
npcEncounterChance = 0.075 // 7.5%
|
npcEncounterChance = 0.075 // 7.5%
|
||||||
npcBuffDuration = 7 * 24 * time.Hour
|
npcBuffDuration = 7 * 24 * time.Hour
|
||||||
|
|
||||||
// Arena effect chances per round
|
// Arena effect chances per round
|
||||||
mistyEffectChance = 0.20 // 20%
|
mistyEffectChance = 0.20 // 20%
|
||||||
@@ -337,10 +337,10 @@ func (p *AdventurePlugin) resolveArina(ctx MessageContext, char *AdventureCharac
|
|||||||
// Returns: extra text to append to round log, enemy HP modifier, player damage taken.
|
// Returns: extra text to append to round log, enemy HP modifier, player damage taken.
|
||||||
type npcArenaResult struct {
|
type npcArenaResult struct {
|
||||||
Text string
|
Text string
|
||||||
EnemyDmg int // damage dealt to enemy
|
EnemyDmg int // damage dealt to enemy
|
||||||
PlayerDmg int // damage dealt to player
|
PlayerDmg int // damage dealt to player
|
||||||
SniperKill bool // enemy instant kill
|
SniperKill bool // enemy instant kill
|
||||||
CondRepair int // equipment condition repair amount
|
CondRepair int // equipment condition repair amount
|
||||||
}
|
}
|
||||||
|
|
||||||
func npcCheckArenaEffects(char *AdventureCharacter, monsterName string) *npcArenaResult {
|
func npcCheckArenaEffects(char *AdventureCharacter, monsterName string) *npcArenaResult {
|
||||||
|
|||||||
@@ -729,12 +729,12 @@ type AdvPlayerDaySummary struct {
|
|||||||
ForagingSkill int
|
ForagingSkill int
|
||||||
FishingSkill int
|
FishingSkill int
|
||||||
|
|
||||||
Activity string
|
Activity string
|
||||||
Location string
|
Location string
|
||||||
Outcome string
|
Outcome string
|
||||||
LootValue int64
|
LootValue int64
|
||||||
IsDead bool
|
IsDead bool
|
||||||
DeadUntil string
|
DeadUntil string
|
||||||
// DeadUntilHours is the integer hours-from-now until revival, used by
|
// DeadUntilHours is the integer hours-from-now until revival, used by
|
||||||
// the standout-death template's {hours} placeholder. Computed when
|
// the standout-death template's {hours} placeholder. Computed when
|
||||||
// the summary row is built; 0 if not dead or already past revival.
|
// the summary row is built; 0 if not dead or already past revival.
|
||||||
@@ -744,11 +744,11 @@ type AdvPlayerDaySummary struct {
|
|||||||
// {duration} placeholder for templates that want precision over the
|
// {duration} placeholder for templates that want precision over the
|
||||||
// rounded {hours} count.
|
// rounded {hours} count.
|
||||||
DeadUntilDuration string
|
DeadUntilDuration string
|
||||||
IsResting bool
|
IsResting bool
|
||||||
SummaryLine string
|
SummaryLine string
|
||||||
HolidayActions int // 0 = not holiday or no action; 1 = took one; 2 = took both
|
HolidayActions int // 0 = not holiday or no action; 1 = took one; 2 = took both
|
||||||
DeathSource string
|
DeathSource string
|
||||||
DeathLocation string
|
DeathLocation string
|
||||||
}
|
}
|
||||||
|
|
||||||
// advPlayerHeadline renders the per-player headline for the daily report.
|
// advPlayerHeadline renders the per-player headline for the daily report.
|
||||||
@@ -920,7 +920,6 @@ func renderAdvDailySummary(date string, tb *TwinBeeResult, tbRewards TwinBeeRewa
|
|||||||
sb.WriteString("\n")
|
sb.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Standout
|
// Standout
|
||||||
if bestPlayer != nil && bestPlayer.LootValue > 0 {
|
if bestPlayer != nil && bestPlayer.LootValue > 0 {
|
||||||
pool := SummaryStandoutGood
|
pool := SummaryStandoutGood
|
||||||
@@ -984,8 +983,8 @@ func renderAdvLeaderboard(chars []AdvLeaderboardEntry) string {
|
|||||||
|
|
||||||
// Sort by score
|
// Sort by score
|
||||||
type entry struct {
|
type entry struct {
|
||||||
Name string
|
Name string
|
||||||
Score int
|
Score int
|
||||||
Levels string
|
Levels string
|
||||||
Streak int
|
Streak int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -281,17 +281,17 @@ func TestLuigiCategoryIntros_Coverage(t *testing.T) {
|
|||||||
|
|
||||||
func TestLuigiFlavorPools_NonEmpty(t *testing.T) {
|
func TestLuigiFlavorPools_NonEmpty(t *testing.T) {
|
||||||
pools := map[string][]string{
|
pools := map[string][]string{
|
||||||
"luigiGreetings": luigiGreetings,
|
"luigiGreetings": luigiGreetings,
|
||||||
"luigiPurchaseConfirm": luigiPurchaseConfirm,
|
"luigiPurchaseConfirm": luigiPurchaseConfirm,
|
||||||
"luigiTier5Confirm": luigiTier5Confirm,
|
"luigiTier5Confirm": luigiTier5Confirm,
|
||||||
"luigiComboConfirm": luigiComboConfirm,
|
"luigiComboConfirm": luigiComboConfirm,
|
||||||
"luigiInsufficientFunds": luigiInsufficientFunds,
|
"luigiInsufficientFunds": luigiInsufficientFunds,
|
||||||
"luigiBrowseTimeout": luigiBrowseTimeout,
|
"luigiBrowseTimeout": luigiBrowseTimeout,
|
||||||
"luigiMaxedOut": luigiMaxedOut,
|
"luigiMaxedOut": luigiMaxedOut,
|
||||||
"luigiMasterworkAck": luigiMasterworkAck,
|
"luigiMasterworkAck": luigiMasterworkAck,
|
||||||
"luigiShowAllComment": luigiShowAllComment,
|
"luigiShowAllComment": luigiShowAllComment,
|
||||||
"luigiCommentary": luigiCommentary,
|
"luigiCommentary": luigiCommentary,
|
||||||
"luigiCancellation": luigiCancellation,
|
"luigiCancellation": luigiCancellation,
|
||||||
}
|
}
|
||||||
for name, pool := range pools {
|
for name, pool := range pools {
|
||||||
if len(pool) == 0 {
|
if len(pool) == 0 {
|
||||||
|
|||||||
@@ -231,13 +231,13 @@ func TestCalculateAdvProbabilities_BetterGearReducesDeath(t *testing.T) {
|
|||||||
|
|
||||||
func TestAdvEffectiveSkill(t *testing.T) {
|
func TestAdvEffectiveSkill(t *testing.T) {
|
||||||
char := &AdventureCharacter{
|
char := &AdventureCharacter{
|
||||||
CombatLevel: 10,
|
CombatLevel: 10,
|
||||||
MiningSkill: 15,
|
MiningSkill: 15,
|
||||||
ForagingSkill: 20,
|
ForagingSkill: 20,
|
||||||
}
|
}
|
||||||
bonuses := &AdvBonusSummary{
|
bonuses := &AdvBonusSummary{
|
||||||
CombatBonus: 5,
|
CombatBonus: 5,
|
||||||
MiningBonus: 3,
|
MiningBonus: 3,
|
||||||
ForagingBonus: 0,
|
ForagingBonus: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -322,11 +322,11 @@ func (p *AdventurePlugin) distributeTwinBeeRewards(result *TwinBeeResult) TwinBe
|
|||||||
// ── TwinBee Gifts (Temporary Buffs) ──────────────────────────────────────────
|
// ── TwinBee Gifts (Temporary Buffs) ──────────────────────────────────────────
|
||||||
|
|
||||||
type twinBeeGiftDef struct {
|
type twinBeeGiftDef struct {
|
||||||
BuffType string
|
BuffType string
|
||||||
BuffName string
|
BuffName string
|
||||||
Modifier float64
|
Modifier float64
|
||||||
Duration time.Duration
|
Duration time.Duration
|
||||||
Flavor string
|
Flavor string
|
||||||
}
|
}
|
||||||
|
|
||||||
var twinBeeGifts = []twinBeeGiftDef{
|
var twinBeeGifts = []twinBeeGiftDef{
|
||||||
|
|||||||
@@ -20,15 +20,15 @@ import (
|
|||||||
|
|
||||||
// jikanAnime represents an anime entry from the Jikan API.
|
// jikanAnime represents an anime entry from the Jikan API.
|
||||||
type jikanAnime struct {
|
type jikanAnime struct {
|
||||||
MalID int `json:"mal_id"`
|
MalID int `json:"mal_id"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
TitleEng string `json:"title_english"`
|
TitleEng string `json:"title_english"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Episodes int `json:"episodes"`
|
Episodes int `json:"episodes"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Score float64 `json:"score"`
|
Score float64 `json:"score"`
|
||||||
Synopsis string `json:"synopsis"`
|
Synopsis string `json:"synopsis"`
|
||||||
Aired struct {
|
Aired struct {
|
||||||
String string `json:"string"`
|
String string `json:"string"`
|
||||||
From string `json:"from"`
|
From string `json:"from"`
|
||||||
|
|||||||
@@ -29,51 +29,51 @@ const (
|
|||||||
thLinkmasterMinMsgs = 50
|
thLinkmasterMinMsgs = 50
|
||||||
|
|
||||||
// Temporal
|
// Temporal
|
||||||
thNightOwlPct = 40
|
thNightOwlPct = 40
|
||||||
thNightOwlMinMsgs = 100
|
thNightOwlMinMsgs = 100
|
||||||
thEarlyBirdPct = 40
|
thEarlyBirdPct = 40
|
||||||
thEarlyBirdMinMsgs = 100
|
thEarlyBirdMinMsgs = 100
|
||||||
|
|
||||||
// Emotional (LLM-gated)
|
// Emotional (LLM-gated)
|
||||||
thEmotionalMinClassified = 100
|
thEmotionalMinClassified = 100
|
||||||
thCheerleaderPosPct = 50
|
thCheerleaderPosPct = 50
|
||||||
thPhilosopherNeutPct = 40
|
thPhilosopherNeutPct = 40
|
||||||
thPhilosopherQPct = 10
|
thPhilosopherQPct = 10
|
||||||
thPhilosopherAvgWords = 8
|
thPhilosopherAvgWords = 8
|
||||||
thAgitatorNegPct = 30
|
thAgitatorNegPct = 30
|
||||||
thAgitatorMinMsgs = 200
|
thAgitatorMinMsgs = 200
|
||||||
thWildcardStdDev = 0.5
|
thWildcardStdDev = 0.5
|
||||||
thWildcardMinClassified = 150
|
thWildcardMinClassified = 150
|
||||||
thHypeMachineExclPct = 20
|
thHypeMachineExclPct = 20
|
||||||
thHypeMachinePosPct = 60
|
thHypeMachinePosPct = 60
|
||||||
|
|
||||||
// Economy
|
// Economy
|
||||||
thBrokeSpiritedMaxBalance = 100.0
|
thBrokeSpiritedMaxBalance = 100.0
|
||||||
thDegenerateMinLosses = 10
|
thDegenerateMinLosses = 10
|
||||||
|
|
||||||
// Games
|
// Games
|
||||||
thSharkWinRate = 55
|
thSharkWinRate = 55
|
||||||
thSharkMinGames = 15
|
thSharkMinGames = 15
|
||||||
thWordleMinPuzzles = 10
|
thWordleMinPuzzles = 10
|
||||||
thArenaChampMinTier = 4
|
thArenaChampMinTier = 4
|
||||||
thArenaChampWinRate = 50
|
thArenaChampWinRate = 50
|
||||||
thArenaCowardMinRuns = 3
|
thArenaCowardMinRuns = 3
|
||||||
thArenaCowardMaxAvgTier = 2
|
thArenaCowardMaxAvgTier = 2
|
||||||
thTriviaNerdMinCorrect = 10
|
thTriviaNerdMinCorrect = 10
|
||||||
|
|
||||||
// Adventure
|
// Adventure
|
||||||
thAdvMinDays = 10
|
thAdvMinDays = 10
|
||||||
thAdvDiverseMinDays = 15
|
thAdvDiverseMinDays = 15
|
||||||
thAdvDiverseMinTypes = 3
|
thAdvDiverseMinTypes = 3
|
||||||
thAdvDiverseMaxShare = 40
|
thAdvDiverseMaxShare = 40
|
||||||
thGearheadMinMasterwork = 3
|
thGearheadMinMasterwork = 3
|
||||||
|
|
||||||
// Communication (vocabulary)
|
// Communication (vocabulary)
|
||||||
thWordsmithMinFancyWords = 10
|
thWordsmithMinFancyWords = 10
|
||||||
|
|
||||||
// Social
|
// Social
|
||||||
thPatronMinRepGiven = 5
|
thPatronMinRepGiven = 5
|
||||||
thPatronRatioMultiplier = 2
|
thPatronRatioMultiplier = 2
|
||||||
|
|
||||||
// Display
|
// Display
|
||||||
maxDisplayArchetypes = 6
|
maxDisplayArchetypes = 6
|
||||||
@@ -83,13 +83,13 @@ const (
|
|||||||
|
|
||||||
var archetypeFlavors = map[string]string{
|
var archetypeFlavors = map[string]string{
|
||||||
// Communication
|
// Communication
|
||||||
"Novelist": "Writes in paragraphs. Has opinions. Probably re-reads their own messages.",
|
"Novelist": "Writes in paragraphs. Has opinions. Probably re-reads their own messages.",
|
||||||
"Minimalist": "Says a lot with very little. You're never sure if they're fine or not.",
|
"Minimalist": "Says a lot with very little. You're never sure if they're fine or not.",
|
||||||
"Inquisitor": "Always asking. Never satisfied with the first answer. Probably has follow-ups.",
|
"Inquisitor": "Always asking. Never satisfied with the first answer. Probably has follow-ups.",
|
||||||
"Enthusiast": "Genuinely excited about things. All the things. Possibly all at once.",
|
"Enthusiast": "Genuinely excited about things. All the things. Possibly all at once.",
|
||||||
"Chatterbox": "Has thoughts. Many thoughts. Shares them all. You wouldn't have it any other way.",
|
"Chatterbox": "Has thoughts. Many thoughts. Shares them all. You wouldn't have it any other way.",
|
||||||
"Linkmaster": "The community's unofficial curator. Their tab count is not your business.",
|
"Linkmaster": "The community's unofficial curator. Their tab count is not your business.",
|
||||||
"Wordsmith": "Uses words most people have to look up. The thesaurus fears them.",
|
"Wordsmith": "Uses words most people have to look up. The thesaurus fears them.",
|
||||||
|
|
||||||
// Temporal
|
// Temporal
|
||||||
"Night Owl": "Awake when they probably shouldn't be. Thriving despite all evidence.",
|
"Night Owl": "Awake when they probably shouldn't be. Thriving despite all evidence.",
|
||||||
@@ -103,8 +103,8 @@ var archetypeFlavors = map[string]string{
|
|||||||
"Hype Machine": "Arrived and immediately made everything louder. The room is better for it.",
|
"Hype Machine": "Arrived and immediately made everything louder. The room is better for it.",
|
||||||
|
|
||||||
// Economy
|
// Economy
|
||||||
"Whale": "Has money. Spends money. Has more money somehow. The math is unclear.",
|
"Whale": "Has money. Spends money. Has more money somehow. The math is unclear.",
|
||||||
"Degenerate": "Knows exactly what they're doing. Does it anyway. Respects it.",
|
"Degenerate": "Knows exactly what they're doing. Does it anyway. Respects it.",
|
||||||
"Broke But Spirited": "Down but not out. The pot fears them anyway.",
|
"Broke But Spirited": "Down but not out. The pot fears them anyway.",
|
||||||
|
|
||||||
// Games
|
// Games
|
||||||
@@ -221,16 +221,16 @@ type userData struct {
|
|||||||
userID string
|
userID string
|
||||||
|
|
||||||
// user_stats
|
// user_stats
|
||||||
totalMsgs int
|
totalMsgs int
|
||||||
totalWords int
|
totalWords int
|
||||||
totalLinks int
|
totalLinks int
|
||||||
totalImages int
|
totalImages int
|
||||||
totalQuestions int
|
totalQuestions int
|
||||||
totalExcl int
|
totalExcl int
|
||||||
totalEmojis int
|
totalEmojis int
|
||||||
nightMsgs int
|
nightMsgs int
|
||||||
morningMsgs int
|
morningMsgs int
|
||||||
fancyWords int
|
fancyWords int
|
||||||
|
|
||||||
// sentiment_stats
|
// sentiment_stats
|
||||||
sentPositive int
|
sentPositive int
|
||||||
@@ -242,7 +242,7 @@ type userData struct {
|
|||||||
sentVariance float64
|
sentVariance float64
|
||||||
|
|
||||||
// economy
|
// economy
|
||||||
balance float64
|
balance float64
|
||||||
gamblingLosses int
|
gamblingLosses int
|
||||||
recentGaming bool
|
recentGaming bool
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ type userData struct {
|
|||||||
hmPlayed, hmWon int
|
hmPlayed, hmWon int
|
||||||
holdemPlayed int
|
holdemPlayed int
|
||||||
holdemNetPositive bool
|
holdemNetPositive bool
|
||||||
unoSoloPlayed, unoSoloWon int
|
unoSoloPlayed, unoSoloWon int
|
||||||
unoMultiPlayed, unoMultiWon int
|
unoMultiPlayed, unoMultiWon int
|
||||||
wordlePlayed int
|
wordlePlayed int
|
||||||
triviaCorrect int
|
triviaCorrect int
|
||||||
@@ -263,13 +263,13 @@ type userData struct {
|
|||||||
arenaAvgCashTier float64
|
arenaAvgCashTier float64
|
||||||
|
|
||||||
// adventure
|
// adventure
|
||||||
advDays int
|
advDays int
|
||||||
advActivities map[string]int // activity_type -> count
|
advActivities map[string]int // activity_type -> count
|
||||||
masterworkCount int
|
masterworkCount int
|
||||||
|
|
||||||
// social
|
// social
|
||||||
repGiven int
|
repGiven int
|
||||||
repReceived int
|
repReceived int
|
||||||
reactionsGiven int
|
reactionsGiven int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,327 +14,327 @@ package plugin
|
|||||||
|
|
||||||
func buildTunedBestiarySRD() map[string]DnDMonsterTemplate {
|
func buildTunedBestiarySRD() map[string]DnDMonsterTemplate {
|
||||||
return map[string]DnDMonsterTemplate{
|
return map[string]DnDMonsterTemplate{
|
||||||
"awakened_shrub": {ID: "awakened_shrub", Name: "Awakened Shrub", CR: 0, HP: 10, AC: 10, Attack: 1, AttackBonus: 1, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: False Appearance"},
|
"awakened_shrub": {ID: "awakened_shrub", Name: "Awakened Shrub", CR: 0, HP: 10, AC: 10, Attack: 1, AttackBonus: 1, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: False Appearance"},
|
||||||
"baboon": {ID: "baboon", Name: "Baboon", CR: 0, HP: 3, AC: 12, Attack: 1, AttackBonus: 1, Speed: 12, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Pack Tactics"},
|
"baboon": {ID: "baboon", Name: "Baboon", CR: 0, HP: 3, AC: 12, Attack: 1, AttackBonus: 1, Speed: 12, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Pack Tactics"},
|
||||||
"badger": {ID: "badger", Name: "Badger", CR: 0, HP: 3, AC: 10, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Smell"},
|
"badger": {ID: "badger", Name: "Badger", CR: 0, HP: 3, AC: 10, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Smell"},
|
||||||
"bat": {ID: "bat", Name: "Bat", CR: 0, HP: 1, AC: 12, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, Keen Hearing"},
|
"bat": {ID: "bat", Name: "Bat", CR: 0, HP: 1, AC: 12, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, Keen Hearing"},
|
||||||
"cat": {ID: "cat", Name: "Cat", CR: 0, HP: 2, AC: 12, Attack: 1, AttackBonus: 0, Speed: 16, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Smell"},
|
"cat": {ID: "cat", Name: "Cat", CR: 0, HP: 2, AC: 12, Attack: 1, AttackBonus: 0, Speed: 16, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Smell"},
|
||||||
"commoner": {ID: "commoner", Name: "Commoner", CR: 0, HP: 4, AC: 10, Attack: 1, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"commoner": {ID: "commoner", Name: "Commoner", CR: 0, HP: 4, AC: 10, Attack: 1, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"crab": {ID: "crab", Name: "Crab", CR: 0, HP: 2, AC: 11, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
|
"crab": {ID: "crab", Name: "Crab", CR: 0, HP: 2, AC: 11, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
|
||||||
"deer": {ID: "deer", Name: "Deer", CR: 0, HP: 4, AC: 13, Attack: 1, AttackBonus: 2, Speed: 18, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"deer": {ID: "deer", Name: "Deer", CR: 0, HP: 4, AC: 13, Attack: 1, AttackBonus: 2, Speed: 18, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"eagle": {ID: "eagle", Name: "Eagle", CR: 0, HP: 3, AC: 12, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Sight"},
|
"eagle": {ID: "eagle", Name: "Eagle", CR: 0, HP: 3, AC: 12, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Sight"},
|
||||||
"frog": {ID: "frog", Name: "Frog", CR: 0, HP: 1, AC: 11, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Amphibious, Standing Leap"},
|
"frog": {ID: "frog", Name: "Frog", CR: 0, HP: 1, AC: 11, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Amphibious, Standing Leap"},
|
||||||
"giant_fire_beetle": {ID: "giant_fire_beetle", Name: "Giant Fire Beetle", CR: 0, HP: 4, AC: 13, Attack: 1, AttackBonus: 1, Speed: 12, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Illumination"},
|
"giant_fire_beetle": {ID: "giant_fire_beetle", Name: "Giant Fire Beetle", CR: 0, HP: 4, AC: 13, Attack: 1, AttackBonus: 1, Speed: 12, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Illumination"},
|
||||||
"goat": {ID: "goat", Name: "Goat", CR: 0, HP: 4, AC: 10, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Sure-Footed"},
|
"goat": {ID: "goat", Name: "Goat", CR: 0, HP: 4, AC: 10, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Sure-Footed"},
|
||||||
"hawk": {ID: "hawk", Name: "Hawk", CR: 0, HP: 1, AC: 13, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Sight"},
|
"hawk": {ID: "hawk", Name: "Hawk", CR: 0, HP: 1, AC: 13, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Sight"},
|
||||||
"homunculus": {ID: "homunculus", Name: "Homunculus", CR: 0, HP: 5, AC: 13, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Telepathic Bond"},
|
"homunculus": {ID: "homunculus", Name: "Homunculus", CR: 0, HP: 5, AC: 13, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Telepathic Bond"},
|
||||||
"hyena": {ID: "hyena", Name: "Hyena", CR: 0, HP: 5, AC: 11, Attack: 1, AttackBonus: 2, Speed: 18, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Pack Tactics"},
|
"hyena": {ID: "hyena", Name: "Hyena", CR: 0, HP: 5, AC: 11, Attack: 1, AttackBonus: 2, Speed: 18, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Pack Tactics"},
|
||||||
"jackal": {ID: "jackal", Name: "Jackal", CR: 0, HP: 3, AC: 12, Attack: 1, AttackBonus: 1, Speed: 16, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics"},
|
"jackal": {ID: "jackal", Name: "Jackal", CR: 0, HP: 3, AC: 12, Attack: 1, AttackBonus: 1, Speed: 16, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics"},
|
||||||
"lemure": {ID: "lemure", Name: "Lemure", CR: 0, HP: 13, AC: 10, Attack: 1, AttackBonus: 3, Speed: 6, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Traits: Devil's Sight, Hellish Rejuvenation"},
|
"lemure": {ID: "lemure", Name: "Lemure", CR: 0, HP: 13, AC: 10, Attack: 1, AttackBonus: 3, Speed: 6, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Traits: Devil's Sight, Hellish Rejuvenation"},
|
||||||
"lizard": {ID: "lizard", Name: "Lizard", CR: 0, HP: 2, AC: 10, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"lizard": {ID: "lizard", Name: "Lizard", CR: 0, HP: 2, AC: 10, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"octopus": {ID: "octopus", Name: "Octopus", CR: 0, HP: 3, AC: 12, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Hold Breath, Underwater Camouflage, Water Breathing"},
|
"octopus": {ID: "octopus", Name: "Octopus", CR: 0, HP: 3, AC: 12, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Hold Breath, Underwater Camouflage, Water Breathing"},
|
||||||
"owl": {ID: "owl", Name: "Owl", CR: 0, HP: 1, AC: 11, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Flyby, Keen Hearing and Sight"},
|
"owl": {ID: "owl", Name: "Owl", CR: 0, HP: 1, AC: 11, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Flyby, Keen Hearing and Sight"},
|
||||||
"quipper": {ID: "quipper", Name: "Quipper", CR: 0, HP: 1, AC: 13, Attack: 1, AttackBonus: 0, Speed: 16, BlockRate: 0.05, XPValue: 10, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Blood Frenzy, Water Breathing"},
|
"quipper": {ID: "quipper", Name: "Quipper", CR: 0, HP: 1, AC: 13, Attack: 1, AttackBonus: 0, Speed: 16, BlockRate: 0.05, XPValue: 10, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Blood Frenzy, Water Breathing"},
|
||||||
"rat": {ID: "rat", Name: "Rat", CR: 0, HP: 1, AC: 10, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Smell"},
|
"rat": {ID: "rat", Name: "Rat", CR: 0, HP: 1, AC: 10, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Smell"},
|
||||||
"raven": {ID: "raven", Name: "Raven", CR: 0, HP: 1, AC: 12, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Mimicry"},
|
"raven": {ID: "raven", Name: "Raven", CR: 0, HP: 1, AC: 12, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Mimicry"},
|
||||||
"scorpion": {ID: "scorpion", Name: "Scorpion", CR: 0, HP: 1, AC: 11, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"scorpion": {ID: "scorpion", Name: "Scorpion", CR: 0, HP: 1, AC: 11, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"sea_horse": {ID: "sea_horse", Name: "Sea Horse", CR: 0, HP: 1, AC: 11, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Water Breathing"},
|
"sea_horse": {ID: "sea_horse", Name: "Sea Horse", CR: 0, HP: 1, AC: 11, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Water Breathing"},
|
||||||
"shrieker": {ID: "shrieker", Name: "Shrieker", CR: 0, HP: 13, AC: 10, Attack: 1, AttackBonus: 0, Speed: 10, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: False Appearance"},
|
"shrieker": {ID: "shrieker", Name: "Shrieker", CR: 0, HP: 13, AC: 10, Attack: 1, AttackBonus: 0, Speed: 10, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: False Appearance"},
|
||||||
"spider": {ID: "spider", Name: "Spider", CR: 0, HP: 1, AC: 12, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Spider Climb, Web Sense, Web Walker"},
|
"spider": {ID: "spider", Name: "Spider", CR: 0, HP: 1, AC: 12, Attack: 1, AttackBonus: 0, Speed: 8, BlockRate: 0, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Spider Climb, Web Sense, Web Walker"},
|
||||||
"vulture": {ID: "vulture", Name: "Vulture", CR: 0, HP: 5, AC: 10, Attack: 1, AttackBonus: 2, Speed: 6, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Sight and Smell, Pack Tactics"},
|
"vulture": {ID: "vulture", Name: "Vulture", CR: 0, HP: 5, AC: 10, Attack: 1, AttackBonus: 2, Speed: 6, BlockRate: 0, XPValue: 10, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Sight and Smell, Pack Tactics"},
|
||||||
"weasel": {ID: "weasel", Name: "Weasel", CR: 0, HP: 1, AC: 13, Attack: 1, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"},
|
"weasel": {ID: "weasel", Name: "Weasel", CR: 0, HP: 1, AC: 13, Attack: 1, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 10, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"},
|
||||||
"bandit": {ID: "bandit", Name: "Bandit", CR: 0.125, HP: 11, AC: 12, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"bandit": {ID: "bandit", Name: "Bandit", CR: 0.125, HP: 11, AC: 12, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"blood_hawk": {ID: "blood_hawk", Name: "Blood Hawk", CR: 0.125, HP: 7, AC: 12, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 25, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Sight, Pack Tactics"},
|
"blood_hawk": {ID: "blood_hawk", Name: "Blood Hawk", CR: 0.125, HP: 7, AC: 12, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 25, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Sight, Pack Tactics"},
|
||||||
"camel": {ID: "camel", Name: "Camel", CR: 0.125, HP: 15, AC: 10, Attack: 1, AttackBonus: 5, Speed: 18, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"camel": {ID: "camel", Name: "Camel", CR: 0.125, HP: 15, AC: 10, Attack: 1, AttackBonus: 5, Speed: 18, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"cultist": {ID: "cultist", Name: "Cultist", CR: 0.125, HP: 9, AC: 12, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Dark Devotion"},
|
"cultist": {ID: "cultist", Name: "Cultist", CR: 0.125, HP: 9, AC: 12, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Dark Devotion"},
|
||||||
"flying_snake": {ID: "flying_snake", Name: "Flying Snake", CR: 0.125, HP: 5, AC: 14, Attack: 1, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 25, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Flyby"},
|
"flying_snake": {ID: "flying_snake", Name: "Flying Snake", CR: 0.125, HP: 5, AC: 14, Attack: 1, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 25, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Flyby"},
|
||||||
"giant_crab": {ID: "giant_crab", Name: "Giant Crab", CR: 0.125, HP: 13, AC: 15, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0.1, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
|
"giant_crab": {ID: "giant_crab", Name: "Giant Crab", CR: 0.125, HP: 13, AC: 15, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0.1, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
|
||||||
"giant_rat": {ID: "giant_rat", Name: "Giant Rat", CR: 0.125, HP: 7, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 25, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Smell, Pack Tactics"},
|
"giant_rat": {ID: "giant_rat", Name: "Giant Rat", CR: 0.125, HP: 7, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 25, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Smell, Pack Tactics"},
|
||||||
"giant_rat_diseased": {ID: "giant_rat_diseased", Name: "Giant Rat", CR: 0.125, HP: 7, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"giant_rat_diseased": {ID: "giant_rat_diseased", Name: "Giant Rat", CR: 0.125, HP: 7, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"giant_weasel": {ID: "giant_weasel", Name: "Giant Weasel", CR: 0.125, HP: 9, AC: 13, Attack: 1, AttackBonus: 5, Speed: 16, BlockRate: 0.05, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"},
|
"giant_weasel": {ID: "giant_weasel", Name: "Giant Weasel", CR: 0.125, HP: 9, AC: 13, Attack: 1, AttackBonus: 5, Speed: 16, BlockRate: 0.05, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"},
|
||||||
"guard": {ID: "guard", Name: "Guard", CR: 0.125, HP: 11, AC: 16, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0.1, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"guard": {ID: "guard", Name: "Guard", CR: 0.125, HP: 11, AC: 16, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0.1, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"kobold": {ID: "kobold", Name: "Kobold", CR: 0.125, HP: 5, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 25, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Sunlight Sensitivity, Pack Tactics"},
|
"kobold": {ID: "kobold", Name: "Kobold", CR: 0.125, HP: 5, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 25, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Sunlight Sensitivity, Pack Tactics"},
|
||||||
"mastiff": {ID: "mastiff", Name: "Mastiff", CR: 0.125, HP: 5, AC: 12, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"},
|
"mastiff": {ID: "mastiff", Name: "Mastiff", CR: 0.125, HP: 5, AC: 12, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"},
|
||||||
"merfolk": {ID: "merfolk", Name: "Merfolk", CR: 0.125, HP: 11, AC: 11, Attack: 1, AttackBonus: 2, Speed: 6, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
|
"merfolk": {ID: "merfolk", Name: "Merfolk", CR: 0.125, HP: 11, AC: 11, Attack: 1, AttackBonus: 2, Speed: 6, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
|
||||||
"mule": {ID: "mule", Name: "Mule", CR: 0.125, HP: 11, AC: 10, Attack: 1, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Beast of Burden, Sure-Footed"},
|
"mule": {ID: "mule", Name: "Mule", CR: 0.125, HP: 11, AC: 10, Attack: 1, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Beast of Burden, Sure-Footed"},
|
||||||
"noble": {ID: "noble", Name: "Noble", CR: 0.125, HP: 9, AC: 15, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0.1, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"noble": {ID: "noble", Name: "Noble", CR: 0.125, HP: 9, AC: 15, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0.1, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"poisonous_snake": {ID: "poisonous_snake", Name: "Poisonous Snake", CR: 0.125, HP: 2, AC: 13, Attack: 1, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"poisonous_snake": {ID: "poisonous_snake", Name: "Poisonous Snake", CR: 0.125, HP: 2, AC: 13, Attack: 1, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"pony": {ID: "pony", Name: "Pony", CR: 0.125, HP: 11, AC: 10, Attack: 1, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"pony": {ID: "pony", Name: "Pony", CR: 0.125, HP: 11, AC: 10, Attack: 1, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"stirge": {ID: "stirge", Name: "Stirge", CR: 0.125, HP: 2, AC: 14, Attack: 1, AttackBonus: 5, Speed: 6, BlockRate: 0.05, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"stirge": {ID: "stirge", Name: "Stirge", CR: 0.125, HP: 2, AC: 14, Attack: 1, AttackBonus: 5, Speed: 6, BlockRate: 0.05, XPValue: 25, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"tribal_warrior": {ID: "tribal_warrior", Name: "Tribal Warrior", CR: 0.125, HP: 11, AC: 12, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 25, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Pack Tactics"},
|
"tribal_warrior": {ID: "tribal_warrior", Name: "Tribal Warrior", CR: 0.125, HP: 11, AC: 12, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 25, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Pack Tactics"},
|
||||||
"acolyte": {ID: "acolyte", Name: "Acolyte", CR: 0.25, HP: 9, AC: 10, Attack: 1, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Spellcasting"},
|
"acolyte": {ID: "acolyte", Name: "Acolyte", CR: 0.25, HP: 9, AC: 10, Attack: 1, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Spellcasting"},
|
||||||
"axe_beak": {ID: "axe_beak", Name: "Axe Beak", CR: 0.25, HP: 19, AC: 11, Attack: 1, AttackBonus: 4, Speed: 18, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"axe_beak": {ID: "axe_beak", Name: "Axe Beak", CR: 0.25, HP: 19, AC: 11, Attack: 1, AttackBonus: 4, Speed: 18, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"blink_dog": {ID: "blink_dog", Name: "Blink Dog", CR: 0.25, HP: 22, AC: 13, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"},
|
"blink_dog": {ID: "blink_dog", Name: "Blink Dog", CR: 0.25, HP: 22, AC: 13, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"},
|
||||||
"boar": {ID: "boar", Name: "Boar", CR: 0.25, HP: 11, AC: 11, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Relentless (Recharges after a Short or Long Rest)"},
|
"boar": {ID: "boar", Name: "Boar", CR: 0.25, HP: 11, AC: 11, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Relentless (Recharges after a Short or Long Rest)"},
|
||||||
"constrictor_snake": {ID: "constrictor_snake", Name: "Constrictor Snake", CR: 0.25, HP: 13, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"constrictor_snake": {ID: "constrictor_snake", Name: "Constrictor Snake", CR: 0.25, HP: 13, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"draft_horse": {ID: "draft_horse", Name: "Draft Horse", CR: 0.25, HP: 19, AC: 10, Attack: 1, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"draft_horse": {ID: "draft_horse", Name: "Draft Horse", CR: 0.25, HP: 19, AC: 10, Attack: 1, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"dretch": {ID: "dretch", Name: "Dretch", CR: 0.25, HP: 18, AC: 11, Attack: 1, AttackBonus: 2, Speed: 8, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dretch makes two attacks: one with its bite and one with its claws."},
|
"dretch": {ID: "dretch", Name: "Dretch", CR: 0.25, HP: 18, AC: 11, Attack: 1, AttackBonus: 2, Speed: 8, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dretch makes two attacks: one with its bite and one with its claws."},
|
||||||
"drow": {ID: "drow", Name: "Drow", CR: 0.25, HP: 13, AC: 15, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 50, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Fey Ancestry, Innate Spellcasting, Sunlight Sensitivity"},
|
"drow": {ID: "drow", Name: "Drow", CR: 0.25, HP: 13, AC: 15, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 50, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Fey Ancestry, Innate Spellcasting, Sunlight Sensitivity"},
|
||||||
"elk": {ID: "elk", Name: "Elk", CR: 0.25, HP: 13, AC: 10, Attack: 1, AttackBonus: 0, Speed: 18, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge"},
|
"elk": {ID: "elk", Name: "Elk", CR: 0.25, HP: 13, AC: 10, Attack: 1, AttackBonus: 0, Speed: 18, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge"},
|
||||||
"flying_sword": {ID: "flying_sword", Name: "Flying Sword", CR: 0.25, HP: 17, AC: 17, Attack: 1, AttackBonus: 3, Speed: 18, BlockRate: 0.1, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Antimagic Susceptibility, False Appearance"},
|
"flying_sword": {ID: "flying_sword", Name: "Flying Sword", CR: 0.25, HP: 17, AC: 17, Attack: 1, AttackBonus: 3, Speed: 18, BlockRate: 0.1, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Antimagic Susceptibility, False Appearance"},
|
||||||
"giant_badger": {ID: "giant_badger", Name: "Giant Badger", CR: 0.25, HP: 13, AC: 10, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The badger makes two attacks: one with its bite and one with its claws. Traits: Keen Smell"},
|
"giant_badger": {ID: "giant_badger", Name: "Giant Badger", CR: 0.25, HP: 13, AC: 10, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The badger makes two attacks: one with its bite and one with its claws. Traits: Keen Smell"},
|
||||||
"giant_bat": {ID: "giant_bat", Name: "Giant Bat", CR: 0.25, HP: 22, AC: 13, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, Keen Hearing"},
|
"giant_bat": {ID: "giant_bat", Name: "Giant Bat", CR: 0.25, HP: 22, AC: 13, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, Keen Hearing"},
|
||||||
"giant_centipede": {ID: "giant_centipede", Name: "Giant Centipede", CR: 0.25, HP: 4, AC: 13, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"giant_centipede": {ID: "giant_centipede", Name: "Giant Centipede", CR: 0.25, HP: 4, AC: 13, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"giant_frog": {ID: "giant_frog", Name: "Giant Frog", CR: 0.25, HP: 18, AC: 11, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Amphibious, Standing Leap"},
|
"giant_frog": {ID: "giant_frog", Name: "Giant Frog", CR: 0.25, HP: 18, AC: 11, Attack: 1, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Amphibious, Standing Leap"},
|
||||||
"giant_lizard": {ID: "giant_lizard", Name: "Giant Lizard", CR: 0.25, HP: 19, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Variant: Hold Breath, Variant: Spider Climb"},
|
"giant_lizard": {ID: "giant_lizard", Name: "Giant Lizard", CR: 0.25, HP: 19, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Variant: Hold Breath, Variant: Spider Climb"},
|
||||||
"giant_owl": {ID: "giant_owl", Name: "Giant Owl", CR: 0.25, HP: 19, AC: 12, Attack: 1, AttackBonus: 3, Speed: 6, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Flyby, Keen Hearing and Sight"},
|
"giant_owl": {ID: "giant_owl", Name: "Giant Owl", CR: 0.25, HP: 19, AC: 12, Attack: 1, AttackBonus: 3, Speed: 6, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Flyby, Keen Hearing and Sight"},
|
||||||
"giant_poisonous_snake": {ID: "giant_poisonous_snake", Name: "Giant Poisonous Snake", CR: 0.25, HP: 11, AC: 14, Attack: 1, AttackBonus: 6, Speed: 12, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"giant_poisonous_snake": {ID: "giant_poisonous_snake", Name: "Giant Poisonous Snake", CR: 0.25, HP: 11, AC: 14, Attack: 1, AttackBonus: 6, Speed: 12, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"giant_wolf_spider": {ID: "giant_wolf_spider", Name: "Giant Wolf Spider", CR: 0.25, HP: 11, AC: 13, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Spider Climb, Web Sense, Web Walker"},
|
"giant_wolf_spider": {ID: "giant_wolf_spider", Name: "Giant Wolf Spider", CR: 0.25, HP: 11, AC: 13, Attack: 1, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Spider Climb, Web Sense, Web Walker"},
|
||||||
"goblin": {ID: "goblin", Name: "Goblin", CR: 0.25, HP: 7, AC: 15, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 50, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Nimble Escape"},
|
"goblin": {ID: "goblin", Name: "Goblin", CR: 0.25, HP: 7, AC: 15, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 50, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Nimble Escape"},
|
||||||
"grimlock": {ID: "grimlock", Name: "Grimlock", CR: 0.25, HP: 11, AC: 11, Attack: 1, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Blind Senses, Keen Hearing and Smell, Stone Camouflage"},
|
"grimlock": {ID: "grimlock", Name: "Grimlock", CR: 0.25, HP: 11, AC: 11, Attack: 1, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Blind Senses, Keen Hearing and Smell, Stone Camouflage"},
|
||||||
"panther": {ID: "panther", Name: "Panther", CR: 0.25, HP: 13, AC: 12, Attack: 1, AttackBonus: 4, Speed: 18, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Keen Smell, Pounce"},
|
"panther": {ID: "panther", Name: "Panther", CR: 0.25, HP: 13, AC: 12, Attack: 1, AttackBonus: 4, Speed: 18, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Keen Smell, Pounce"},
|
||||||
"pseudodragon": {ID: "pseudodragon", Name: "Pseudodragon", CR: 0.25, HP: 7, AC: 13, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0.05, XPValue: 50, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Traits: Keen Senses, Magic Resistance, Limited Telepathy, Variant: Familiar"},
|
"pseudodragon": {ID: "pseudodragon", Name: "Pseudodragon", CR: 0.25, HP: 7, AC: 13, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0.05, XPValue: 50, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Traits: Keen Senses, Magic Resistance, Limited Telepathy, Variant: Familiar"},
|
||||||
"riding_horse": {ID: "riding_horse", Name: "Riding Horse", CR: 0.25, HP: 13, AC: 10, Attack: 1, AttackBonus: 5, Speed: 18, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"riding_horse": {ID: "riding_horse", Name: "Riding Horse", CR: 0.25, HP: 13, AC: 10, Attack: 1, AttackBonus: 5, Speed: 18, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"skeleton": {ID: "skeleton", Name: "Skeleton", CR: 0.25, HP: 13, AC: 13, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"skeleton": {ID: "skeleton", Name: "Skeleton", CR: 0.25, HP: 13, AC: 13, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"sprite": {ID: "sprite", Name: "Sprite", CR: 0.25, HP: 2, AC: 15, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0.1, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"sprite": {ID: "sprite", Name: "Sprite", CR: 0.25, HP: 2, AC: 15, Attack: 1, AttackBonus: 0, Speed: 6, BlockRate: 0.1, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"steam_mephit": {ID: "steam_mephit", Name: "Steam Mephit", CR: 0.25, HP: 21, AC: 10, Attack: 1, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, Innate Spellcasting (1/Day)"},
|
"steam_mephit": {ID: "steam_mephit", Name: "Steam Mephit", CR: 0.25, HP: 21, AC: 10, Attack: 1, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, Innate Spellcasting (1/Day)"},
|
||||||
"swarm_of_bats": {ID: "swarm_of_bats", Name: "Swarm of Bats", CR: 0.25, HP: 22, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, Keen Hearing, Swarm"},
|
"swarm_of_bats": {ID: "swarm_of_bats", Name: "Swarm of Bats", CR: 0.25, HP: 22, AC: 12, Attack: 1, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, Keen Hearing, Swarm"},
|
||||||
"swarm_of_rats": {ID: "swarm_of_rats", Name: "Swarm of Rats", CR: 0.25, HP: 24, AC: 10, Attack: 1, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Smell, Swarm"},
|
"swarm_of_rats": {ID: "swarm_of_rats", Name: "Swarm of Rats", CR: 0.25, HP: 24, AC: 10, Attack: 1, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Smell, Swarm"},
|
||||||
"swarm_of_ravens": {ID: "swarm_of_ravens", Name: "Swarm of Ravens", CR: 0.25, HP: 24, AC: 12, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
|
"swarm_of_ravens": {ID: "swarm_of_ravens", Name: "Swarm of Ravens", CR: 0.25, HP: 24, AC: 12, Attack: 1, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
|
||||||
"violet_fungus": {ID: "violet_fungus", Name: "Violet Fungus", CR: 0.25, HP: 18, AC: 10, Attack: 1, AttackBonus: 2, Speed: 6, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The fungus makes 1d4 Rotting Touch attacks. Traits: False Appearance"},
|
"violet_fungus": {ID: "violet_fungus", Name: "Violet Fungus", CR: 0.25, HP: 18, AC: 10, Attack: 1, AttackBonus: 2, Speed: 6, BlockRate: 0, XPValue: 50, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The fungus makes 1d4 Rotting Touch attacks. Traits: False Appearance"},
|
||||||
"wolf": {ID: "wolf", Name: "Wolf", CR: 0.25, HP: 11, AC: 13, Attack: 1, AttackBonus: 4, Speed: 16, BlockRate: 0.05, XPValue: 50, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics"},
|
"wolf": {ID: "wolf", Name: "Wolf", CR: 0.25, HP: 11, AC: 13, Attack: 1, AttackBonus: 4, Speed: 16, BlockRate: 0.05, XPValue: 50, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics"},
|
||||||
"zombie": {ID: "zombie", Name: "Zombie", CR: 0.25, HP: 22, AC: 10, Attack: 1, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Undead Fortitude", Phase: "decisive", ProcChance: 0.5, Effect: "survive_at_1"}, Notes: "SRD-tuned baseline — Undead Fortitude (survive_at_1) wired from traits. Traits: Undead Fortitude"},
|
"zombie": {ID: "zombie", Name: "Zombie", CR: 0.25, HP: 22, AC: 10, Attack: 1, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 50, Ability: &MonsterAbility{Name: "Undead Fortitude", Phase: "decisive", ProcChance: 0.5, Effect: "survive_at_1"}, Notes: "SRD-tuned baseline — Undead Fortitude (survive_at_1) wired from traits. Traits: Undead Fortitude"},
|
||||||
"ape": {ID: "ape", Name: "Ape", CR: 0.5, HP: 19, AC: 12, Attack: 2, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The ape makes two fist attacks."},
|
"ape": {ID: "ape", Name: "Ape", CR: 0.5, HP: 19, AC: 12, Attack: 2, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The ape makes two fist attacks."},
|
||||||
"black_bear": {ID: "black_bear", Name: "Black Bear", CR: 0.5, HP: 19, AC: 11, Attack: 2, AttackBonus: 3, Speed: 16, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The bear makes two attacks: one with its bite and one with its claws. Traits: Keen Smell"},
|
"black_bear": {ID: "black_bear", Name: "Black Bear", CR: 0.5, HP: 19, AC: 11, Attack: 2, AttackBonus: 3, Speed: 16, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The bear makes two attacks: one with its bite and one with its claws. Traits: Keen Smell"},
|
||||||
"cockatrice": {ID: "cockatrice", Name: "Cockatrice", CR: 0.5, HP: 27, AC: 11, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"cockatrice": {ID: "cockatrice", Name: "Cockatrice", CR: 0.5, HP: 27, AC: 11, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"crocodile": {ID: "crocodile", Name: "Crocodile", CR: 0.5, HP: 19, AC: 12, Attack: 2, AttackBonus: 4, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Hold Breath"},
|
"crocodile": {ID: "crocodile", Name: "Crocodile", CR: 0.5, HP: 19, AC: 12, Attack: 2, AttackBonus: 4, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Hold Breath"},
|
||||||
"darkmantle": {ID: "darkmantle", Name: "Darkmantle", CR: 0.5, HP: 22, AC: 11, Attack: 2, AttackBonus: 5, Speed: 6, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, False Appearance"},
|
"darkmantle": {ID: "darkmantle", Name: "Darkmantle", CR: 0.5, HP: 22, AC: 11, Attack: 2, AttackBonus: 5, Speed: 6, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, False Appearance"},
|
||||||
"deep_gnome_svirfneblin": {ID: "deep_gnome_svirfneblin", Name: "Deep Gnome", CR: 0.5, HP: 16, AC: 15, Attack: 2, AttackBonus: 4, Speed: 8, BlockRate: 0.1, XPValue: 100, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Stone Camouflage, Gnome Cunning, Innate Spellcasting"},
|
"deep_gnome_svirfneblin": {ID: "deep_gnome_svirfneblin", Name: "Deep Gnome", CR: 0.5, HP: 16, AC: 15, Attack: 2, AttackBonus: 4, Speed: 8, BlockRate: 0.1, XPValue: 100, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Stone Camouflage, Gnome Cunning, Innate Spellcasting"},
|
||||||
"dust_mephit": {ID: "dust_mephit", Name: "Dust Mephit", CR: 0.5, HP: 17, AC: 12, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, Innate Spellcasting (1/Day)"},
|
"dust_mephit": {ID: "dust_mephit", Name: "Dust Mephit", CR: 0.5, HP: 17, AC: 12, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, Innate Spellcasting (1/Day)"},
|
||||||
"giant_goat": {ID: "giant_goat", Name: "Giant Goat", CR: 0.5, HP: 19, AC: 11, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Sure-Footed"},
|
"giant_goat": {ID: "giant_goat", Name: "Giant Goat", CR: 0.5, HP: 19, AC: 11, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Sure-Footed"},
|
||||||
"giant_sea_horse": {ID: "giant_sea_horse", Name: "Giant Sea Horse", CR: 0.5, HP: 16, AC: 13, Attack: 2, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Water Breathing"},
|
"giant_sea_horse": {ID: "giant_sea_horse", Name: "Giant Sea Horse", CR: 0.5, HP: 16, AC: 13, Attack: 2, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Water Breathing"},
|
||||||
"giant_wasp": {ID: "giant_wasp", Name: "Giant Wasp", CR: 0.5, HP: 13, AC: 12, Attack: 2, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"giant_wasp": {ID: "giant_wasp", Name: "Giant Wasp", CR: 0.5, HP: 13, AC: 12, Attack: 2, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"gnoll": {ID: "gnoll", Name: "Gnoll", CR: 0.5, HP: 22, AC: 15, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 100, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Rampage"},
|
"gnoll": {ID: "gnoll", Name: "Gnoll", CR: 0.5, HP: 22, AC: 15, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 100, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Rampage"},
|
||||||
"gray_ooze": {ID: "gray_ooze", Name: "Gray Ooze", CR: 0.5, HP: 22, AC: 10, Attack: 2, AttackBonus: 3, Speed: 6, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amorphous, Corrode Metal, False Appearance"},
|
"gray_ooze": {ID: "gray_ooze", Name: "Gray Ooze", CR: 0.5, HP: 22, AC: 10, Attack: 2, AttackBonus: 3, Speed: 6, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amorphous, Corrode Metal, False Appearance"},
|
||||||
"hobgoblin": {ID: "hobgoblin", Name: "Hobgoblin", CR: 0.5, HP: 11, AC: 18, Attack: 2, AttackBonus: 3, Speed: 12, BlockRate: 0.15, XPValue: 100, Ability: &MonsterAbility{Name: "Martial Advantage", Phase: "any", ProcChance: 0.35, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Martial Advantage (bonus_damage) wired from traits. Traits: Martial Advantage"},
|
"hobgoblin": {ID: "hobgoblin", Name: "Hobgoblin", CR: 0.5, HP: 11, AC: 18, Attack: 2, AttackBonus: 3, Speed: 12, BlockRate: 0.15, XPValue: 100, Ability: &MonsterAbility{Name: "Martial Advantage", Phase: "any", ProcChance: 0.35, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Martial Advantage (bonus_damage) wired from traits. Traits: Martial Advantage"},
|
||||||
"ice_mephit": {ID: "ice_mephit", Name: "Ice Mephit", CR: 0.5, HP: 21, AC: 11, Attack: 2, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, False Appearance, Innate Spellcasting (1/Day)"},
|
"ice_mephit": {ID: "ice_mephit", Name: "Ice Mephit", CR: 0.5, HP: 21, AC: 11, Attack: 2, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, False Appearance, Innate Spellcasting (1/Day)"},
|
||||||
"lizardfolk": {ID: "lizardfolk", Name: "Lizardfolk", CR: 0.5, HP: 22, AC: 15, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The lizardfolk makes two melee attacks, each one with a different weapon. Traits: Hold Breath"},
|
"lizardfolk": {ID: "lizardfolk", Name: "Lizardfolk", CR: 0.5, HP: 22, AC: 15, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The lizardfolk makes two melee attacks, each one with a different weapon. Traits: Hold Breath"},
|
||||||
"magma_mephit": {ID: "magma_mephit", Name: "Magma Mephit", CR: 0.5, HP: 22, AC: 11, Attack: 2, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, False Appearance, Innate Spellcasting (1/Day)"},
|
"magma_mephit": {ID: "magma_mephit", Name: "Magma Mephit", CR: 0.5, HP: 22, AC: 11, Attack: 2, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, False Appearance, Innate Spellcasting (1/Day)"},
|
||||||
"magmin": {ID: "magmin", Name: "Magmin", CR: 0.5, HP: 9, AC: 14, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 100, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, Ignited Illumination", FireAttacker: true},
|
"magmin": {ID: "magmin", Name: "Magmin", CR: 0.5, HP: 9, AC: 14, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 100, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Traits: Death Burst, Ignited Illumination", FireAttacker: true},
|
||||||
"orc": {ID: "orc", Name: "Orc", CR: 0.5, HP: 15, AC: 13, Attack: 2, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 100, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Aggressive"},
|
"orc": {ID: "orc", Name: "Orc", CR: 0.5, HP: 15, AC: 13, Attack: 2, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 100, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Aggressive"},
|
||||||
"reef_shark": {ID: "reef_shark", Name: "Reef Shark", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Pack Tactics, Water Breathing"},
|
"reef_shark": {ID: "reef_shark", Name: "Reef Shark", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Pack Tactics, Water Breathing"},
|
||||||
"rust_monster": {ID: "rust_monster", Name: "Rust Monster", CR: 0.5, HP: 27, AC: 14, Attack: 2, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Iron Scent, Rust Metal"},
|
"rust_monster": {ID: "rust_monster", Name: "Rust Monster", CR: 0.5, HP: 27, AC: 14, Attack: 2, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Iron Scent, Rust Metal"},
|
||||||
"sahuagin": {ID: "sahuagin", Name: "Sahuagin", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Multiattack: The sahuagin makes two melee attacks: one with its bite and one with its claws or spear. Traits: Blood Frenzy, Limited Amphibiousness, Shark Telepathy"},
|
"sahuagin": {ID: "sahuagin", Name: "Sahuagin", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Multiattack: The sahuagin makes two melee attacks: one with its bite and one with its claws or spear. Traits: Blood Frenzy, Limited Amphibiousness, Shark Telepathy"},
|
||||||
"satyr": {ID: "satyr", Name: "Satyr", CR: 0.5, HP: 31, AC: 14, Attack: 2, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 100, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Traits: Magic Resistance"},
|
"satyr": {ID: "satyr", Name: "Satyr", CR: 0.5, HP: 31, AC: 14, Attack: 2, AttackBonus: 3, Speed: 16, BlockRate: 0.05, XPValue: 100, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Traits: Magic Resistance"},
|
||||||
"scout": {ID: "scout", Name: "Scout", CR: 0.5, HP: 16, AC: 13, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The scout makes two melee attacks or two ranged attacks. Traits: Keen Hearing and Sight"},
|
"scout": {ID: "scout", Name: "Scout", CR: 0.5, HP: 16, AC: 13, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The scout makes two melee attacks or two ranged attacks. Traits: Keen Hearing and Sight"},
|
||||||
"shadow": {ID: "shadow", Name: "Shadow", CR: 0.5, HP: 16, AC: 12, Attack: 2, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Surprise Attack", Phase: "opening", ProcChance: 0.8, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Surprise Attack (bonus_damage) wired from traits. Traits: Amorphous, Shadow Stealth, Sunlight Weakness"},
|
"shadow": {ID: "shadow", Name: "Shadow", CR: 0.5, HP: 16, AC: 12, Attack: 2, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Surprise Attack", Phase: "opening", ProcChance: 0.8, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Surprise Attack (bonus_damage) wired from traits. Traits: Amorphous, Shadow Stealth, Sunlight Weakness"},
|
||||||
"swarm_of_beetles": {ID: "swarm_of_beetles", Name: "Swarm of Beetles", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
|
"swarm_of_beetles": {ID: "swarm_of_beetles", Name: "Swarm of Beetles", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
|
||||||
"swarm_of_centipedes": {ID: "swarm_of_centipedes", Name: "Swarm of Centipedes", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
|
"swarm_of_centipedes": {ID: "swarm_of_centipedes", Name: "Swarm of Centipedes", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
|
||||||
"swarm_of_insects": {ID: "swarm_of_insects", Name: "Swarm of Insects", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
|
"swarm_of_insects": {ID: "swarm_of_insects", Name: "Swarm of Insects", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
|
||||||
"swarm_of_spiders": {ID: "swarm_of_spiders", Name: "Swarm of Spiders", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm, Spider Climb, Web Sense, Web Walker"},
|
"swarm_of_spiders": {ID: "swarm_of_spiders", Name: "Swarm of Spiders", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm, Spider Climb, Web Sense, Web Walker"},
|
||||||
"swarm_of_wasps": {ID: "swarm_of_wasps", Name: "Swarm of Wasps", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 6, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
|
"swarm_of_wasps": {ID: "swarm_of_wasps", Name: "Swarm of Wasps", CR: 0.5, HP: 22, AC: 12, Attack: 2, AttackBonus: 3, Speed: 6, BlockRate: 0, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
|
||||||
"thug": {ID: "thug", Name: "Thug", CR: 0.5, HP: 32, AC: 11, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Multiattack: The thug makes two melee attacks. Traits: Pack Tactics"},
|
"thug": {ID: "thug", Name: "Thug", CR: 0.5, HP: 32, AC: 11, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Multiattack: The thug makes two melee attacks. Traits: Pack Tactics"},
|
||||||
"warhorse": {ID: "warhorse", Name: "Warhorse", CR: 0.5, HP: 19, AC: 11, Attack: 2, AttackBonus: 6, Speed: 18, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"},
|
"warhorse": {ID: "warhorse", Name: "Warhorse", CR: 0.5, HP: 19, AC: 11, Attack: 2, AttackBonus: 6, Speed: 18, BlockRate: 0, XPValue: 100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"},
|
||||||
"warhorse_skeleton": {ID: "warhorse_skeleton", Name: "Warhorse Skeleton", CR: 0.5, HP: 22, AC: 13, Attack: 2, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"warhorse_skeleton": {ID: "warhorse_skeleton", Name: "Warhorse Skeleton", CR: 0.5, HP: 22, AC: 13, Attack: 2, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"worg": {ID: "worg", Name: "Worg", CR: 0.5, HP: 26, AC: 13, Attack: 2, AttackBonus: 5, Speed: 18, BlockRate: 0.05, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"},
|
"worg": {ID: "worg", Name: "Worg", CR: 0.5, HP: 26, AC: 13, Attack: 2, AttackBonus: 5, Speed: 18, BlockRate: 0.05, XPValue: 100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Keen Hearing and Smell"},
|
||||||
"animated_armor": {ID: "animated_armor", Name: "Animated Armor", CR: 1, HP: 33, AC: 18, Attack: 2, AttackBonus: 4, Speed: 10, BlockRate: 0.15, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The armor makes two melee attacks. Traits: Antimagic Susceptibility, False Appearance"},
|
"animated_armor": {ID: "animated_armor", Name: "Animated Armor", CR: 1, HP: 33, AC: 18, Attack: 2, AttackBonus: 4, Speed: 10, BlockRate: 0.15, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The armor makes two melee attacks. Traits: Antimagic Susceptibility, False Appearance"},
|
||||||
"brass_dragon_wyrmling": {ID: "brass_dragon_wyrmling", Name: "Brass Dragon Wyrmling", CR: 1, HP: 16, AC: 16, Attack: 2, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat).", FireAttacker: true},
|
"brass_dragon_wyrmling": {ID: "brass_dragon_wyrmling", Name: "Brass Dragon Wyrmling", CR: 1, HP: 16, AC: 16, Attack: 2, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat).", FireAttacker: true},
|
||||||
"brown_bear": {ID: "brown_bear", Name: "Brown Bear", CR: 1, HP: 34, AC: 11, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The bear makes two attacks: one with its bite and one with its claws. Traits: Keen Smell"},
|
"brown_bear": {ID: "brown_bear", Name: "Brown Bear", CR: 1, HP: 34, AC: 11, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The bear makes two attacks: one with its bite and one with its claws. Traits: Keen Smell"},
|
||||||
"bugbear": {ID: "bugbear", Name: "Bugbear", CR: 1, HP: 27, AC: 16, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 200, Ability: &MonsterAbility{Name: "Surprise Attack", Phase: "opening", ProcChance: 0.8, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Surprise Attack (bonus_damage) wired from traits. Traits: Brute, Surprise Attack"},
|
"bugbear": {ID: "bugbear", Name: "Bugbear", CR: 1, HP: 27, AC: 16, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 200, Ability: &MonsterAbility{Name: "Surprise Attack", Phase: "opening", ProcChance: 0.8, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Surprise Attack (bonus_damage) wired from traits. Traits: Brute, Surprise Attack"},
|
||||||
"copper_dragon_wyrmling": {ID: "copper_dragon_wyrmling", Name: "Copper Dragon Wyrmling", CR: 1, HP: 22, AC: 16, Attack: 2, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"copper_dragon_wyrmling": {ID: "copper_dragon_wyrmling", Name: "Copper Dragon Wyrmling", CR: 1, HP: 22, AC: 16, Attack: 2, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"death_dog": {ID: "death_dog", Name: "Death Dog", CR: 1, HP: 39, AC: 12, Attack: 2, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dog makes two bite attacks. Traits: Two-Headed"},
|
"death_dog": {ID: "death_dog", Name: "Death Dog", CR: 1, HP: 39, AC: 12, Attack: 2, AttackBonus: 4, Speed: 16, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dog makes two bite attacks. Traits: Two-Headed"},
|
||||||
"dire_wolf": {ID: "dire_wolf", Name: "Dire Wolf", CR: 1, HP: 37, AC: 14, Attack: 2, AttackBonus: 5, Speed: 18, BlockRate: 0.05, XPValue: 200, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics"},
|
"dire_wolf": {ID: "dire_wolf", Name: "Dire Wolf", CR: 1, HP: 37, AC: 14, Attack: 2, AttackBonus: 5, Speed: 18, BlockRate: 0.05, XPValue: 200, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics"},
|
||||||
"dryad": {ID: "dryad", Name: "Dryad", CR: 1, HP: 22, AC: 11, Attack: 2, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Innate Spellcasting, Magic Resistance, Speak with Beasts and Plants, Tree Stride"},
|
"dryad": {ID: "dryad", Name: "Dryad", CR: 1, HP: 22, AC: 11, Attack: 2, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Innate Spellcasting, Magic Resistance, Speak with Beasts and Plants, Tree Stride"},
|
||||||
"duergar": {ID: "duergar", Name: "Duergar", CR: 1, HP: 26, AC: 16, Attack: 2, AttackBonus: 4, Speed: 10, BlockRate: 0.1, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Duergar Resilience, Sunlight Sensitivity"},
|
"duergar": {ID: "duergar", Name: "Duergar", CR: 1, HP: 26, AC: 16, Attack: 2, AttackBonus: 4, Speed: 10, BlockRate: 0.1, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Duergar Resilience, Sunlight Sensitivity"},
|
||||||
"ghoul": {ID: "ghoul", Name: "Ghoul", CR: 1, HP: 22, AC: 12, Attack: 2, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"ghoul": {ID: "ghoul", Name: "Ghoul", CR: 1, HP: 22, AC: 12, Attack: 2, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"giant_eagle": {ID: "giant_eagle", Name: "Giant Eagle", CR: 1, HP: 26, AC: 13, Attack: 2, AttackBonus: 5, Speed: 6, BlockRate: 0.05, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The eagle makes two attacks: one with its beak and one with its talons. Traits: Keen Sight"},
|
"giant_eagle": {ID: "giant_eagle", Name: "Giant Eagle", CR: 1, HP: 26, AC: 13, Attack: 2, AttackBonus: 5, Speed: 6, BlockRate: 0.05, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The eagle makes two attacks: one with its beak and one with its talons. Traits: Keen Sight"},
|
||||||
"giant_hyena": {ID: "giant_hyena", Name: "Giant Hyena", CR: 1, HP: 45, AC: 12, Attack: 2, AttackBonus: 5, Speed: 18, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Rampage"},
|
"giant_hyena": {ID: "giant_hyena", Name: "Giant Hyena", CR: 1, HP: 45, AC: 12, Attack: 2, AttackBonus: 5, Speed: 18, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Rampage"},
|
||||||
"giant_octopus": {ID: "giant_octopus", Name: "Giant Octopus", CR: 1, HP: 52, AC: 11, Attack: 2, AttackBonus: 5, Speed: 6, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Hold Breath, Underwater Camouflage, Water Breathing"},
|
"giant_octopus": {ID: "giant_octopus", Name: "Giant Octopus", CR: 1, HP: 52, AC: 11, Attack: 2, AttackBonus: 5, Speed: 6, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Hold Breath, Underwater Camouflage, Water Breathing"},
|
||||||
"giant_spider": {ID: "giant_spider", Name: "Giant Spider", CR: 1, HP: 26, AC: 14, Attack: 2, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Spider Climb, Web Sense, Web Walker"},
|
"giant_spider": {ID: "giant_spider", Name: "Giant Spider", CR: 1, HP: 26, AC: 14, Attack: 2, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Spider Climb, Web Sense, Web Walker"},
|
||||||
"giant_toad": {ID: "giant_toad", Name: "Giant Toad", CR: 1, HP: 39, AC: 11, Attack: 2, AttackBonus: 4, Speed: 8, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Amphibious, Standing Leap"},
|
"giant_toad": {ID: "giant_toad", Name: "Giant Toad", CR: 1, HP: 39, AC: 11, Attack: 2, AttackBonus: 4, Speed: 8, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Amphibious, Standing Leap"},
|
||||||
"giant_vulture": {ID: "giant_vulture", Name: "Giant Vulture", CR: 1, HP: 22, AC: 10, Attack: 2, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Multiattack: The vulture makes two attacks: one with its beak and one with its talons. Traits: Keen Sight and Smell, Pack Tactics"},
|
"giant_vulture": {ID: "giant_vulture", Name: "Giant Vulture", CR: 1, HP: 22, AC: 10, Attack: 2, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Multiattack: The vulture makes two attacks: one with its beak and one with its talons. Traits: Keen Sight and Smell, Pack Tactics"},
|
||||||
"harpy": {ID: "harpy", Name: "Harpy", CR: 1, HP: 38, AC: 11, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The harpy makes two attacks: one with its claws and one with its club."},
|
"harpy": {ID: "harpy", Name: "Harpy", CR: 1, HP: 38, AC: 11, Attack: 2, AttackBonus: 3, Speed: 8, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The harpy makes two attacks: one with its claws and one with its club."},
|
||||||
"hippogriff": {ID: "hippogriff", Name: "Hippogriff", CR: 1, HP: 19, AC: 11, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The hippogriff makes two attacks: one with its beak and one with its claws. Traits: Keen Sight"},
|
"hippogriff": {ID: "hippogriff", Name: "Hippogriff", CR: 1, HP: 19, AC: 11, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The hippogriff makes two attacks: one with its beak and one with its claws. Traits: Keen Sight"},
|
||||||
"imp": {ID: "imp", Name: "Imp", CR: 1, HP: 10, AC: 13, Attack: 2, AttackBonus: 5, Speed: 8, BlockRate: 0.05, XPValue: 200, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Traits: Shapechanger, Devil's Sight, Magic Resistance, Variant: Familiar"},
|
"imp": {ID: "imp", Name: "Imp", CR: 1, HP: 10, AC: 13, Attack: 2, AttackBonus: 5, Speed: 8, BlockRate: 0.05, XPValue: 200, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Traits: Shapechanger, Devil's Sight, Magic Resistance, Variant: Familiar"},
|
||||||
"lion": {ID: "lion", Name: "Lion", CR: 1, HP: 26, AC: 12, Attack: 2, AttackBonus: 5, Speed: 18, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Keen Smell, Pack Tactics, Pounce, Running Leap"},
|
"lion": {ID: "lion", Name: "Lion", CR: 1, HP: 26, AC: 12, Attack: 2, AttackBonus: 5, Speed: 18, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Keen Smell, Pack Tactics, Pounce, Running Leap"},
|
||||||
"quasit": {ID: "quasit", Name: "Quasit", CR: 1, HP: 7, AC: 13, Attack: 2, AttackBonus: 4, Speed: 16, BlockRate: 0.05, XPValue: 200, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Traits: Shapechanger, Magic Resistance, Variant: Familiar"},
|
"quasit": {ID: "quasit", Name: "Quasit", CR: 1, HP: 7, AC: 13, Attack: 2, AttackBonus: 4, Speed: 16, BlockRate: 0.05, XPValue: 200, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Traits: Shapechanger, Magic Resistance, Variant: Familiar"},
|
||||||
"specter": {ID: "specter", Name: "Specter", CR: 1, HP: 22, AC: 12, Attack: 2, AttackBonus: 4, Speed: 18, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Incorporeal Movement, Sunlight Sensitivity"},
|
"specter": {ID: "specter", Name: "Specter", CR: 1, HP: 22, AC: 12, Attack: 2, AttackBonus: 4, Speed: 18, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Incorporeal Movement, Sunlight Sensitivity"},
|
||||||
"spy": {ID: "spy", Name: "Spy", CR: 1, HP: 27, AC: 12, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Martial Advantage", Phase: "any", ProcChance: 0.35, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Martial Advantage (bonus_damage) wired from traits. Multiattack: The spy makes two melee attacks. Traits: Cunning Action, Sneak Attack (1/Turn)"},
|
"spy": {ID: "spy", Name: "Spy", CR: 1, HP: 27, AC: 12, Attack: 2, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Martial Advantage", Phase: "any", ProcChance: 0.35, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Martial Advantage (bonus_damage) wired from traits. Multiattack: The spy makes two melee attacks. Traits: Cunning Action, Sneak Attack (1/Turn)"},
|
||||||
"swarm_of_quippers": {ID: "swarm_of_quippers", Name: "Swarm of Quippers", CR: 1, HP: 28, AC: 13, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0.05, XPValue: 200, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Blood Frenzy, Swarm, Water Breathing"},
|
"swarm_of_quippers": {ID: "swarm_of_quippers", Name: "Swarm of Quippers", CR: 1, HP: 28, AC: 13, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0.05, XPValue: 200, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Blood Frenzy, Swarm, Water Breathing"},
|
||||||
"tiger": {ID: "tiger", Name: "Tiger", CR: 1, HP: 37, AC: 12, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Keen Smell, Pounce"},
|
"tiger": {ID: "tiger", Name: "Tiger", CR: 1, HP: 37, AC: 12, Attack: 2, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 200, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Keen Smell, Pounce"},
|
||||||
"ankheg": {ID: "ankheg", Name: "Ankheg", CR: 2, HP: 39, AC: 14, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"ankheg": {ID: "ankheg", Name: "Ankheg", CR: 2, HP: 39, AC: 14, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"awakened_tree": {ID: "awakened_tree", Name: "Awakened Tree", CR: 2, HP: 59, AC: 13, Attack: 4, AttackBonus: 6, Speed: 8, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: False Appearance"},
|
"awakened_tree": {ID: "awakened_tree", Name: "Awakened Tree", CR: 2, HP: 59, AC: 13, Attack: 4, AttackBonus: 6, Speed: 8, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: False Appearance"},
|
||||||
"azer": {ID: "azer", Name: "Azer", CR: 2, HP: 39, AC: 17, Attack: 4, AttackBonus: 5, Speed: 12, BlockRate: 0.1, XPValue: 450, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Traits: Heated Body, Heated Weapons, Illumination"},
|
"azer": {ID: "azer", Name: "Azer", CR: 2, HP: 39, AC: 17, Attack: 4, AttackBonus: 5, Speed: 12, BlockRate: 0.1, XPValue: 450, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Traits: Heated Body, Heated Weapons, Illumination"},
|
||||||
"bandit_captain": {ID: "bandit_captain", Name: "Bandit Captain", CR: 2, HP: 65, AC: 15, Attack: 4, AttackBonus: 5, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The captain makes three melee attacks: two with its scimitar and one with its dagger."},
|
"bandit_captain": {ID: "bandit_captain", Name: "Bandit Captain", CR: 2, HP: 65, AC: 15, Attack: 4, AttackBonus: 5, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The captain makes three melee attacks: two with its scimitar and one with its dagger."},
|
||||||
"berserker": {ID: "berserker", Name: "Berserker", CR: 2, HP: 67, AC: 13, Attack: 4, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Reckless"},
|
"berserker": {ID: "berserker", Name: "Berserker", CR: 2, HP: 67, AC: 13, Attack: 4, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Reckless"},
|
||||||
"black_dragon_wyrmling": {ID: "black_dragon_wyrmling", Name: "Black Dragon Wyrmling", CR: 2, HP: 33, AC: 17, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
|
"black_dragon_wyrmling": {ID: "black_dragon_wyrmling", Name: "Black Dragon Wyrmling", CR: 2, HP: 33, AC: 17, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
|
||||||
"bronze_dragon_wyrmling": {ID: "bronze_dragon_wyrmling", Name: "Bronze Dragon Wyrmling", CR: 2, HP: 32, AC: 17, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
|
"bronze_dragon_wyrmling": {ID: "bronze_dragon_wyrmling", Name: "Bronze Dragon Wyrmling", CR: 2, HP: 32, AC: 17, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
|
||||||
"centaur": {ID: "centaur", Name: "Centaur", CR: 2, HP: 45, AC: 12, Attack: 4, AttackBonus: 6, Speed: 18, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Multiattack: The centaur makes two attacks: one with its pike and one with its hooves or two with its longbow. Traits: Charge"},
|
"centaur": {ID: "centaur", Name: "Centaur", CR: 2, HP: 45, AC: 12, Attack: 4, AttackBonus: 6, Speed: 18, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Multiattack: The centaur makes two attacks: one with its pike and one with its hooves or two with its longbow. Traits: Charge"},
|
||||||
"cult_fanatic": {ID: "cult_fanatic", Name: "Cult Fanatic", CR: 2, HP: 22, AC: 13, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The fanatic makes two melee attacks. Traits: Dark Devotion, Spellcasting"},
|
"cult_fanatic": {ID: "cult_fanatic", Name: "Cult Fanatic", CR: 2, HP: 22, AC: 13, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The fanatic makes two melee attacks. Traits: Dark Devotion, Spellcasting"},
|
||||||
"druid": {ID: "druid", Name: "Druid", CR: 2, HP: 27, AC: 11, Attack: 4, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Spellcasting"},
|
"druid": {ID: "druid", Name: "Druid", CR: 2, HP: 27, AC: 11, Attack: 4, AttackBonus: 2, Speed: 12, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Spellcasting"},
|
||||||
"ettercap": {ID: "ettercap", Name: "Ettercap", CR: 2, HP: 44, AC: 13, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The ettercap makes two attacks: one with its bite and one with its claws. Traits: Spider Climb, Web Sense, Web Walker"},
|
"ettercap": {ID: "ettercap", Name: "Ettercap", CR: 2, HP: 44, AC: 13, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The ettercap makes two attacks: one with its bite and one with its claws. Traits: Spider Climb, Web Sense, Web Walker"},
|
||||||
"gargoyle": {ID: "gargoyle", Name: "Gargoyle", CR: 2, HP: 52, AC: 15, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The gargoyle makes two attacks: one with its bite and one with its claws. Traits: False Appearance"},
|
"gargoyle": {ID: "gargoyle", Name: "Gargoyle", CR: 2, HP: 52, AC: 15, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The gargoyle makes two attacks: one with its bite and one with its claws. Traits: False Appearance"},
|
||||||
"gelatinous_cube": {ID: "gelatinous_cube", Name: "Gelatinous Cube", CR: 2, HP: 84, AC: 10, Attack: 4, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Ooze Cube, Transparent"},
|
"gelatinous_cube": {ID: "gelatinous_cube", Name: "Gelatinous Cube", CR: 2, HP: 84, AC: 10, Attack: 4, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Ooze Cube, Transparent"},
|
||||||
"ghast": {ID: "ghast", Name: "Ghast", CR: 2, HP: 36, AC: 13, Attack: 4, AttackBonus: 3, Speed: 12, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Traits: Stench, Turn Defiance"},
|
"ghast": {ID: "ghast", Name: "Ghast", CR: 2, HP: 36, AC: 13, Attack: 4, AttackBonus: 3, Speed: 12, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Traits: Stench, Turn Defiance"},
|
||||||
"giant_boar": {ID: "giant_boar", Name: "Giant Boar", CR: 2, HP: 42, AC: 12, Attack: 4, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Relentless (Recharges after a Short or Long Rest)"},
|
"giant_boar": {ID: "giant_boar", Name: "Giant Boar", CR: 2, HP: 42, AC: 12, Attack: 4, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge, Relentless (Recharges after a Short or Long Rest)"},
|
||||||
"giant_constrictor_snake": {ID: "giant_constrictor_snake", Name: "Giant Constrictor Snake", CR: 2, HP: 60, AC: 12, Attack: 4, AttackBonus: 6, Speed: 12, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"giant_constrictor_snake": {ID: "giant_constrictor_snake", Name: "Giant Constrictor Snake", CR: 2, HP: 60, AC: 12, Attack: 4, AttackBonus: 6, Speed: 12, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"giant_elk": {ID: "giant_elk", Name: "Giant Elk", CR: 2, HP: 42, AC: 15, Attack: 4, AttackBonus: 6, Speed: 18, BlockRate: 0.1, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge"},
|
"giant_elk": {ID: "giant_elk", Name: "Giant Elk", CR: 2, HP: 42, AC: 15, Attack: 4, AttackBonus: 6, Speed: 18, BlockRate: 0.1, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge"},
|
||||||
"gibbering_mouther": {ID: "gibbering_mouther", Name: "Gibbering Mouther", CR: 2, HP: 67, AC: 10, Attack: 4, AttackBonus: 2, Speed: 6, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The gibbering mouther makes one bite attack and, if it can, uses its Blinding Spittle. Traits: Aberrant Ground, Gibbering"},
|
"gibbering_mouther": {ID: "gibbering_mouther", Name: "Gibbering Mouther", CR: 2, HP: 67, AC: 10, Attack: 4, AttackBonus: 2, Speed: 6, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The gibbering mouther makes one bite attack and, if it can, uses its Blinding Spittle. Traits: Aberrant Ground, Gibbering"},
|
||||||
"green_dragon_wyrmling": {ID: "green_dragon_wyrmling", Name: "Green Dragon Wyrmling", CR: 2, HP: 38, AC: 17, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
|
"green_dragon_wyrmling": {ID: "green_dragon_wyrmling", Name: "Green Dragon Wyrmling", CR: 2, HP: 38, AC: 17, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious"},
|
||||||
"grick": {ID: "grick", Name: "Grick", CR: 2, HP: 27, AC: 14, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The grick makes one attack with its tentacles. Traits: Stone Camouflage"},
|
"grick": {ID: "grick", Name: "Grick", CR: 2, HP: 27, AC: 14, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The grick makes one attack with its tentacles. Traits: Stone Camouflage"},
|
||||||
"griffon": {ID: "griffon", Name: "Griffon", CR: 2, HP: 59, AC: 12, Attack: 4, AttackBonus: 6, Speed: 12, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The griffon makes two attacks: one with its beak and one with its claws. Traits: Keen Sight"},
|
"griffon": {ID: "griffon", Name: "Griffon", CR: 2, HP: 59, AC: 12, Attack: 4, AttackBonus: 6, Speed: 12, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The griffon makes two attacks: one with its beak and one with its claws. Traits: Keen Sight"},
|
||||||
"hunter_shark": {ID: "hunter_shark", Name: "Hunter Shark", CR: 2, HP: 45, AC: 12, Attack: 4, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Blood Frenzy, Water Breathing"},
|
"hunter_shark": {ID: "hunter_shark", Name: "Hunter Shark", CR: 2, HP: 45, AC: 12, Attack: 4, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Blood Frenzy, Water Breathing"},
|
||||||
"merrow": {ID: "merrow", Name: "Merrow", CR: 2, HP: 45, AC: 13, Attack: 4, AttackBonus: 6, Speed: 6, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The merrow makes two attacks: one with its bite and one with its claws or harpoon. Traits: Amphibious"},
|
"merrow": {ID: "merrow", Name: "Merrow", CR: 2, HP: 45, AC: 13, Attack: 4, AttackBonus: 6, Speed: 6, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The merrow makes two attacks: one with its bite and one with its claws or harpoon. Traits: Amphibious"},
|
||||||
"mimic": {ID: "mimic", Name: "Mimic", CR: 2, HP: 58, AC: 12, Attack: 4, AttackBonus: 5, Speed: 6, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Shapechanger, Adhesive (Object Form Only), False Appearance (Object Form Only), Grappler"},
|
"mimic": {ID: "mimic", Name: "Mimic", CR: 2, HP: 58, AC: 12, Attack: 4, AttackBonus: 5, Speed: 6, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Shapechanger, Adhesive (Object Form Only), False Appearance (Object Form Only), Grappler"},
|
||||||
"minotaur_skeleton": {ID: "minotaur_skeleton", Name: "Minotaur Skeleton", CR: 2, HP: 67, AC: 12, Attack: 4, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge"},
|
"minotaur_skeleton": {ID: "minotaur_skeleton", Name: "Minotaur Skeleton", CR: 2, HP: 67, AC: 12, Attack: 4, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge"},
|
||||||
"ochre_jelly": {ID: "ochre_jelly", Name: "Ochre Jelly", CR: 2, HP: 45, AC: 10, Attack: 4, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amorphous, Spider Climb"},
|
"ochre_jelly": {ID: "ochre_jelly", Name: "Ochre Jelly", CR: 2, HP: 45, AC: 10, Attack: 4, AttackBonus: 4, Speed: 6, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amorphous, Spider Climb"},
|
||||||
"ogre": {ID: "ogre", Name: "Ogre", CR: 2, HP: 59, AC: 11, Attack: 4, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"ogre": {ID: "ogre", Name: "Ogre", CR: 2, HP: 59, AC: 11, Attack: 4, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"ogre_zombie": {ID: "ogre_zombie", Name: "Ogre Zombie", CR: 2, HP: 85, AC: 10, Attack: 4, AttackBonus: 6, Speed: 12, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Undead Fortitude", Phase: "decisive", ProcChance: 0.5, Effect: "survive_at_1"}, Notes: "SRD-tuned baseline — Undead Fortitude (survive_at_1) wired from traits. Traits: Undead Fortitude"},
|
"ogre_zombie": {ID: "ogre_zombie", Name: "Ogre Zombie", CR: 2, HP: 85, AC: 10, Attack: 4, AttackBonus: 6, Speed: 12, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Undead Fortitude", Phase: "decisive", ProcChance: 0.5, Effect: "survive_at_1"}, Notes: "SRD-tuned baseline — Undead Fortitude (survive_at_1) wired from traits. Traits: Undead Fortitude"},
|
||||||
"pegasus": {ID: "pegasus", Name: "Pegasus", CR: 2, HP: 59, AC: 12, Attack: 4, AttackBonus: 6, Speed: 18, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"pegasus": {ID: "pegasus", Name: "Pegasus", CR: 2, HP: 59, AC: 12, Attack: 4, AttackBonus: 6, Speed: 18, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"plesiosaurus": {ID: "plesiosaurus", Name: "Plesiosaurus", CR: 2, HP: 68, AC: 13, Attack: 4, AttackBonus: 6, Speed: 8, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Hold Breath"},
|
"plesiosaurus": {ID: "plesiosaurus", Name: "Plesiosaurus", CR: 2, HP: 68, AC: 13, Attack: 4, AttackBonus: 6, Speed: 8, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Hold Breath"},
|
||||||
"polar_bear": {ID: "polar_bear", Name: "Polar Bear", CR: 2, HP: 42, AC: 12, Attack: 4, AttackBonus: 7, Speed: 16, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The bear makes two attacks: one with its bite and one with its claws. Traits: Keen Smell"},
|
"polar_bear": {ID: "polar_bear", Name: "Polar Bear", CR: 2, HP: 42, AC: 12, Attack: 4, AttackBonus: 7, Speed: 16, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The bear makes two attacks: one with its bite and one with its claws. Traits: Keen Smell"},
|
||||||
"priest": {ID: "priest", Name: "Priest", CR: 2, HP: 27, AC: 13, Attack: 4, AttackBonus: 2, Speed: 10, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Divine Eminence, Spellcasting"},
|
"priest": {ID: "priest", Name: "Priest", CR: 2, HP: 27, AC: 13, Attack: 4, AttackBonus: 2, Speed: 10, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Divine Eminence, Spellcasting"},
|
||||||
"rhinoceros": {ID: "rhinoceros", Name: "Rhinoceros", CR: 2, HP: 45, AC: 11, Attack: 4, AttackBonus: 7, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge"},
|
"rhinoceros": {ID: "rhinoceros", Name: "Rhinoceros", CR: 2, HP: 45, AC: 11, Attack: 4, AttackBonus: 7, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Charge"},
|
||||||
"rug_of_smothering": {ID: "rug_of_smothering", Name: "Rug of Smothering", CR: 2, HP: 33, AC: 12, Attack: 4, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Antimagic Susceptibility, Damage Transfer, False Appearance"},
|
"rug_of_smothering": {ID: "rug_of_smothering", Name: "Rug of Smothering", CR: 2, HP: 33, AC: 12, Attack: 4, AttackBonus: 0, Speed: 6, BlockRate: 0, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Antimagic Susceptibility, Damage Transfer, False Appearance"},
|
||||||
"saber_toothed_tiger": {ID: "saber_toothed_tiger", Name: "Saber-Toothed Tiger", CR: 2, HP: 52, AC: 12, Attack: 4, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Keen Smell, Pounce"},
|
"saber_toothed_tiger": {ID: "saber_toothed_tiger", Name: "Saber-Toothed Tiger", CR: 2, HP: 52, AC: 12, Attack: 4, AttackBonus: 6, Speed: 16, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Keen Smell, Pounce"},
|
||||||
"sea_hag": {ID: "sea_hag", Name: "Sea Hag", CR: 2, HP: 52, AC: 14, Attack: 4, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Amphibious, Horrific Appearance, Hag Coven, Shared Spellcasting (Coven Only), Hag Eye (Coven Only)"},
|
"sea_hag": {ID: "sea_hag", Name: "Sea Hag", CR: 2, HP: 52, AC: 14, Attack: 4, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 450, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Amphibious, Horrific Appearance, Hag Coven, Shared Spellcasting (Coven Only), Hag Eye (Coven Only)"},
|
||||||
"silver_dragon_wyrmling": {ID: "silver_dragon_wyrmling", Name: "Silver Dragon Wyrmling", CR: 2, HP: 45, AC: 17, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"silver_dragon_wyrmling": {ID: "silver_dragon_wyrmling", Name: "Silver Dragon Wyrmling", CR: 2, HP: 45, AC: 17, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"swarm_of_poisonous_snakes": {ID: "swarm_of_poisonous_snakes", Name: "Swarm of Poisonous Snakes", CR: 2, HP: 36, AC: 14, Attack: 4, AttackBonus: 6, Speed: 12, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
|
"swarm_of_poisonous_snakes": {ID: "swarm_of_poisonous_snakes", Name: "Swarm of Poisonous Snakes", CR: 2, HP: 36, AC: 14, Attack: 4, AttackBonus: 6, Speed: 12, BlockRate: 0.05, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Swarm"},
|
||||||
"wererat": {ID: "wererat", Name: "Wererat", CR: 2, HP: 33, AC: 12, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Shapechanger, Keen Smell"},
|
"wererat": {ID: "wererat", Name: "Wererat", CR: 2, HP: 33, AC: 12, Attack: 4, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 450, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Shapechanger, Keen Smell"},
|
||||||
"white_dragon_wyrmling": {ID: "white_dragon_wyrmling", Name: "White Dragon Wyrmling", CR: 2, HP: 32, AC: 16, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"white_dragon_wyrmling": {ID: "white_dragon_wyrmling", Name: "White Dragon Wyrmling", CR: 2, HP: 32, AC: 16, Attack: 4, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 450, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"will_o_wisp": {ID: "will_o_wisp", Name: "Will-o'-Wisp", CR: 2, HP: 22, AC: 19, Attack: 4, AttackBonus: 4, Speed: 18, BlockRate: 0.15, XPValue: 450, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Consume Life, Ephemeral, Incorporeal Movement, Variable Illumination"},
|
"will_o_wisp": {ID: "will_o_wisp", Name: "Will-o'-Wisp", CR: 2, HP: 22, AC: 19, Attack: 4, AttackBonus: 4, Speed: 18, BlockRate: 0.15, XPValue: 450, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Consume Life, Ephemeral, Incorporeal Movement, Variable Illumination"},
|
||||||
"basilisk": {ID: "basilisk", Name: "Basilisk", CR: 3, HP: 52, AC: 12, Attack: 6, AttackBonus: 5, Speed: 8, BlockRate: 0, XPValue: 700, Ability: &MonsterAbility{Name: "Frightful Presence", Phase: "opening", ProcChance: 0.4, Effect: "stun"}, Notes: "SRD-tuned baseline — Frightful Presence (stun) wired from traits. Traits: Petrifying Gaze"},
|
"basilisk": {ID: "basilisk", Name: "Basilisk", CR: 3, HP: 52, AC: 12, Attack: 6, AttackBonus: 5, Speed: 8, BlockRate: 0, XPValue: 700, Ability: &MonsterAbility{Name: "Frightful Presence", Phase: "opening", ProcChance: 0.4, Effect: "stun"}, Notes: "SRD-tuned baseline — Frightful Presence (stun) wired from traits. Traits: Petrifying Gaze"},
|
||||||
"bearded_devil": {ID: "bearded_devil", Name: "Bearded Devil", CR: 3, HP: 52, AC: 13, Attack: 6, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes two attacks: one with its beard and one with its glaive. Traits: Devil's Sight, Magic Resistance, Steadfast"},
|
"bearded_devil": {ID: "bearded_devil", Name: "Bearded Devil", CR: 3, HP: 52, AC: 13, Attack: 6, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes two attacks: one with its beard and one with its glaive. Traits: Devil's Sight, Magic Resistance, Steadfast"},
|
||||||
"blue_dragon_wyrmling": {ID: "blue_dragon_wyrmling", Name: "Blue Dragon Wyrmling", CR: 3, HP: 52, AC: 17, Attack: 6, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
"blue_dragon_wyrmling": {ID: "blue_dragon_wyrmling", Name: "Blue Dragon Wyrmling", CR: 3, HP: 52, AC: 17, Attack: 6, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat)."},
|
||||||
"doppelganger": {ID: "doppelganger", Name: "Doppelganger", CR: 3, HP: 52, AC: 14, Attack: 6, AttackBonus: 6, Speed: 12, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Surprise Attack", Phase: "opening", ProcChance: 0.8, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Surprise Attack (bonus_damage) wired from traits. Multiattack: The doppelganger makes two melee attacks. Traits: Shapechanger, Ambusher, Surprise Attack"},
|
"doppelganger": {ID: "doppelganger", Name: "Doppelganger", CR: 3, HP: 52, AC: 14, Attack: 6, AttackBonus: 6, Speed: 12, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Surprise Attack", Phase: "opening", ProcChance: 0.8, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Surprise Attack (bonus_damage) wired from traits. Multiattack: The doppelganger makes two melee attacks. Traits: Shapechanger, Ambusher, Surprise Attack"},
|
||||||
"giant_scorpion": {ID: "giant_scorpion", Name: "Giant Scorpion", CR: 3, HP: 52, AC: 15, Attack: 6, AttackBonus: 4, Speed: 16, BlockRate: 0.1, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The scorpion makes three attacks: two with its claws and one with its sting."},
|
"giant_scorpion": {ID: "giant_scorpion", Name: "Giant Scorpion", CR: 3, HP: 52, AC: 15, Attack: 6, AttackBonus: 4, Speed: 16, BlockRate: 0.1, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The scorpion makes three attacks: two with its claws and one with its sting."},
|
||||||
"gold_dragon_wyrmling": {ID: "gold_dragon_wyrmling", Name: "Gold Dragon Wyrmling", CR: 3, HP: 60, AC: 17, Attack: 6, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious", FireAttacker: true},
|
"gold_dragon_wyrmling": {ID: "gold_dragon_wyrmling", Name: "Gold Dragon Wyrmling", CR: 3, HP: 60, AC: 17, Attack: 6, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amphibious", FireAttacker: true},
|
||||||
"green_hag": {ID: "green_hag", Name: "Green Hag", CR: 3, HP: 82, AC: 17, Attack: 6, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 700, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Amphibious, Innate Spellcasting, Mimicry, Hag Coven, Shared Spellcasting (Coven Only), Hag Eye (Coven Only)"},
|
"green_hag": {ID: "green_hag", Name: "Green Hag", CR: 3, HP: 82, AC: 17, Attack: 6, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 700, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Amphibious, Innate Spellcasting, Mimicry, Hag Coven, Shared Spellcasting (Coven Only), Hag Eye (Coven Only)"},
|
||||||
"hell_hound": {ID: "hell_hound", Name: "Hell Hound", CR: 3, HP: 45, AC: 15, Attack: 6, AttackBonus: 0, Speed: 18, BlockRate: 0.1, XPValue: 700, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics", FireAttacker: true},
|
"hell_hound": {ID: "hell_hound", Name: "Hell Hound", CR: 3, HP: 45, AC: 15, Attack: 6, AttackBonus: 0, Speed: 18, BlockRate: 0.1, XPValue: 700, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics", FireAttacker: true},
|
||||||
"killer_whale": {ID: "killer_whale", Name: "Killer Whale", CR: 3, HP: 90, AC: 12, Attack: 6, AttackBonus: 0, Speed: 18, BlockRate: 0, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, Hold Breath, Keen Hearing"},
|
"killer_whale": {ID: "killer_whale", Name: "Killer Whale", CR: 3, HP: 90, AC: 12, Attack: 6, AttackBonus: 0, Speed: 18, BlockRate: 0, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Echolocation, Hold Breath, Keen Hearing"},
|
||||||
"knight": {ID: "knight", Name: "Knight", CR: 3, HP: 52, AC: 18, Attack: 6, AttackBonus: 5, Speed: 12, BlockRate: 0.15, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The knight makes two melee attacks. Traits: Brave"},
|
"knight": {ID: "knight", Name: "Knight", CR: 3, HP: 52, AC: 18, Attack: 6, AttackBonus: 5, Speed: 12, BlockRate: 0.15, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The knight makes two melee attacks. Traits: Brave"},
|
||||||
"manticore": {ID: "manticore", Name: "Manticore", CR: 3, HP: 68, AC: 14, Attack: 6, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The manticore makes three attacks: one with its bite and two with its claws or three with its tail spikes. Traits: Tail Spike Regrowth"},
|
"manticore": {ID: "manticore", Name: "Manticore", CR: 3, HP: 68, AC: 14, Attack: 6, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The manticore makes three attacks: one with its bite and two with its claws or three with its tail spikes. Traits: Tail Spike Regrowth"},
|
||||||
"minotaur": {ID: "minotaur", Name: "Minotaur", CR: 3, HP: 76, AC: 14, Attack: 6, AttackBonus: 6, Speed: 16, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Charge, Labyrinthine Recall, Reckless"},
|
"minotaur": {ID: "minotaur", Name: "Minotaur", CR: 3, HP: 76, AC: 14, Attack: 6, AttackBonus: 6, Speed: 16, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Charge, Labyrinthine Recall, Reckless"},
|
||||||
"mummy": {ID: "mummy", Name: "Mummy", CR: 3, HP: 58, AC: 11, Attack: 6, AttackBonus: 5, Speed: 8, BlockRate: 0, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The mummy can use its Dreadful Glare and makes one attack with its rotting fist."},
|
"mummy": {ID: "mummy", Name: "Mummy", CR: 3, HP: 58, AC: 11, Attack: 6, AttackBonus: 5, Speed: 8, BlockRate: 0, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The mummy can use its Dreadful Glare and makes one attack with its rotting fist."},
|
||||||
"nightmare": {ID: "nightmare", Name: "Nightmare", CR: 3, HP: 68, AC: 13, Attack: 6, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Confer Fire Resistance, Illumination"},
|
"nightmare": {ID: "nightmare", Name: "Nightmare", CR: 3, HP: 68, AC: 13, Attack: 6, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Confer Fire Resistance, Illumination"},
|
||||||
"owlbear": {ID: "owlbear", Name: "Owlbear", CR: 3, HP: 59, AC: 13, Attack: 6, AttackBonus: 7, Speed: 16, BlockRate: 0.05, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The owlbear makes two attacks: one with its beak and one with its claws. Traits: Keen Sight and Smell"},
|
"owlbear": {ID: "owlbear", Name: "Owlbear", CR: 3, HP: 59, AC: 13, Attack: 6, AttackBonus: 7, Speed: 16, BlockRate: 0.05, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The owlbear makes two attacks: one with its beak and one with its claws. Traits: Keen Sight and Smell"},
|
||||||
"phase_spider": {ID: "phase_spider", Name: "Phase Spider", CR: 3, HP: 32, AC: 13, Attack: 6, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Ethereal Jaunt, Spider Climb, Web Walker"},
|
"phase_spider": {ID: "phase_spider", Name: "Phase Spider", CR: 3, HP: 32, AC: 13, Attack: 6, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Ethereal Jaunt, Spider Climb, Web Walker"},
|
||||||
"veteran": {ID: "veteran", Name: "Veteran", CR: 3, HP: 58, AC: 17, Attack: 6, AttackBonus: 5, Speed: 12, BlockRate: 0.1, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The veteran makes two longsword attacks."},
|
"veteran": {ID: "veteran", Name: "Veteran", CR: 3, HP: 58, AC: 17, Attack: 6, AttackBonus: 5, Speed: 12, BlockRate: 0.1, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The veteran makes two longsword attacks."},
|
||||||
"werewolf": {ID: "werewolf", Name: "Werewolf", CR: 3, HP: 58, AC: 11, Attack: 6, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 700, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Shapechanger, Keen Hearing and Smell"},
|
"werewolf": {ID: "werewolf", Name: "Werewolf", CR: 3, HP: 58, AC: 11, Attack: 6, AttackBonus: 4, Speed: 12, BlockRate: 0, XPValue: 700, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Shapechanger, Keen Hearing and Smell"},
|
||||||
"wight": {ID: "wight", Name: "Wight", CR: 3, HP: 45, AC: 14, Attack: 6, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The wight makes two longsword attacks or two longbow attacks. Traits: Sunlight Sensitivity"},
|
"wight": {ID: "wight", Name: "Wight", CR: 3, HP: 45, AC: 14, Attack: 6, AttackBonus: 4, Speed: 12, BlockRate: 0.05, XPValue: 700, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The wight makes two longsword attacks or two longbow attacks. Traits: Sunlight Sensitivity"},
|
||||||
"winter_wolf": {ID: "winter_wolf", Name: "Winter Wolf", CR: 3, HP: 75, AC: 13, Attack: 6, AttackBonus: 0, Speed: 18, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics, Snow Camouflage"},
|
"winter_wolf": {ID: "winter_wolf", Name: "Winter Wolf", CR: 3, HP: 75, AC: 13, Attack: 6, AttackBonus: 0, Speed: 18, BlockRate: 0.05, XPValue: 700, Ability: &MonsterAbility{Name: "Pack Tactics", Phase: "any", ProcChance: 0.3, Effect: "advantage"}, Notes: "SRD-tuned baseline — Pack Tactics (advantage) wired from traits. Traits: Keen Hearing and Smell, Pack Tactics, Snow Camouflage"},
|
||||||
"black_pudding": {ID: "black_pudding", Name: "Black Pudding", CR: 4, HP: 85, AC: 10, Attack: 7, AttackBonus: 5, Speed: 8, BlockRate: 0, XPValue: 1100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amorphous, Corrosive Form, Spider Climb"},
|
"black_pudding": {ID: "black_pudding", Name: "Black Pudding", CR: 4, HP: 85, AC: 10, Attack: 7, AttackBonus: 5, Speed: 8, BlockRate: 0, XPValue: 1100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Traits: Amorphous, Corrosive Form, Spider Climb"},
|
||||||
"chuul": {ID: "chuul", Name: "Chuul", CR: 4, HP: 93, AC: 16, Attack: 7, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 1100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The chuul makes two pincer attacks. Traits: Amphibious, Sense Magic"},
|
"chuul": {ID: "chuul", Name: "Chuul", CR: 4, HP: 93, AC: 16, Attack: 7, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 1100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The chuul makes two pincer attacks. Traits: Amphibious, Sense Magic"},
|
||||||
"couatl": {ID: "couatl", Name: "Couatl", CR: 4, HP: 97, AC: 19, Attack: 7, AttackBonus: 6, Speed: 12, BlockRate: 0.15, XPValue: 1100, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Innate Spellcasting, Magic Weapons, Shielded Mind"},
|
"couatl": {ID: "couatl", Name: "Couatl", CR: 4, HP: 97, AC: 19, Attack: 7, AttackBonus: 6, Speed: 12, BlockRate: 0.15, XPValue: 1100, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Innate Spellcasting, Magic Weapons, Shielded Mind"},
|
||||||
"elephant": {ID: "elephant", Name: "Elephant", CR: 4, HP: 76, AC: 12, Attack: 7, AttackBonus: 8, Speed: 16, BlockRate: 0, XPValue: 1100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"},
|
"elephant": {ID: "elephant", Name: "Elephant", CR: 4, HP: 76, AC: 12, Attack: 7, AttackBonus: 8, Speed: 16, BlockRate: 0, XPValue: 1100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"},
|
||||||
"ettin": {ID: "ettin", Name: "Ettin", CR: 4, HP: 85, AC: 12, Attack: 7, AttackBonus: 7, Speed: 16, BlockRate: 0, XPValue: 1100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The ettin makes two attacks: one with its battleaxe and one with its morningstar. Traits: Two Heads, Wakeful"},
|
"ettin": {ID: "ettin", Name: "Ettin", CR: 4, HP: 85, AC: 12, Attack: 7, AttackBonus: 7, Speed: 16, BlockRate: 0, XPValue: 1100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The ettin makes two attacks: one with its battleaxe and one with its morningstar. Traits: Two Heads, Wakeful"},
|
||||||
"ghost": {ID: "ghost", Name: "Ghost", CR: 4, HP: 45, AC: 11, Attack: 7, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 1100, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Ethereal Sight, Incorporeal Movement"},
|
"ghost": {ID: "ghost", Name: "Ghost", CR: 4, HP: 45, AC: 11, Attack: 7, AttackBonus: 5, Speed: 16, BlockRate: 0, XPValue: 1100, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Ethereal Sight, Incorporeal Movement"},
|
||||||
"lamia": {ID: "lamia", Name: "Lamia", CR: 4, HP: 97, AC: 13, Attack: 7, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 1100, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The lamia makes two attacks: one with its claws and one with its dagger or Intoxicating Touch. Traits: Innate Spellcasting"},
|
"lamia": {ID: "lamia", Name: "Lamia", CR: 4, HP: 97, AC: 13, Attack: 7, AttackBonus: 5, Speed: 12, BlockRate: 0.05, XPValue: 1100, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The lamia makes two attacks: one with its claws and one with its dagger or Intoxicating Touch. Traits: Innate Spellcasting"},
|
||||||
"red_dragon_wyrmling": {ID: "red_dragon_wyrmling", Name: "Red Dragon Wyrmling", CR: 4, HP: 75, AC: 17, Attack: 7, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 1100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat).", FireAttacker: true},
|
"red_dragon_wyrmling": {ID: "red_dragon_wyrmling", Name: "Red Dragon Wyrmling", CR: 4, HP: 75, AC: 17, Attack: 7, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 1100, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat).", FireAttacker: true},
|
||||||
"succubusincubus": {ID: "succubusincubus", Name: "Succubus/Incubus", CR: 4, HP: 66, AC: 15, Attack: 7, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 1100, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Telepathic Bond, Shapechanger"},
|
"succubusincubus": {ID: "succubusincubus", Name: "Succubus/Incubus", CR: 4, HP: 66, AC: 15, Attack: 7, AttackBonus: 0, Speed: 12, BlockRate: 0.1, XPValue: 1100, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Telepathic Bond, Shapechanger"},
|
||||||
"wereboar": {ID: "wereboar", Name: "Wereboar", CR: 4, HP: 78, AC: 10, Attack: 7, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 1100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Shapechanger, Charge (Boar or Hybrid Form Only), Relentless (Recharges after a Short or Long Rest)"},
|
"wereboar": {ID: "wereboar", Name: "Wereboar", CR: 4, HP: 78, AC: 10, Attack: 7, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 1100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Shapechanger, Charge (Boar or Hybrid Form Only), Relentless (Recharges after a Short or Long Rest)"},
|
||||||
"weretiger": {ID: "weretiger", Name: "Weretiger", CR: 4, HP: 120, AC: 12, Attack: 7, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 1100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Shapechanger, Keen Hearing and Smell, Pounce (Tiger or Hybrid Form Only)"},
|
"weretiger": {ID: "weretiger", Name: "Weretiger", CR: 4, HP: 120, AC: 12, Attack: 7, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 1100, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Shapechanger, Keen Hearing and Smell, Pounce (Tiger or Hybrid Form Only)"},
|
||||||
"air_elemental": {ID: "air_elemental", Name: "Air Elemental", CR: 5, HP: 90, AC: 15, Attack: 8, AttackBonus: 8, Speed: 18, BlockRate: 0.1, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The elemental makes two slam attacks. Traits: Air Form"},
|
"air_elemental": {ID: "air_elemental", Name: "Air Elemental", CR: 5, HP: 90, AC: 15, Attack: 8, AttackBonus: 8, Speed: 18, BlockRate: 0.1, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The elemental makes two slam attacks. Traits: Air Form"},
|
||||||
"barbed_devil": {ID: "barbed_devil", Name: "Barbed Devil", CR: 5, HP: 110, AC: 15, Attack: 8, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes three melee attacks: one with its tail and two with its claws. Traits: Barbed Hide, Devil's Sight, Magic Resistance"},
|
"barbed_devil": {ID: "barbed_devil", Name: "Barbed Devil", CR: 5, HP: 110, AC: 15, Attack: 8, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes three melee attacks: one with its tail and two with its claws. Traits: Barbed Hide, Devil's Sight, Magic Resistance"},
|
||||||
"bulette": {ID: "bulette", Name: "Bulette", CR: 5, HP: 94, AC: 17, Attack: 8, AttackBonus: 7, Speed: 16, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Standing Leap"},
|
"bulette": {ID: "bulette", Name: "Bulette", CR: 5, HP: 94, AC: 17, Attack: 8, AttackBonus: 7, Speed: 16, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Standing Leap"},
|
||||||
"earth_elemental": {ID: "earth_elemental", Name: "Earth Elemental", CR: 5, HP: 126, AC: 17, Attack: 8, AttackBonus: 8, Speed: 12, BlockRate: 0.1, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The elemental makes two slam attacks. Traits: Earth Glide, Siege Monster"},
|
"earth_elemental": {ID: "earth_elemental", Name: "Earth Elemental", CR: 5, HP: 126, AC: 17, Attack: 8, AttackBonus: 8, Speed: 12, BlockRate: 0.1, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The elemental makes two slam attacks. Traits: Earth Glide, Siege Monster"},
|
||||||
"fire_elemental": {ID: "fire_elemental", Name: "Fire Elemental", CR: 5, HP: 102, AC: 13, Attack: 8, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 1800, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Multiattack: The elemental makes two touch attacks. Traits: Fire Form, Illumination, Water Susceptibility", FireAttacker: true},
|
"fire_elemental": {ID: "fire_elemental", Name: "Fire Elemental", CR: 5, HP: 102, AC: 13, Attack: 8, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 1800, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Multiattack: The elemental makes two touch attacks. Traits: Fire Form, Illumination, Water Susceptibility", FireAttacker: true},
|
||||||
"flesh_golem": {ID: "flesh_golem", Name: "Flesh Golem", CR: 5, HP: 93, AC: 10, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0, XPValue: 1800, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The golem makes two slam attacks. Traits: Berserk, Aversion of Fire, Immutable Form, Lightning Absorption, Magic Resistance, Magic Weapons"},
|
"flesh_golem": {ID: "flesh_golem", Name: "Flesh Golem", CR: 5, HP: 93, AC: 10, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0, XPValue: 1800, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The golem makes two slam attacks. Traits: Berserk, Aversion of Fire, Immutable Form, Lightning Absorption, Magic Resistance, Magic Weapons"},
|
||||||
"giant_crocodile": {ID: "giant_crocodile", Name: "Giant Crocodile", CR: 5, HP: 85, AC: 14, Attack: 8, AttackBonus: 8, Speed: 12, BlockRate: 0.05, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The crocodile makes two attacks: one with its bite and one with its tail. Traits: Hold Breath"},
|
"giant_crocodile": {ID: "giant_crocodile", Name: "Giant Crocodile", CR: 5, HP: 85, AC: 14, Attack: 8, AttackBonus: 8, Speed: 12, BlockRate: 0.05, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The crocodile makes two attacks: one with its bite and one with its tail. Traits: Hold Breath"},
|
||||||
"giant_shark": {ID: "giant_shark", Name: "Giant Shark", CR: 5, HP: 126, AC: 13, Attack: 8, AttackBonus: 9, Speed: 18, BlockRate: 0.05, XPValue: 1800, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Blood Frenzy, Water Breathing"},
|
"giant_shark": {ID: "giant_shark", Name: "Giant Shark", CR: 5, HP: 126, AC: 13, Attack: 8, AttackBonus: 9, Speed: 18, BlockRate: 0.05, XPValue: 1800, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Traits: Blood Frenzy, Water Breathing"},
|
||||||
"gladiator": {ID: "gladiator", Name: "Gladiator", CR: 5, HP: 112, AC: 16, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Multiattack: The gladiator makes three melee attacks or two ranged attacks. Traits: Brave, Brute"},
|
"gladiator": {ID: "gladiator", Name: "Gladiator", CR: 5, HP: 112, AC: 16, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Frenzy", Phase: "any", ProcChance: 0.4, Effect: "enrage"}, Notes: "SRD-tuned baseline — Frenzy (enrage) wired from traits. Multiattack: The gladiator makes three melee attacks or two ranged attacks. Traits: Brave, Brute"},
|
||||||
"gorgon": {ID: "gorgon", Name: "Gorgon", CR: 5, HP: 114, AC: 19, Attack: 8, AttackBonus: 8, Speed: 16, BlockRate: 0.15, XPValue: 1800, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"},
|
"gorgon": {ID: "gorgon", Name: "Gorgon", CR: 5, HP: 114, AC: 19, Attack: 8, AttackBonus: 8, Speed: 16, BlockRate: 0.15, XPValue: 1800, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"},
|
||||||
"half_red_dragon_veteran": {ID: "half_red_dragon_veteran", Name: "Half-Red Dragon Veteran", CR: 5, HP: 65, AC: 18, Attack: 8, AttackBonus: 0, Speed: 12, BlockRate: 0.15, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The veteran makes two longsword attacks.", FireAttacker: true},
|
"half_red_dragon_veteran": {ID: "half_red_dragon_veteran", Name: "Half-Red Dragon Veteran", CR: 5, HP: 65, AC: 18, Attack: 8, AttackBonus: 0, Speed: 12, BlockRate: 0.15, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The veteran makes two longsword attacks.", FireAttacker: true},
|
||||||
"hill_giant": {ID: "hill_giant", Name: "Hill Giant", CR: 5, HP: 105, AC: 13, Attack: 8, AttackBonus: 8, Speed: 16, BlockRate: 0.05, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The giant makes two greatclub attacks."},
|
"hill_giant": {ID: "hill_giant", Name: "Hill Giant", CR: 5, HP: 105, AC: 13, Attack: 8, AttackBonus: 8, Speed: 16, BlockRate: 0.05, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The giant makes two greatclub attacks."},
|
||||||
"night_hag": {ID: "night_hag", Name: "Night Hag", CR: 5, HP: 112, AC: 17, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Innate Spellcasting, Magic Resistance, Night Hag Items, Hag Coven, Shared Spellcasting (Coven Only), Hag Eye (Coven Only)"},
|
"night_hag": {ID: "night_hag", Name: "Night Hag", CR: 5, HP: 112, AC: 17, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Innate Spellcasting, Magic Resistance, Night Hag Items, Hag Coven, Shared Spellcasting (Coven Only), Hag Eye (Coven Only)"},
|
||||||
"otyugh": {ID: "otyugh", Name: "Otyugh", CR: 5, HP: 114, AC: 14, Attack: 8, AttackBonus: 6, Speed: 12, BlockRate: 0.05, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The otyugh makes three attacks: one with its bite and two with its tentacles. Traits: Limited Telepathy"},
|
"otyugh": {ID: "otyugh", Name: "Otyugh", CR: 5, HP: 114, AC: 14, Attack: 8, AttackBonus: 6, Speed: 12, BlockRate: 0.05, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The otyugh makes three attacks: one with its bite and two with its tentacles. Traits: Limited Telepathy"},
|
||||||
"roper": {ID: "roper", Name: "Roper", CR: 5, HP: 93, AC: 20, Attack: 8, AttackBonus: 7, Speed: 6, BlockRate: 0.15, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The roper makes four attacks with its tendrils, uses Reel, and makes one attack with its bite. Traits: False Appearance, Grasping Tendrils, Spider Climb"},
|
"roper": {ID: "roper", Name: "Roper", CR: 5, HP: 93, AC: 20, Attack: 8, AttackBonus: 7, Speed: 6, BlockRate: 0.15, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The roper makes four attacks with its tendrils, uses Reel, and makes one attack with its bite. Traits: False Appearance, Grasping Tendrils, Spider Climb"},
|
||||||
"salamander": {ID: "salamander", Name: "Salamander", CR: 5, HP: 90, AC: 15, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Multiattack: The salamander makes two attacks: one with its spear and one with its tail. Traits: Heated Body, Heated Weapons"},
|
"salamander": {ID: "salamander", Name: "Salamander", CR: 5, HP: 90, AC: 15, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Multiattack: The salamander makes two attacks: one with its spear and one with its tail. Traits: Heated Body, Heated Weapons"},
|
||||||
"shambling_mound": {ID: "shambling_mound", Name: "Shambling Mound", CR: 5, HP: 136, AC: 15, Attack: 8, AttackBonus: 7, Speed: 8, BlockRate: 0.1, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The shambling mound makes two slam attacks. Traits: Lightning Absorption"},
|
"shambling_mound": {ID: "shambling_mound", Name: "Shambling Mound", CR: 5, HP: 136, AC: 15, Attack: 8, AttackBonus: 7, Speed: 8, BlockRate: 0.1, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The shambling mound makes two slam attacks. Traits: Lightning Absorption"},
|
||||||
"triceratops": {ID: "triceratops", Name: "Triceratops", CR: 5, HP: 95, AC: 13, Attack: 8, AttackBonus: 9, Speed: 18, BlockRate: 0.05, XPValue: 1800, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"},
|
"triceratops": {ID: "triceratops", Name: "Triceratops", CR: 5, HP: 95, AC: 13, Attack: 8, AttackBonus: 9, Speed: 18, BlockRate: 0.05, XPValue: 1800, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"},
|
||||||
"troll": {ID: "troll", Name: "Troll", CR: 5, HP: 84, AC: 15, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Multiattack: The troll makes three attacks: one with its bite and two with its claws. Traits: Keen Smell, Regeneration, Variant: Loathsome Limbs"},
|
"troll": {ID: "troll", Name: "Troll", CR: 5, HP: 84, AC: 15, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Multiattack: The troll makes three attacks: one with its bite and two with its claws. Traits: Keen Smell, Regeneration, Variant: Loathsome Limbs"},
|
||||||
"unicorn": {ID: "unicorn", Name: "Unicorn", CR: 5, HP: 67, AC: 12, Attack: 8, AttackBonus: 7, Speed: 18, BlockRate: 0, XPValue: 1800, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The unicorn makes two attacks: one with its hooves and one with its horn. Traits: Charge, Innate Spellcasting, Magic Resistance, Magic Weapons"},
|
"unicorn": {ID: "unicorn", Name: "Unicorn", CR: 5, HP: 67, AC: 12, Attack: 8, AttackBonus: 7, Speed: 18, BlockRate: 0, XPValue: 1800, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The unicorn makes two attacks: one with its hooves and one with its horn. Traits: Charge, Innate Spellcasting, Magic Resistance, Magic Weapons"},
|
||||||
"vampire_spawn": {ID: "vampire_spawn", Name: "Vampire Spawn", CR: 5, HP: 82, AC: 15, Attack: 8, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Multiattack: The vampire makes two attacks, only one of which can be a bite attack. Traits: Regeneration, Spider Climb, Vampire Weaknesses"},
|
"vampire_spawn": {ID: "vampire_spawn", Name: "Vampire Spawn", CR: 5, HP: 82, AC: 15, Attack: 8, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 1800, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Multiattack: The vampire makes two attacks, only one of which can be a bite attack. Traits: Regeneration, Spider Climb, Vampire Weaknesses"},
|
||||||
"water_elemental": {ID: "water_elemental", Name: "Water Elemental", CR: 5, HP: 114, AC: 14, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.05, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The elemental makes two slam attacks. Traits: Water Form, Freeze"},
|
"water_elemental": {ID: "water_elemental", Name: "Water Elemental", CR: 5, HP: 114, AC: 14, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0.05, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The elemental makes two slam attacks. Traits: Water Form, Freeze"},
|
||||||
"werebear": {ID: "werebear", Name: "Werebear", CR: 5, HP: 135, AC: 10, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0, XPValue: 1800, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Multiattack: In bear form, the werebear makes two claw attacks. Traits: Shapechanger, Keen Smell"},
|
"werebear": {ID: "werebear", Name: "Werebear", CR: 5, HP: 135, AC: 10, Attack: 8, AttackBonus: 7, Speed: 12, BlockRate: 0, XPValue: 1800, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Multiattack: In bear form, the werebear makes two claw attacks. Traits: Shapechanger, Keen Smell"},
|
||||||
"wraith": {ID: "wraith", Name: "Wraith", CR: 5, HP: 67, AC: 13, Attack: 8, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 1800, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Incorporeal Movement, Sunlight Sensitivity"},
|
"wraith": {ID: "wraith", Name: "Wraith", CR: 5, HP: 67, AC: 13, Attack: 8, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 1800, Ability: &MonsterAbility{Name: "Evasive", Phase: "any", ProcChance: 0.3, Effect: "evade"}, Notes: "SRD-tuned baseline — Evasive (evade) wired from traits. Traits: Incorporeal Movement, Sunlight Sensitivity"},
|
||||||
"xorn": {ID: "xorn", Name: "Xorn", CR: 5, HP: 73, AC: 19, Attack: 8, AttackBonus: 6, Speed: 8, BlockRate: 0.15, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The xorn makes three claw attacks and one bite attack. Traits: Earth Glide, Stone Camouflage, Treasure Sense"},
|
"xorn": {ID: "xorn", Name: "Xorn", CR: 5, HP: 73, AC: 19, Attack: 8, AttackBonus: 6, Speed: 8, BlockRate: 0.15, XPValue: 1800, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The xorn makes three claw attacks and one bite attack. Traits: Earth Glide, Stone Camouflage, Treasure Sense"},
|
||||||
"chimera": {ID: "chimera", Name: "Chimera", CR: 6, HP: 114, AC: 14, Attack: 10, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The chimera makes three attacks: one with its bite, one with its horns, and one with its claws.", FireAttacker: true},
|
"chimera": {ID: "chimera", Name: "Chimera", CR: 6, HP: 114, AC: 14, Attack: 10, AttackBonus: 0, Speed: 12, BlockRate: 0.05, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The chimera makes three attacks: one with its bite, one with its horns, and one with its claws.", FireAttacker: true},
|
||||||
"drider": {ID: "drider", Name: "Drider", CR: 6, HP: 123, AC: 19, Attack: 10, AttackBonus: 6, Speed: 12, BlockRate: 0.15, XPValue: 2300, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The drider makes three attacks, either with its longsword or its longbow. Traits: Fey Ancestry, Innate Spellcasting, Spider Climb, Sunlight Sensitivity, Web Walker"},
|
"drider": {ID: "drider", Name: "Drider", CR: 6, HP: 123, AC: 19, Attack: 10, AttackBonus: 6, Speed: 12, BlockRate: 0.15, XPValue: 2300, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The drider makes three attacks, either with its longsword or its longbow. Traits: Fey Ancestry, Innate Spellcasting, Spider Climb, Sunlight Sensitivity, Web Walker"},
|
||||||
"invisible_stalker": {ID: "invisible_stalker", Name: "Invisible Stalker", CR: 6, HP: 104, AC: 14, Attack: 10, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The stalker makes two slam attacks. Traits: Invisibility, Faultless Tracker"},
|
"invisible_stalker": {ID: "invisible_stalker", Name: "Invisible Stalker", CR: 6, HP: 104, AC: 14, Attack: 10, AttackBonus: 6, Speed: 18, BlockRate: 0.05, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The stalker makes two slam attacks. Traits: Invisibility, Faultless Tracker"},
|
||||||
"mage": {ID: "mage", Name: "Mage", CR: 6, HP: 40, AC: 12, Attack: 10, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 2300, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Spellcasting"},
|
"mage": {ID: "mage", Name: "Mage", CR: 6, HP: 40, AC: 12, Attack: 10, AttackBonus: 5, Speed: 12, BlockRate: 0, XPValue: 2300, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Spellcasting"},
|
||||||
"mammoth": {ID: "mammoth", Name: "Mammoth", CR: 6, HP: 126, AC: 13, Attack: 10, AttackBonus: 10, Speed: 16, BlockRate: 0.05, XPValue: 2300, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"},
|
"mammoth": {ID: "mammoth", Name: "Mammoth", CR: 6, HP: 126, AC: 13, Attack: 10, AttackBonus: 10, Speed: 16, BlockRate: 0.05, XPValue: 2300, Ability: &MonsterAbility{Name: "Charge", Phase: "opening", ProcChance: 0.4, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Charge (bonus_damage) wired from traits. Traits: Trampling Charge"},
|
||||||
"medusa": {ID: "medusa", Name: "Medusa", CR: 6, HP: 127, AC: 15, Attack: 10, AttackBonus: 5, Speed: 12, BlockRate: 0.1, XPValue: 2300, Ability: &MonsterAbility{Name: "Frightful Presence", Phase: "opening", ProcChance: 0.4, Effect: "stun"}, Notes: "SRD-tuned baseline — Frightful Presence (stun) wired from traits. Multiattack: The medusa makes either three melee attacks - one with its snake hair and two with its shortsword - or two ranged attacks with its longbow. Traits: Petrifying Gaze"},
|
"medusa": {ID: "medusa", Name: "Medusa", CR: 6, HP: 127, AC: 15, Attack: 10, AttackBonus: 5, Speed: 12, BlockRate: 0.1, XPValue: 2300, Ability: &MonsterAbility{Name: "Frightful Presence", Phase: "opening", ProcChance: 0.4, Effect: "stun"}, Notes: "SRD-tuned baseline — Frightful Presence (stun) wired from traits. Multiattack: The medusa makes either three melee attacks - one with its snake hair and two with its shortsword - or two ranged attacks with its longbow. Traits: Petrifying Gaze"},
|
||||||
"vrock": {ID: "vrock", Name: "Vrock", CR: 6, HP: 104, AC: 15, Attack: 10, AttackBonus: 6, Speed: 16, BlockRate: 0.1, XPValue: 2300, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The vrock makes two attacks: one with its beak and one with its talons. Traits: Magic Resistance"},
|
"vrock": {ID: "vrock", Name: "Vrock", CR: 6, HP: 104, AC: 15, Attack: 10, AttackBonus: 6, Speed: 16, BlockRate: 0.1, XPValue: 2300, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The vrock makes two attacks: one with its beak and one with its talons. Traits: Magic Resistance"},
|
||||||
"wyvern": {ID: "wyvern", Name: "Wyvern", CR: 6, HP: 110, AC: 13, Attack: 10, AttackBonus: 7, Speed: 8, BlockRate: 0.05, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The wyvern makes two attacks: one with its bite and one with its stinger."},
|
"wyvern": {ID: "wyvern", Name: "Wyvern", CR: 6, HP: 110, AC: 13, Attack: 10, AttackBonus: 7, Speed: 8, BlockRate: 0.05, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The wyvern makes two attacks: one with its bite and one with its stinger."},
|
||||||
"young_brass_dragon": {ID: "young_brass_dragon", Name: "Young Brass Dragon", CR: 6, HP: 110, AC: 17, Attack: 10, AttackBonus: 0, Speed: 16, BlockRate: 0.1, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws.", FireAttacker: true},
|
"young_brass_dragon": {ID: "young_brass_dragon", Name: "Young Brass Dragon", CR: 6, HP: 110, AC: 17, Attack: 10, AttackBonus: 0, Speed: 16, BlockRate: 0.1, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws.", FireAttacker: true},
|
||||||
"young_white_dragon": {ID: "young_white_dragon", Name: "Young White Dragon", CR: 6, HP: 133, AC: 17, Attack: 10, AttackBonus: 0, Speed: 16, BlockRate: 0.1, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Ice Walk"},
|
"young_white_dragon": {ID: "young_white_dragon", Name: "Young White Dragon", CR: 6, HP: 133, AC: 17, Attack: 10, AttackBonus: 0, Speed: 16, BlockRate: 0.1, XPValue: 2300, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Ice Walk"},
|
||||||
"giant_ape": {ID: "giant_ape", Name: "Giant Ape", CR: 7, HP: 157, AC: 12, Attack: 12, AttackBonus: 9, Speed: 16, BlockRate: 0, XPValue: 2900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The ape makes two fist attacks."},
|
"giant_ape": {ID: "giant_ape", Name: "Giant Ape", CR: 7, HP: 157, AC: 12, Attack: 12, AttackBonus: 9, Speed: 16, BlockRate: 0, XPValue: 2900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The ape makes two fist attacks."},
|
||||||
"oni": {ID: "oni", Name: "Oni", CR: 7, HP: 110, AC: 16, Attack: 12, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 2900, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The oni makes two attacks, either with its claws or its glaive. Traits: Innate Spellcasting, Magic Weapons, Regeneration"},
|
"oni": {ID: "oni", Name: "Oni", CR: 7, HP: 110, AC: 16, Attack: 12, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 2900, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The oni makes two attacks, either with its claws or its glaive. Traits: Innate Spellcasting, Magic Weapons, Regeneration"},
|
||||||
"shield_guardian": {ID: "shield_guardian", Name: "Shield Guardian", CR: 7, HP: 142, AC: 17, Attack: 12, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 2900, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Multiattack: The guardian makes two fist attacks. Traits: Bound, Regeneration, Spell Storing"},
|
"shield_guardian": {ID: "shield_guardian", Name: "Shield Guardian", CR: 7, HP: 142, AC: 17, Attack: 12, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 2900, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Multiattack: The guardian makes two fist attacks. Traits: Bound, Regeneration, Spell Storing"},
|
||||||
"stone_giant": {ID: "stone_giant", Name: "Stone Giant", CR: 7, HP: 126, AC: 17, Attack: 12, AttackBonus: 9, Speed: 16, BlockRate: 0.1, XPValue: 2900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The giant makes two greatclub attacks. Traits: Stone Camouflage"},
|
"stone_giant": {ID: "stone_giant", Name: "Stone Giant", CR: 7, HP: 126, AC: 17, Attack: 12, AttackBonus: 9, Speed: 16, BlockRate: 0.1, XPValue: 2900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The giant makes two greatclub attacks. Traits: Stone Camouflage"},
|
||||||
"young_black_dragon": {ID: "young_black_dragon", Name: "Young Black Dragon", CR: 7, HP: 127, AC: 18, Attack: 12, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 2900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Amphibious"},
|
"young_black_dragon": {ID: "young_black_dragon", Name: "Young Black Dragon", CR: 7, HP: 127, AC: 18, Attack: 12, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 2900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Amphibious"},
|
||||||
"young_copper_dragon": {ID: "young_copper_dragon", Name: "Young Copper Dragon", CR: 7, HP: 119, AC: 17, Attack: 12, AttackBonus: 0, Speed: 16, BlockRate: 0.1, XPValue: 2900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws."},
|
"young_copper_dragon": {ID: "young_copper_dragon", Name: "Young Copper Dragon", CR: 7, HP: 119, AC: 17, Attack: 12, AttackBonus: 0, Speed: 16, BlockRate: 0.1, XPValue: 2900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws."},
|
||||||
"assassin": {ID: "assassin", Name: "Assassin", CR: 8, HP: 78, AC: 15, Attack: 13, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 3900, Ability: &MonsterAbility{Name: "Surprise Attack", Phase: "opening", ProcChance: 0.8, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Surprise Attack (bonus_damage) wired from traits. Multiattack: The assassin makes two shortsword attacks. Traits: Assassinate, Evasion, Sneak Attack (1/Turn)"},
|
"assassin": {ID: "assassin", Name: "Assassin", CR: 8, HP: 78, AC: 15, Attack: 13, AttackBonus: 6, Speed: 12, BlockRate: 0.1, XPValue: 3900, Ability: &MonsterAbility{Name: "Surprise Attack", Phase: "opening", ProcChance: 0.8, Effect: "bonus_damage"}, Notes: "SRD-tuned baseline — Surprise Attack (bonus_damage) wired from traits. Multiattack: The assassin makes two shortsword attacks. Traits: Assassinate, Evasion, Sneak Attack (1/Turn)"},
|
||||||
"cloaker": {ID: "cloaker", Name: "Cloaker", CR: 8, HP: 78, AC: 14, Attack: 13, AttackBonus: 6, Speed: 6, BlockRate: 0.05, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The cloaker makes two attacks: one with its bite and one with its tail. Traits: Damage Transfer, False Appearance, Light Sensitivity"},
|
"cloaker": {ID: "cloaker", Name: "Cloaker", CR: 8, HP: 78, AC: 14, Attack: 13, AttackBonus: 6, Speed: 6, BlockRate: 0.05, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The cloaker makes two attacks: one with its bite and one with its tail. Traits: Damage Transfer, False Appearance, Light Sensitivity"},
|
||||||
"frost_giant": {ID: "frost_giant", Name: "Frost Giant", CR: 8, HP: 138, AC: 15, Attack: 13, AttackBonus: 9, Speed: 16, BlockRate: 0.1, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The giant makes two greataxe attacks."},
|
"frost_giant": {ID: "frost_giant", Name: "Frost Giant", CR: 8, HP: 138, AC: 15, Attack: 13, AttackBonus: 9, Speed: 16, BlockRate: 0.1, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The giant makes two greataxe attacks."},
|
||||||
"hezrou": {ID: "hezrou", Name: "Hezrou", CR: 8, HP: 136, AC: 16, Attack: 13, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 3900, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The hezrou makes three attacks: one with its bite and two with its claws. Traits: Magic Resistance, Stench"},
|
"hezrou": {ID: "hezrou", Name: "Hezrou", CR: 8, HP: 136, AC: 16, Attack: 13, AttackBonus: 7, Speed: 12, BlockRate: 0.1, XPValue: 3900, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The hezrou makes three attacks: one with its bite and two with its claws. Traits: Magic Resistance, Stench"},
|
||||||
"hydra": {ID: "hydra", Name: "Hydra", CR: 8, HP: 172, AC: 15, Attack: 13, AttackBonus: 8, Speed: 12, BlockRate: 0.1, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The hydra makes as many bite attacks as it has heads. Traits: Hold Breath, Multiple Heads, Reactive Heads, Wakeful"},
|
"hydra": {ID: "hydra", Name: "Hydra", CR: 8, HP: 172, AC: 15, Attack: 13, AttackBonus: 8, Speed: 12, BlockRate: 0.1, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The hydra makes as many bite attacks as it has heads. Traits: Hold Breath, Multiple Heads, Reactive Heads, Wakeful"},
|
||||||
"spirit_naga": {ID: "spirit_naga", Name: "Spirit Naga", CR: 8, HP: 75, AC: 15, Attack: 13, AttackBonus: 7, Speed: 16, BlockRate: 0.1, XPValue: 3900, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Rejuvenation, Spellcasting"},
|
"spirit_naga": {ID: "spirit_naga", Name: "Spirit Naga", CR: 8, HP: 75, AC: 15, Attack: 13, AttackBonus: 7, Speed: 16, BlockRate: 0.1, XPValue: 3900, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Rejuvenation, Spellcasting"},
|
||||||
"tyrannosaurus_rex": {ID: "tyrannosaurus_rex", Name: "Tyrannosaurus Rex", CR: 8, HP: 136, AC: 13, Attack: 13, AttackBonus: 10, Speed: 18, BlockRate: 0.05, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The tyrannosaurus makes two attacks: one with its bite and one with its tail."},
|
"tyrannosaurus_rex": {ID: "tyrannosaurus_rex", Name: "Tyrannosaurus Rex", CR: 8, HP: 136, AC: 13, Attack: 13, AttackBonus: 10, Speed: 18, BlockRate: 0.05, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The tyrannosaurus makes two attacks: one with its bite and one with its tail."},
|
||||||
"young_bronze_dragon": {ID: "young_bronze_dragon", Name: "Young Bronze Dragon", CR: 8, HP: 142, AC: 18, Attack: 13, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Amphibious"},
|
"young_bronze_dragon": {ID: "young_bronze_dragon", Name: "Young Bronze Dragon", CR: 8, HP: 142, AC: 18, Attack: 13, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Amphibious"},
|
||||||
"young_green_dragon": {ID: "young_green_dragon", Name: "Young Green Dragon", CR: 8, HP: 136, AC: 18, Attack: 13, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Amphibious"},
|
"young_green_dragon": {ID: "young_green_dragon", Name: "Young Green Dragon", CR: 8, HP: 136, AC: 18, Attack: 13, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 3900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Amphibious"},
|
||||||
"bone_devil": {ID: "bone_devil", Name: "Bone Devil", CR: 9, HP: 142, AC: 19, Attack: 14, AttackBonus: 8, Speed: 16, BlockRate: 0.15, XPValue: 5000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes three attacks: two with its claws and one with its sting. Traits: Devil's Sight, Magic Resistance"},
|
"bone_devil": {ID: "bone_devil", Name: "Bone Devil", CR: 9, HP: 142, AC: 19, Attack: 14, AttackBonus: 8, Speed: 16, BlockRate: 0.15, XPValue: 5000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes three attacks: two with its claws and one with its sting. Traits: Devil's Sight, Magic Resistance"},
|
||||||
"clay_golem": {ID: "clay_golem", Name: "Clay Golem", CR: 9, HP: 133, AC: 14, Attack: 14, AttackBonus: 8, Speed: 8, BlockRate: 0.05, XPValue: 5000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The golem makes two slam attacks. Traits: Acid Absorption, Berserk, Immutable Form, Magic Resistance, Magic Weapons"},
|
"clay_golem": {ID: "clay_golem", Name: "Clay Golem", CR: 9, HP: 133, AC: 14, Attack: 14, AttackBonus: 8, Speed: 8, BlockRate: 0.05, XPValue: 5000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The golem makes two slam attacks. Traits: Acid Absorption, Berserk, Immutable Form, Magic Resistance, Magic Weapons"},
|
||||||
"cloud_giant": {ID: "cloud_giant", Name: "Cloud Giant", CR: 9, HP: 200, AC: 14, Attack: 14, AttackBonus: 12, Speed: 16, BlockRate: 0.05, XPValue: 5000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The giant makes two morningstar attacks. Traits: Keen Smell, Innate Spellcasting"},
|
"cloud_giant": {ID: "cloud_giant", Name: "Cloud Giant", CR: 9, HP: 200, AC: 14, Attack: 14, AttackBonus: 12, Speed: 16, BlockRate: 0.05, XPValue: 5000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The giant makes two morningstar attacks. Traits: Keen Smell, Innate Spellcasting"},
|
||||||
"fire_giant": {ID: "fire_giant", Name: "Fire Giant", CR: 9, HP: 162, AC: 18, Attack: 14, AttackBonus: 11, Speed: 12, BlockRate: 0.15, XPValue: 5000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The giant makes two greatsword attacks."},
|
"fire_giant": {ID: "fire_giant", Name: "Fire Giant", CR: 9, HP: 162, AC: 18, Attack: 14, AttackBonus: 11, Speed: 12, BlockRate: 0.15, XPValue: 5000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The giant makes two greatsword attacks."},
|
||||||
"glabrezu": {ID: "glabrezu", Name: "Glabrezu", CR: 9, HP: 157, AC: 17, Attack: 14, AttackBonus: 9, Speed: 16, BlockRate: 0.1, XPValue: 5000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The glabrezu makes four attacks: two with its pincers and two with its fists. Traits: Innate Spellcasting, Magic Resistance"},
|
"glabrezu": {ID: "glabrezu", Name: "Glabrezu", CR: 9, HP: 157, AC: 17, Attack: 14, AttackBonus: 9, Speed: 16, BlockRate: 0.1, XPValue: 5000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The glabrezu makes four attacks: two with its pincers and two with its fists. Traits: Innate Spellcasting, Magic Resistance"},
|
||||||
"treant": {ID: "treant", Name: "Treant", CR: 9, HP: 138, AC: 16, Attack: 14, AttackBonus: 10, Speed: 12, BlockRate: 0.1, XPValue: 5000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The treant makes two slam attacks. Traits: False Appearance, Siege Monster"},
|
"treant": {ID: "treant", Name: "Treant", CR: 9, HP: 138, AC: 16, Attack: 14, AttackBonus: 10, Speed: 12, BlockRate: 0.1, XPValue: 5000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The treant makes two slam attacks. Traits: False Appearance, Siege Monster"},
|
||||||
"young_blue_dragon": {ID: "young_blue_dragon", Name: "Young Blue Dragon", CR: 9, HP: 152, AC: 18, Attack: 14, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 5000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws."},
|
"young_blue_dragon": {ID: "young_blue_dragon", Name: "Young Blue Dragon", CR: 9, HP: 152, AC: 18, Attack: 14, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 5000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws."},
|
||||||
"young_silver_dragon": {ID: "young_silver_dragon", Name: "Young Silver Dragon", CR: 9, HP: 168, AC: 18, Attack: 14, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 5000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws."},
|
"young_silver_dragon": {ID: "young_silver_dragon", Name: "Young Silver Dragon", CR: 9, HP: 168, AC: 18, Attack: 14, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 5000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws."},
|
||||||
"aboleth": {ID: "aboleth", Name: "Aboleth", CR: 10, HP: 135, AC: 17, Attack: 16, AttackBonus: 9, Speed: 6, BlockRate: 0.1, XPValue: 5900, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Multiattack: The aboleth makes three tentacle attacks. Traits: Amphibious, Mucous Cloud, Probing Telepathy"},
|
"aboleth": {ID: "aboleth", Name: "Aboleth", CR: 10, HP: 135, AC: 17, Attack: 16, AttackBonus: 9, Speed: 6, BlockRate: 0.1, XPValue: 5900, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Multiattack: The aboleth makes three tentacle attacks. Traits: Amphibious, Mucous Cloud, Probing Telepathy"},
|
||||||
"deva": {ID: "deva", Name: "Deva", CR: 10, HP: 136, AC: 17, Attack: 16, AttackBonus: 8, Speed: 12, BlockRate: 0.1, XPValue: 5900, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The deva makes two melee attacks. Traits: Angelic Weapons, Innate Spellcasting, Magic Resistance"},
|
"deva": {ID: "deva", Name: "Deva", CR: 10, HP: 136, AC: 17, Attack: 16, AttackBonus: 8, Speed: 12, BlockRate: 0.1, XPValue: 5900, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The deva makes two melee attacks. Traits: Angelic Weapons, Innate Spellcasting, Magic Resistance"},
|
||||||
"guardian_naga": {ID: "guardian_naga", Name: "Guardian Naga", CR: 10, HP: 127, AC: 18, Attack: 16, AttackBonus: 8, Speed: 16, BlockRate: 0.15, XPValue: 5900, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Rejuvenation, Spellcasting"},
|
"guardian_naga": {ID: "guardian_naga", Name: "Guardian Naga", CR: 10, HP: 127, AC: 18, Attack: 16, AttackBonus: 8, Speed: 16, BlockRate: 0.15, XPValue: 5900, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Rejuvenation, Spellcasting"},
|
||||||
"stone_golem": {ID: "stone_golem", Name: "Stone Golem", CR: 10, HP: 178, AC: 17, Attack: 16, AttackBonus: 10, Speed: 12, BlockRate: 0.1, XPValue: 5900, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The golem makes two slam attacks. Traits: Immutable Form, Magic Resistance, Magic Weapons"},
|
"stone_golem": {ID: "stone_golem", Name: "Stone Golem", CR: 10, HP: 178, AC: 17, Attack: 16, AttackBonus: 10, Speed: 12, BlockRate: 0.1, XPValue: 5900, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The golem makes two slam attacks. Traits: Immutable Form, Magic Resistance, Magic Weapons"},
|
||||||
"young_gold_dragon": {ID: "young_gold_dragon", Name: "Young Gold Dragon", CR: 10, HP: 178, AC: 18, Attack: 16, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 5900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Amphibious", FireAttacker: true},
|
"young_gold_dragon": {ID: "young_gold_dragon", Name: "Young Gold Dragon", CR: 10, HP: 178, AC: 18, Attack: 16, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 5900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws. Traits: Amphibious", FireAttacker: true},
|
||||||
"young_red_dragon": {ID: "young_red_dragon", Name: "Young Red Dragon", CR: 10, HP: 178, AC: 18, Attack: 16, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 5900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws.", FireAttacker: true},
|
"young_red_dragon": {ID: "young_red_dragon", Name: "Young Red Dragon", CR: 10, HP: 178, AC: 18, Attack: 16, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 5900, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon makes three attacks: one with its bite and two with its claws.", FireAttacker: true},
|
||||||
"behir": {ID: "behir", Name: "Behir", CR: 11, HP: 168, AC: 17, Attack: 18, AttackBonus: 0, Speed: 18, BlockRate: 0.1, XPValue: 7200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The behir makes two attacks: one with its bite and one to constrict."},
|
"behir": {ID: "behir", Name: "Behir", CR: 11, HP: 168, AC: 17, Attack: 18, AttackBonus: 0, Speed: 18, BlockRate: 0.1, XPValue: 7200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The behir makes two attacks: one with its bite and one to constrict."},
|
||||||
"chain_devil": {ID: "chain_devil", Name: "Chain Devil", CR: 11, HP: 85, AC: 16, Attack: 18, AttackBonus: 8, Speed: 12, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes two attacks with its chains. Traits: Devil's Sight, Magic Resistance"},
|
"chain_devil": {ID: "chain_devil", Name: "Chain Devil", CR: 11, HP: 85, AC: 16, Attack: 18, AttackBonus: 8, Speed: 12, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes two attacks with its chains. Traits: Devil's Sight, Magic Resistance"},
|
||||||
"djinni": {ID: "djinni", Name: "Djinni", CR: 11, HP: 161, AC: 17, Attack: 18, AttackBonus: 9, Speed: 12, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Multiattack: The djinni makes three scimitar attacks. Traits: Elemental Demise, Innate Spellcasting, Variant: Genie Powers"},
|
"djinni": {ID: "djinni", Name: "Djinni", CR: 11, HP: 161, AC: 17, Attack: 18, AttackBonus: 9, Speed: 12, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Multiattack: The djinni makes three scimitar attacks. Traits: Elemental Demise, Innate Spellcasting, Variant: Genie Powers"},
|
||||||
"efreeti": {ID: "efreeti", Name: "Efreeti", CR: 11, HP: 200, AC: 17, Attack: 18, AttackBonus: 7, Speed: 16, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Multiattack: The efreeti makes two scimitar attacks or uses its Hurl Flame twice. Traits: Elemental Demise, Innate Spellcasting, Variant: Genie Powers", FireAttacker: true},
|
"efreeti": {ID: "efreeti", Name: "Efreeti", CR: 11, HP: 200, AC: 17, Attack: 18, AttackBonus: 7, Speed: 16, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Multiattack: The efreeti makes two scimitar attacks or uses its Hurl Flame twice. Traits: Elemental Demise, Innate Spellcasting, Variant: Genie Powers", FireAttacker: true},
|
||||||
"gynosphinx": {ID: "gynosphinx", Name: "Gynosphinx", CR: 11, HP: 136, AC: 17, Attack: 18, AttackBonus: 9, Speed: 16, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The sphinx makes two claw attacks. Traits: Inscrutable, Magic Weapons, Spellcasting"},
|
"gynosphinx": {ID: "gynosphinx", Name: "Gynosphinx", CR: 11, HP: 136, AC: 17, Attack: 18, AttackBonus: 9, Speed: 16, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The sphinx makes two claw attacks. Traits: Inscrutable, Magic Weapons, Spellcasting"},
|
||||||
"horned_devil": {ID: "horned_devil", Name: "Horned Devil", CR: 11, HP: 178, AC: 18, Attack: 18, AttackBonus: 10, Speed: 8, BlockRate: 0.15, XPValue: 7200, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes three melee attacks: two with its fork and one with its tail. Traits: Devil's Sight, Magic Resistance"},
|
"horned_devil": {ID: "horned_devil", Name: "Horned Devil", CR: 11, HP: 178, AC: 18, Attack: 18, AttackBonus: 10, Speed: 8, BlockRate: 0.15, XPValue: 7200, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes three melee attacks: two with its fork and one with its tail. Traits: Devil's Sight, Magic Resistance"},
|
||||||
"remorhaz": {ID: "remorhaz", Name: "Remorhaz", CR: 11, HP: 195, AC: 17, Attack: 18, AttackBonus: 11, Speed: 12, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Traits: Heated Body"},
|
"remorhaz": {ID: "remorhaz", Name: "Remorhaz", CR: 11, HP: 195, AC: 17, Attack: 18, AttackBonus: 11, Speed: 12, BlockRate: 0.1, XPValue: 7200, Ability: &MonsterAbility{Name: "Damaging Aura", Phase: "any", ProcChance: 0.4, Effect: "retaliate"}, Notes: "SRD-tuned baseline — Damaging Aura (retaliate) wired from traits. Traits: Heated Body"},
|
||||||
"roc": {ID: "roc", Name: "Roc", CR: 11, HP: 248, AC: 15, Attack: 18, AttackBonus: 13, Speed: 8, BlockRate: 0.1, XPValue: 7200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The roc makes two attacks: one with its beak and one with its talons. Traits: Keen Sight"},
|
"roc": {ID: "roc", Name: "Roc", CR: 11, HP: 248, AC: 15, Attack: 18, AttackBonus: 13, Speed: 8, BlockRate: 0.1, XPValue: 7200, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The roc makes two attacks: one with its beak and one with its talons. Traits: Keen Sight"},
|
||||||
"archmage": {ID: "archmage", Name: "Archmage", CR: 12, HP: 99, AC: 12, Attack: 19, AttackBonus: 6, Speed: 12, BlockRate: 0, XPValue: 8400, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Magic Resistance, Spellcasting"},
|
"archmage": {ID: "archmage", Name: "Archmage", CR: 12, HP: 99, AC: 12, Attack: 19, AttackBonus: 6, Speed: 12, BlockRate: 0, XPValue: 8400, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Magic Resistance, Spellcasting"},
|
||||||
"erinyes": {ID: "erinyes", Name: "Erinyes", CR: 12, HP: 153, AC: 18, Attack: 19, AttackBonus: 8, Speed: 12, BlockRate: 0.15, XPValue: 8400, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The erinyes makes three attacks Traits: Hellish Weapons, Magic Resistance"},
|
"erinyes": {ID: "erinyes", Name: "Erinyes", CR: 12, HP: 153, AC: 18, Attack: 19, AttackBonus: 8, Speed: 12, BlockRate: 0.15, XPValue: 8400, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The erinyes makes three attacks Traits: Hellish Weapons, Magic Resistance"},
|
||||||
"adult_brass_dragon": {ID: "adult_brass_dragon", Name: "Adult Brass Dragon", CR: 13, HP: 172, AC: 18, Attack: 21, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 10000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)", FireAttacker: true},
|
"adult_brass_dragon": {ID: "adult_brass_dragon", Name: "Adult Brass Dragon", CR: 13, HP: 172, AC: 18, Attack: 21, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 10000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)", FireAttacker: true},
|
||||||
"adult_white_dragon": {ID: "adult_white_dragon", Name: "Adult White Dragon", CR: 13, HP: 200, AC: 18, Attack: 21, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 10000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Ice Walk, Legendary Resistance (3/Day)"},
|
"adult_white_dragon": {ID: "adult_white_dragon", Name: "Adult White Dragon", CR: 13, HP: 200, AC: 18, Attack: 21, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 10000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Ice Walk, Legendary Resistance (3/Day)"},
|
||||||
"nalfeshnee": {ID: "nalfeshnee", Name: "Nalfeshnee", CR: 13, HP: 184, AC: 18, Attack: 21, AttackBonus: 10, Speed: 8, BlockRate: 0.15, XPValue: 10000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The nalfeshnee uses Horror Nimbus if it can. Traits: Magic Resistance"},
|
"nalfeshnee": {ID: "nalfeshnee", Name: "Nalfeshnee", CR: 13, HP: 184, AC: 18, Attack: 21, AttackBonus: 10, Speed: 8, BlockRate: 0.15, XPValue: 10000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The nalfeshnee uses Horror Nimbus if it can. Traits: Magic Resistance"},
|
||||||
"rakshasa": {ID: "rakshasa", Name: "Rakshasa", CR: 13, HP: 110, AC: 16, Attack: 21, AttackBonus: 7, Speed: 16, BlockRate: 0.1, XPValue: 10000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The rakshasa makes two claw attacks Traits: Limited Magic Immunity, Innate Spellcasting"},
|
"rakshasa": {ID: "rakshasa", Name: "Rakshasa", CR: 13, HP: 110, AC: 16, Attack: 21, AttackBonus: 7, Speed: 16, BlockRate: 0.1, XPValue: 10000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The rakshasa makes two claw attacks Traits: Limited Magic Immunity, Innate Spellcasting"},
|
||||||
"storm_giant": {ID: "storm_giant", Name: "Storm Giant", CR: 13, HP: 230, AC: 16, Attack: 21, AttackBonus: 0, Speed: 18, BlockRate: 0.1, XPValue: 10000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The giant makes two greatsword attacks. Traits: Amphibious, Innate Spellcasting"},
|
"storm_giant": {ID: "storm_giant", Name: "Storm Giant", CR: 13, HP: 230, AC: 16, Attack: 21, AttackBonus: 0, Speed: 18, BlockRate: 0.1, XPValue: 10000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The giant makes two greatsword attacks. Traits: Amphibious, Innate Spellcasting"},
|
||||||
"vampire": {ID: "vampire", Name: "Vampire", CR: 13, HP: 144, AC: 16, Attack: 21, AttackBonus: 9, Speed: 12, BlockRate: 0.1, XPValue: 10000, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Traits: Shapechanger, Legendary Resistance (3/Day), Misty Escape, Regeneration, Spider Climb, Vampire Weaknesses"},
|
"vampire": {ID: "vampire", Name: "Vampire", CR: 13, HP: 144, AC: 16, Attack: 21, AttackBonus: 9, Speed: 12, BlockRate: 0.1, XPValue: 10000, Ability: &MonsterAbility{Name: "Regeneration", Phase: "any", ProcChance: 0.5, Effect: "regenerate"}, Notes: "SRD-tuned baseline — Regeneration (regenerate) wired from traits. Traits: Shapechanger, Legendary Resistance (3/Day), Misty Escape, Regeneration, Spider Climb, Vampire Weaknesses"},
|
||||||
"adult_black_dragon": {ID: "adult_black_dragon", Name: "Adult Black Dragon", CR: 14, HP: 195, AC: 19, Attack: 23, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 11500, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
|
"adult_black_dragon": {ID: "adult_black_dragon", Name: "Adult Black Dragon", CR: 14, HP: 195, AC: 19, Attack: 23, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 11500, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
|
||||||
"adult_copper_dragon": {ID: "adult_copper_dragon", Name: "Adult Copper Dragon", CR: 14, HP: 184, AC: 18, Attack: 23, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 11500, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
|
"adult_copper_dragon": {ID: "adult_copper_dragon", Name: "Adult Copper Dragon", CR: 14, HP: 184, AC: 18, Attack: 23, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 11500, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
|
||||||
"ice_devil": {ID: "ice_devil", Name: "Ice Devil", CR: 14, HP: 180, AC: 18, Attack: 23, AttackBonus: 10, Speed: 16, BlockRate: 0.15, XPValue: 11500, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes three attacks: one with its bite, one with its claws, and one with its tail. Traits: Devil's Sight, Magic Resistance"},
|
"ice_devil": {ID: "ice_devil", Name: "Ice Devil", CR: 14, HP: 180, AC: 18, Attack: 23, AttackBonus: 10, Speed: 16, BlockRate: 0.15, XPValue: 11500, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The devil makes three attacks: one with its bite, one with its claws, and one with its tail. Traits: Devil's Sight, Magic Resistance"},
|
||||||
"adult_bronze_dragon": {ID: "adult_bronze_dragon", Name: "Adult Bronze Dragon", CR: 15, HP: 212, AC: 19, Attack: 24, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 13000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
|
"adult_bronze_dragon": {ID: "adult_bronze_dragon", Name: "Adult Bronze Dragon", CR: 15, HP: 212, AC: 19, Attack: 24, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 13000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
|
||||||
"adult_green_dragon": {ID: "adult_green_dragon", Name: "Adult Green Dragon", CR: 15, HP: 207, AC: 19, Attack: 24, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 13000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
|
"adult_green_dragon": {ID: "adult_green_dragon", Name: "Adult Green Dragon", CR: 15, HP: 207, AC: 19, Attack: 24, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 13000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
|
||||||
"mummy_lord": {ID: "mummy_lord", Name: "Mummy Lord", CR: 15, HP: 97, AC: 17, Attack: 24, AttackBonus: 9, Speed: 8, BlockRate: 0.1, XPValue: 13000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The mummy can use its Dreadful Glare and makes one attack with its rotting fist. Traits: Magic Resistance, Rejuvenation, Spellcasting"},
|
"mummy_lord": {ID: "mummy_lord", Name: "Mummy Lord", CR: 15, HP: 97, AC: 17, Attack: 24, AttackBonus: 9, Speed: 8, BlockRate: 0.1, XPValue: 13000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The mummy can use its Dreadful Glare and makes one attack with its rotting fist. Traits: Magic Resistance, Rejuvenation, Spellcasting"},
|
||||||
"purple_worm": {ID: "purple_worm", Name: "Purple Worm", CR: 15, HP: 247, AC: 18, Attack: 24, AttackBonus: 9, Speed: 18, BlockRate: 0.15, XPValue: 13000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The worm makes two attacks: one with its bite and one with its stinger. Traits: Tunneler"},
|
"purple_worm": {ID: "purple_worm", Name: "Purple Worm", CR: 15, HP: 247, AC: 18, Attack: 24, AttackBonus: 9, Speed: 18, BlockRate: 0.15, XPValue: 13000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The worm makes two attacks: one with its bite and one with its stinger. Traits: Tunneler"},
|
||||||
"adult_blue_dragon": {ID: "adult_blue_dragon", Name: "Adult Blue Dragon", CR: 16, HP: 225, AC: 19, Attack: 26, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
|
"adult_blue_dragon": {ID: "adult_blue_dragon", Name: "Adult Blue Dragon", CR: 16, HP: 225, AC: 19, Attack: 26, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
|
||||||
"adult_silver_dragon": {ID: "adult_silver_dragon", Name: "Adult Silver Dragon", CR: 16, HP: 243, AC: 19, Attack: 26, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
|
"adult_silver_dragon": {ID: "adult_silver_dragon", Name: "Adult Silver Dragon", CR: 16, HP: 243, AC: 19, Attack: 26, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
|
||||||
"iron_golem": {ID: "iron_golem", Name: "Iron Golem", CR: 16, HP: 210, AC: 20, Attack: 26, AttackBonus: 0, Speed: 12, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The golem makes two melee attacks. Traits: Fire Absorption, Immutable Form, Magic Resistance, Magic Weapons"},
|
"iron_golem": {ID: "iron_golem", Name: "Iron Golem", CR: 16, HP: 210, AC: 20, Attack: 26, AttackBonus: 0, Speed: 12, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The golem makes two melee attacks. Traits: Fire Absorption, Immutable Form, Magic Resistance, Magic Weapons"},
|
||||||
"marilith": {ID: "marilith", Name: "Marilith", CR: 16, HP: 189, AC: 18, Attack: 26, AttackBonus: 9, Speed: 16, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The marilith can make seven attacks: six with its longswords and one with its tail. Traits: Magic Resistance, Magic Weapons, Reactive"},
|
"marilith": {ID: "marilith", Name: "Marilith", CR: 16, HP: 189, AC: 18, Attack: 26, AttackBonus: 9, Speed: 16, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The marilith can make seven attacks: six with its longswords and one with its tail. Traits: Magic Resistance, Magic Weapons, Reactive"},
|
||||||
"planetar": {ID: "planetar", Name: "Planetar", CR: 16, HP: 200, AC: 19, Attack: 26, AttackBonus: 12, Speed: 16, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The planetar makes two melee attacks. Traits: Angelic Weapons, Divine Awareness, Innate Spellcasting, Magic Resistance"},
|
"planetar": {ID: "planetar", Name: "Planetar", CR: 16, HP: 200, AC: 19, Attack: 26, AttackBonus: 12, Speed: 16, BlockRate: 0.15, XPValue: 15000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The planetar makes two melee attacks. Traits: Angelic Weapons, Divine Awareness, Innate Spellcasting, Magic Resistance"},
|
||||||
"adult_gold_dragon": {ID: "adult_gold_dragon", Name: "Adult Gold Dragon", CR: 17, HP: 256, AC: 19, Attack: 28, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 18000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)", FireAttacker: true},
|
"adult_gold_dragon": {ID: "adult_gold_dragon", Name: "Adult Gold Dragon", CR: 17, HP: 256, AC: 19, Attack: 28, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 18000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)", FireAttacker: true},
|
||||||
"adult_red_dragon": {ID: "adult_red_dragon", Name: "Adult Red Dragon", CR: 17, HP: 256, AC: 19, Attack: 28, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 18000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)", FireAttacker: true},
|
"adult_red_dragon": {ID: "adult_red_dragon", Name: "Adult Red Dragon", CR: 17, HP: 256, AC: 19, Attack: 28, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 18000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)", FireAttacker: true},
|
||||||
"androsphinx": {ID: "androsphinx", Name: "Androsphinx", CR: 17, HP: 199, AC: 17, Attack: 28, AttackBonus: 12, Speed: 16, BlockRate: 0.1, XPValue: 18000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The sphinx makes two claw attacks. Traits: Inscrutable, Magic Weapons, Spellcasting"},
|
"androsphinx": {ID: "androsphinx", Name: "Androsphinx", CR: 17, HP: 199, AC: 17, Attack: 28, AttackBonus: 12, Speed: 16, BlockRate: 0.1, XPValue: 18000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The sphinx makes two claw attacks. Traits: Inscrutable, Magic Weapons, Spellcasting"},
|
||||||
"dragon_turtle": {ID: "dragon_turtle", Name: "Dragon Turtle", CR: 17, HP: 341, AC: 20, Attack: 28, AttackBonus: 0, Speed: 8, BlockRate: 0.15, XPValue: 18000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon turtle makes three attacks: one with its bite and two with its claws. Traits: Amphibious", FireAttacker: true},
|
"dragon_turtle": {ID: "dragon_turtle", Name: "Dragon Turtle", CR: 17, HP: 341, AC: 20, Attack: 28, AttackBonus: 0, Speed: 8, BlockRate: 0.15, XPValue: 18000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The dragon turtle makes three attacks: one with its bite and two with its claws. Traits: Amphibious", FireAttacker: true},
|
||||||
"balor": {ID: "balor", Name: "Balor", CR: 19, HP: 262, AC: 19, Attack: 31, AttackBonus: 14, Speed: 16, BlockRate: 0.15, XPValue: 22000, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Multiattack: The balor makes two attacks: one with its longsword and one with its whip. Traits: Death Throes, Fire Aura, Magic Resistance, Magic Weapons"},
|
"balor": {ID: "balor", Name: "Balor", CR: 19, HP: 262, AC: 19, Attack: 31, AttackBonus: 14, Speed: 16, BlockRate: 0.15, XPValue: 22000, Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1, Effect: "death_aoe"}, Notes: "SRD-tuned baseline — Death Burst (death_aoe) wired from traits. Multiattack: The balor makes two attacks: one with its longsword and one with its whip. Traits: Death Throes, Fire Aura, Magic Resistance, Magic Weapons"},
|
||||||
"ancient_brass_dragon": {ID: "ancient_brass_dragon", Name: "Ancient Brass Dragon", CR: 20, HP: 297, AC: 20, Attack: 33, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 25000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)", FireAttacker: true},
|
"ancient_brass_dragon": {ID: "ancient_brass_dragon", Name: "Ancient Brass Dragon", CR: 20, HP: 297, AC: 20, Attack: 33, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 25000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)", FireAttacker: true},
|
||||||
"ancient_white_dragon": {ID: "ancient_white_dragon", Name: "Ancient White Dragon", CR: 20, HP: 333, AC: 20, Attack: 33, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 25000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Ice Walk, Legendary Resistance (3/Day)"},
|
"ancient_white_dragon": {ID: "ancient_white_dragon", Name: "Ancient White Dragon", CR: 20, HP: 333, AC: 20, Attack: 33, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 25000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Ice Walk, Legendary Resistance (3/Day)"},
|
||||||
"pit_fiend": {ID: "pit_fiend", Name: "Pit Fiend", CR: 20, HP: 300, AC: 19, Attack: 33, AttackBonus: 14, Speed: 12, BlockRate: 0.15, XPValue: 25000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The pit fiend makes four attacks: one with its bite, one with its claw, one with its mace, and one with its tail. Traits: Fear Aura, Magic Resistance, Magic Weapons, Innate Spellcasting"},
|
"pit_fiend": {ID: "pit_fiend", Name: "Pit Fiend", CR: 20, HP: 300, AC: 19, Attack: 33, AttackBonus: 14, Speed: 12, BlockRate: 0.15, XPValue: 25000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The pit fiend makes four attacks: one with its bite, one with its claw, one with its mace, and one with its tail. Traits: Fear Aura, Magic Resistance, Magic Weapons, Innate Spellcasting"},
|
||||||
"ancient_black_dragon": {ID: "ancient_black_dragon", Name: "Ancient Black Dragon", CR: 21, HP: 367, AC: 22, Attack: 34, AttackBonus: 15, Speed: 16, BlockRate: 0.15, XPValue: 33000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
|
"ancient_black_dragon": {ID: "ancient_black_dragon", Name: "Ancient Black Dragon", CR: 21, HP: 367, AC: 22, Attack: 34, AttackBonus: 15, Speed: 16, BlockRate: 0.15, XPValue: 33000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
|
||||||
"ancient_copper_dragon": {ID: "ancient_copper_dragon", Name: "Ancient Copper Dragon", CR: 21, HP: 350, AC: 21, Attack: 34, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 33000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
|
"ancient_copper_dragon": {ID: "ancient_copper_dragon", Name: "Ancient Copper Dragon", CR: 21, HP: 350, AC: 21, Attack: 34, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 33000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
|
||||||
"lich": {ID: "lich", Name: "Lich", CR: 21, HP: 135, AC: 17, Attack: 34, AttackBonus: 12, Speed: 12, BlockRate: 0.1, XPValue: 33000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Legendary Resistance (3/Day), Rejuvenation, Spellcasting, Turn Resistance"},
|
"lich": {ID: "lich", Name: "Lich", CR: 21, HP: 135, AC: 17, Attack: 34, AttackBonus: 12, Speed: 12, BlockRate: 0.1, XPValue: 33000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Traits: Legendary Resistance (3/Day), Rejuvenation, Spellcasting, Turn Resistance"},
|
||||||
"solar": {ID: "solar", Name: "Solar", CR: 21, HP: 243, AC: 21, Attack: 34, AttackBonus: 15, Speed: 18, BlockRate: 0.15, XPValue: 33000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The solar makes two greatsword attacks. Traits: Angelic Weapons, Divine Awareness, Innate Spellcasting, Magic Resistance"},
|
"solar": {ID: "solar", Name: "Solar", CR: 21, HP: 243, AC: 21, Attack: 34, AttackBonus: 15, Speed: 18, BlockRate: 0.15, XPValue: 33000, Ability: &MonsterAbility{Name: "Spellcasting", Phase: "opening", ProcChance: 0.5, Effect: "aoe"}, Notes: "SRD-tuned baseline — Spellcasting (aoe) wired from traits. Multiattack: The solar makes two greatsword attacks. Traits: Angelic Weapons, Divine Awareness, Innate Spellcasting, Magic Resistance"},
|
||||||
"ancient_bronze_dragon": {ID: "ancient_bronze_dragon", Name: "Ancient Bronze Dragon", CR: 22, HP: 444, AC: 22, Attack: 36, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 41000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
|
"ancient_bronze_dragon": {ID: "ancient_bronze_dragon", Name: "Ancient Bronze Dragon", CR: 22, HP: 444, AC: 22, Attack: 36, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 41000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
|
||||||
"ancient_green_dragon": {ID: "ancient_green_dragon", Name: "Ancient Green Dragon", CR: 22, HP: 385, AC: 21, Attack: 36, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 41000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
|
"ancient_green_dragon": {ID: "ancient_green_dragon", Name: "Ancient Green Dragon", CR: 22, HP: 385, AC: 21, Attack: 36, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 41000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)"},
|
||||||
"ancient_blue_dragon": {ID: "ancient_blue_dragon", Name: "Ancient Blue Dragon", CR: 23, HP: 481, AC: 22, Attack: 37, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 50000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
|
"ancient_blue_dragon": {ID: "ancient_blue_dragon", Name: "Ancient Blue Dragon", CR: 23, HP: 481, AC: 22, Attack: 37, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 50000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
|
||||||
"ancient_silver_dragon": {ID: "ancient_silver_dragon", Name: "Ancient Silver Dragon", CR: 23, HP: 487, AC: 22, Attack: 37, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 50000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
|
"ancient_silver_dragon": {ID: "ancient_silver_dragon", Name: "Ancient Silver Dragon", CR: 23, HP: 487, AC: 22, Attack: 37, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 50000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)"},
|
||||||
"kraken": {ID: "kraken", Name: "Kraken", CR: 23, HP: 472, AC: 18, Attack: 37, AttackBonus: 7, Speed: 8, BlockRate: 0.15, XPValue: 50000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The kraken makes three tentacle attacks, each of which it can replace with one use of Fling. Traits: Amphibious, Freedom of Movement, Siege Monster"},
|
"kraken": {ID: "kraken", Name: "Kraken", CR: 23, HP: 472, AC: 18, Attack: 37, AttackBonus: 7, Speed: 8, BlockRate: 0.15, XPValue: 50000, Notes: "SRD-tuned baseline — no ability wired (traits are non-combat). Multiattack: The kraken makes three tentacle attacks, each of which it can replace with one use of Fling. Traits: Amphibious, Freedom of Movement, Siege Monster"},
|
||||||
"ancient_gold_dragon": {ID: "ancient_gold_dragon", Name: "Ancient Gold Dragon", CR: 24, HP: 546, AC: 22, Attack: 38, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 62000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)", FireAttacker: true},
|
"ancient_gold_dragon": {ID: "ancient_gold_dragon", Name: "Ancient Gold Dragon", CR: 24, HP: 546, AC: 22, Attack: 38, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 62000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Amphibious, Legendary Resistance (3/Day)", FireAttacker: true},
|
||||||
"ancient_red_dragon": {ID: "ancient_red_dragon", Name: "Ancient Red Dragon", CR: 24, HP: 546, AC: 22, Attack: 38, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 62000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)", FireAttacker: true},
|
"ancient_red_dragon": {ID: "ancient_red_dragon", Name: "Ancient Red Dragon", CR: 24, HP: 546, AC: 22, Attack: 38, AttackBonus: 0, Speed: 16, BlockRate: 0.15, XPValue: 62000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The dragon can use its Frightful Presence. Traits: Legendary Resistance (3/Day)", FireAttacker: true},
|
||||||
"tarrasque": {ID: "tarrasque", Name: "Tarrasque", CR: 30, HP: 676, AC: 25, Attack: 46, AttackBonus: 19, Speed: 16, BlockRate: 0.15, XPValue: 155000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The tarrasque can use its Frightful Presence. Traits: Legendary Resistance (3/Day), Magic Resistance, Reflective Carapace, Siege Monster"},
|
"tarrasque": {ID: "tarrasque", Name: "Tarrasque", CR: 30, HP: 676, AC: 25, Attack: 46, AttackBonus: 19, Speed: 16, BlockRate: 0.15, XPValue: 155000, Ability: &MonsterAbility{Name: "Magic Resistance", Phase: "any", ProcChance: 0.5, Effect: "spell_resist"}, Notes: "SRD-tuned baseline — Magic Resistance (spell_resist) wired from traits. Multiattack: The tarrasque can use its Frightful Presence. Traits: Legendary Resistance (3/Day), Magic Resistance, Reflective Carapace, Siege Monster"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,11 +141,11 @@ type bjTable struct {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
type bjConfig struct {
|
type bjConfig struct {
|
||||||
TimeoutSeconds int
|
TimeoutSeconds int
|
||||||
AutoplayThreshold int
|
AutoplayThreshold int
|
||||||
MinBet float64
|
MinBet float64
|
||||||
MaxBet float64
|
MaxBet float64
|
||||||
DebtLimit float64
|
DebtLimit float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadBJConfig() bjConfig {
|
func loadBJConfig() bjConfig {
|
||||||
@@ -877,5 +877,3 @@ func (p *BlackjackPlugin) recordBJScore(userID id.UserID, net float64) {
|
|||||||
recordBotDefeat(userID, "blackjack")
|
recordBotDefeat(userID, "blackjack")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -592,4 +592,3 @@ func (p *AdventurePlugin) resolveDungeonAction(
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,13 +46,13 @@ type CombatStats struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CombatModifiers struct {
|
type CombatModifiers struct {
|
||||||
DamageBonus float64 // additive: 0 = neutral, 0.25 = +25% damage. Applied as (1 + DamageBonus).
|
DamageBonus float64 // additive: 0 = neutral, 0.25 = +25% damage. Applied as (1 + DamageBonus).
|
||||||
DamageReduct float64 // multiplicative damage-taken reduction, 1.0 = neutral
|
DamageReduct float64 // multiplicative damage-taken reduction, 1.0 = neutral
|
||||||
DeathSave bool // Sovereign reprieve — survive one lethal hit
|
DeathSave bool // Sovereign reprieve — survive one lethal hit
|
||||||
PetAttackProc float64
|
PetAttackProc float64
|
||||||
PetAttackDmg int
|
PetAttackDmg int
|
||||||
PetDeflectProc float64
|
PetDeflectProc float64
|
||||||
PetWhiffProc float64 // pet distracts enemy → guaranteed miss
|
PetWhiffProc float64 // pet distracts enemy → guaranteed miss
|
||||||
// Spiritual Weapon — separate channel from the pet so the spectral mace
|
// Spiritual Weapon — separate channel from the pet so the spectral mace
|
||||||
// gets its own narration when a cleric without a companion casts it.
|
// gets its own narration when a cleric without a companion casts it.
|
||||||
// Damage formula mirrors PetAttack (Dmg + d5), proc rolls per round.
|
// Damage formula mirrors PetAttack (Dmg + d5), proc rolls per round.
|
||||||
@@ -1298,4 +1298,3 @@ func trySave(st *combatState, player *Combatant, phaseName string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -218,8 +218,8 @@ func DeriveDungeonMonsterStats(loc *AdvLocation) (CombatStats, CombatModifiers)
|
|||||||
death := loc.BaseDeathPct // 8 to 60
|
death := loc.BaseDeathPct // 8 to 60
|
||||||
|
|
||||||
stats := CombatStats{
|
stats := CombatStats{
|
||||||
MaxHP: 12 + int(t*7+death*0.3), // T1≈22, T5≈65 — sized so dnd-scale players can finish a kill within the phase budget
|
MaxHP: 12 + int(t*7+death*0.3), // T1≈22, T5≈65 — sized so dnd-scale players can finish a kill within the phase budget
|
||||||
Attack: int(t*1.5) + int(death*0.04), // T1=1, T5=9 — tuned to dnd HP pools (players have 13–80 HP, not 100+)
|
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),
|
Defense: 2 + int(t*1.2),
|
||||||
Speed: 4 + int(t*1.5),
|
Speed: 4 + int(t*1.5),
|
||||||
CritRate: 0.03 + death*0.003,
|
CritRate: 0.03 + death*0.003,
|
||||||
|
|||||||
@@ -284,11 +284,11 @@ func TestBalanceRegression_DungeonDeathRates(t *testing.T) {
|
|||||||
// drift), not 1:1 well-equipped combat. These bounds reflect the
|
// drift), not 1:1 well-equipped combat. These bounds reflect the
|
||||||
// new "fairly breezy with some death" target.
|
// new "fairly breezy with some death" target.
|
||||||
cases := []tc{
|
cases := []tc{
|
||||||
{1, 1, advDungeons[0], 0.03, 0.0}, // T1: trivial with proper gear
|
{1, 1, advDungeons[0], 0.03, 0.0}, // T1: trivial with proper gear
|
||||||
{2, 2, advDungeons[1], 0.05, 0.0}, // T2: trivial at level
|
{2, 2, advDungeons[1], 0.05, 0.0}, // T2: trivial at level
|
||||||
{5, 3, advDungeons[2], 0.08, 0.0}, // T3: low risk
|
{5, 3, advDungeons[2], 0.08, 0.0}, // T3: low risk
|
||||||
{7, 4, advDungeons[3], 0.15, 0.0}, // T4: still mostly safe
|
{7, 4, advDungeons[3], 0.15, 0.0}, // T4: still mostly safe
|
||||||
{9, 5, advDungeons[4], 0.25, 0.0}, // T5: occasional bad luck
|
{9, 5, advDungeons[4], 0.25, 0.0}, // T5: occasional bad luck
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ type bandsintownEvent struct {
|
|||||||
Latitude string `json:"latitude"`
|
Latitude string `json:"latitude"`
|
||||||
Longitude string `json:"longitude"`
|
Longitude string `json:"longitude"`
|
||||||
} `json:"venue"`
|
} `json:"venue"`
|
||||||
Lineup []string `json:"lineup"`
|
Lineup []string `json:"lineup"`
|
||||||
OnSaleAt string `json:"on_sale_datetime"`
|
OnSaleAt string `json:"on_sale_datetime"`
|
||||||
Offers []struct {
|
Offers []struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
|||||||
@@ -228,21 +228,21 @@ func computeAC(class DnDClass, dexMod int) int {
|
|||||||
// ── DnDCharacter struct + repository ─────────────────────────────────────────
|
// ── DnDCharacter struct + repository ─────────────────────────────────────────
|
||||||
|
|
||||||
type DnDCharacter struct {
|
type DnDCharacter struct {
|
||||||
UserID id.UserID
|
UserID id.UserID
|
||||||
Race DnDRace
|
Race DnDRace
|
||||||
Class DnDClass
|
Class DnDClass
|
||||||
Level int
|
Level int
|
||||||
XP int
|
XP int
|
||||||
STR, DEX, CON int
|
STR, DEX, CON int
|
||||||
INT, WIS, CHA int
|
INT, WIS, CHA int
|
||||||
HPCurrent int
|
HPCurrent int
|
||||||
HPMax int
|
HPMax int
|
||||||
TempHP int
|
TempHP int
|
||||||
ArmorClass int
|
ArmorClass int
|
||||||
PendingSetup bool
|
PendingSetup bool
|
||||||
AutoMigrated bool
|
AutoMigrated bool
|
||||||
OnboardingSent bool
|
OnboardingSent bool
|
||||||
ArmedAbility string
|
ArmedAbility string
|
||||||
// Phase 9 — spell layer.
|
// Phase 9 — spell layer.
|
||||||
// PendingCast: JSON blob describing the queued spell to fire on next
|
// PendingCast: JSON blob describing the queued spell to fire on next
|
||||||
// one-shot combat (empty string = nothing queued). Set by !cast for
|
// one-shot combat (empty string = nothing queued). Set by !cast for
|
||||||
@@ -251,30 +251,30 @@ type DnDCharacter struct {
|
|||||||
// (empty = nothing active). ConcentrationExpiresAt is the wall-clock
|
// (empty = nothing active). ConcentrationExpiresAt is the wall-clock
|
||||||
// expiry; nil for indefinite. Casting another concentration spell
|
// expiry; nil for indefinite. Casting another concentration spell
|
||||||
// supersedes whatever's here.
|
// supersedes whatever's here.
|
||||||
PendingCast string
|
PendingCast string
|
||||||
ConcentrationSpell string
|
ConcentrationSpell string
|
||||||
ConcentrationExpiresAt *time.Time
|
ConcentrationExpiresAt *time.Time
|
||||||
// Phase 10 — subclass.
|
// Phase 10 — subclass.
|
||||||
// Subclass: empty until the player runs !subclass at L5+. Once set,
|
// Subclass: empty until the player runs !subclass at L5+. Once set,
|
||||||
// changing requires `!subclass <name>` again subject to a 30-day
|
// changing requires `!subclass <name>` again subject to a 30-day
|
||||||
// cooldown (LastSubclassRespecAt) and 500-coin cost.
|
// cooldown (LastSubclassRespecAt) and 500-coin cost.
|
||||||
Subclass DnDSubclass
|
Subclass DnDSubclass
|
||||||
LastSubclassRespecAt *time.Time
|
LastSubclassRespecAt *time.Time
|
||||||
// Phase 10 SUB2a — Berserker Frenzy increments after each rage'd
|
// Phase 10 SUB2a — Berserker Frenzy increments after each rage'd
|
||||||
// combat; future class abilities will also tick this. Cleared on
|
// combat; future class abilities will also tick this. Cleared on
|
||||||
// long rest. 5e caps at 6 (death); we let it grow and let consumers
|
// long rest. 5e caps at 6 (death); we let it grow and let consumers
|
||||||
// clamp where needed.
|
// clamp where needed.
|
||||||
Exhaustion int
|
Exhaustion int
|
||||||
// 2026-05-10 immersion: short rest charges (1/level, restored on long
|
// 2026-05-10 immersion: short rest charges (1/level, restored on long
|
||||||
// rest). resting_until gates !zone enter / !expedition start while the
|
// rest). resting_until gates !zone enter / !expedition start while the
|
||||||
// character is "still resting" — short rest = 1h, long rest = 8h.
|
// character is "still resting" — short rest = 1h, long rest = 8h.
|
||||||
ShortRestCharges int
|
ShortRestCharges int
|
||||||
RestingUntil *time.Time
|
RestingUntil *time.Time
|
||||||
LastRespecAt *time.Time
|
LastRespecAt *time.Time
|
||||||
LastShortRestAt *time.Time
|
LastShortRestAt *time.Time
|
||||||
LastLongRestAt *time.Time
|
LastLongRestAt *time.Time
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modifiers returns the six ability modifiers in STR/DEX/CON/INT/WIS/CHA order.
|
// Modifiers returns the six ability modifiers in STR/DEX/CON/INT/WIS/CHA order.
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ import (
|
|||||||
// ── Ability definitions ──────────────────────────────────────────────────────
|
// ── Ability definitions ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
type DnDAbility struct {
|
type DnDAbility struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Class DnDClass
|
Class DnDClass
|
||||||
// Subclass: if non-empty, ability is only available when the player's
|
// Subclass: if non-empty, ability is only available when the player's
|
||||||
// subclass matches. Phase 10 adds the first such ability (Berserker
|
// subclass matches. Phase 10 adds the first such ability (Berserker
|
||||||
// rage). Used by parseAbility/arm gating and by classActiveAbilities
|
// rage). Used by parseAbility/arm gating and by classActiveAbilities
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ func TestActiveAbilityCatalogue(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseAbility(t *testing.T) {
|
func TestParseAbility(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
in string
|
in string
|
||||||
id string
|
id string
|
||||||
ok bool
|
ok bool
|
||||||
}{
|
}{
|
||||||
{"second_wind", "second_wind", true},
|
{"second_wind", "second_wind", true},
|
||||||
{"second wind", "second_wind", true},
|
{"second wind", "second_wind", true},
|
||||||
|
|||||||
@@ -38,28 +38,28 @@ func setupAuditTestDB(t *testing.T) {
|
|||||||
// regardless of MaxHP parity.
|
// regardless of MaxHP parity.
|
||||||
func TestOrcRage_ThresholdExact(t *testing.T) {
|
func TestOrcRage_ThresholdExact(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
hp, maxHP int
|
hp, maxHP int
|
||||||
shouldFire bool
|
shouldFire bool
|
||||||
}{
|
}{
|
||||||
{50, 100, false}, // exactly 50% — does NOT fire (strict <)
|
{50, 100, false}, // exactly 50% — does NOT fire (strict <)
|
||||||
{49, 100, true}, // just under
|
{49, 100, true}, // just under
|
||||||
{8, 16, false}, // exactly 50% even MaxHP
|
{8, 16, false}, // exactly 50% even MaxHP
|
||||||
{7, 16, true},
|
{7, 16, true},
|
||||||
{8, 15, true}, // 8/15 = 53% — under former threshold of MaxHP/2=7. New: 8*2=16, 16<15 false. Hmm.
|
{8, 15, true}, // 8/15 = 53% — under former threshold of MaxHP/2=7. New: 8*2=16, 16<15 false. Hmm.
|
||||||
}
|
}
|
||||||
// Re-derive: HP*2 < MaxHP. So 8*2=16 < 15? No. Doesn't fire. That means at MaxHP=15, threshold trips at HP*2 < 15, i.e. HP < 7.5, i.e. HP ≤ 7.
|
// Re-derive: HP*2 < MaxHP. So 8*2=16 < 15? No. Doesn't fire. That means at MaxHP=15, threshold trips at HP*2 < 15, i.e. HP < 7.5, i.e. HP ≤ 7.
|
||||||
// Old behavior used HP < MaxHP/2 = HP < 7 (integer div). So fired at HP < 7, i.e., HP ≤ 6. Different by one.
|
// Old behavior used HP < MaxHP/2 = HP < 7 (integer div). So fired at HP < 7, i.e., HP ≤ 6. Different by one.
|
||||||
// Pick cases that distinguish.
|
// Pick cases that distinguish.
|
||||||
cases = []struct {
|
cases = []struct {
|
||||||
hp, maxHP int
|
hp, maxHP int
|
||||||
shouldFire bool
|
shouldFire bool
|
||||||
}{
|
}{
|
||||||
{50, 100, false},
|
{50, 100, false},
|
||||||
{49, 100, true},
|
{49, 100, true},
|
||||||
{8, 16, false},
|
{8, 16, false},
|
||||||
{7, 16, true},
|
{7, 16, true},
|
||||||
{6, 15, true}, // 6*2=12 < 15 ✓
|
{6, 15, true}, // 6*2=12 < 15 ✓
|
||||||
{7, 15, true}, // 7*2=14 < 15 ✓ (old code's MaxHP/2=7 would have NOT fired at HP=7)
|
{7, 15, true}, // 7*2=14 < 15 ✓ (old code's MaxHP/2=7 would have NOT fired at HP=7)
|
||||||
{8, 15, false}, // 8*2=16, not < 15
|
{8, 15, false}, // 8*2=16, not < 15
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ type DnDMonsterTemplate struct {
|
|||||||
CR float32 // challenge rating (display only for now)
|
CR float32 // challenge rating (display only for now)
|
||||||
HP int
|
HP int
|
||||||
AC int
|
AC int
|
||||||
Attack int // engine's "Attack" stat (raw damage value)
|
Attack int // engine's "Attack" stat (raw damage value)
|
||||||
AttackBonus int // d20 to-hit bonus
|
AttackBonus int // d20 to-hit bonus
|
||||||
Speed int
|
Speed int
|
||||||
BlockRate float64
|
BlockRate float64
|
||||||
Ability *MonsterAbility
|
Ability *MonsterAbility
|
||||||
@@ -176,10 +176,10 @@ var _ = func() bool {
|
|||||||
"flameskull": {
|
"flameskull": {
|
||||||
ID: "flameskull", Name: "Flameskull",
|
ID: "flameskull", Name: "Flameskull",
|
||||||
CR: 4, HP: 40, AC: 13, Attack: 7, AttackBonus: 5, Speed: 15,
|
CR: 4, HP: 40, AC: 13, Attack: 7, AttackBonus: 5, Speed: 15,
|
||||||
BlockRate: 0.05,
|
BlockRate: 0.05,
|
||||||
Ability: &MonsterAbility{Name: "Fireball", Phase: "decisive", ProcChance: 0.55, Effect: "aoe_fire"},
|
Ability: &MonsterAbility{Name: "Fireball", Phase: "decisive", ProcChance: 0.55, Effect: "aoe_fire"},
|
||||||
XPValue: 1100,
|
XPValue: 1100,
|
||||||
Notes: "Elite. Fireball 8d6 DEX DC 13. Rejuvenation in 1h unless holy water used.",
|
Notes: "Elite. Fireball 8d6 DEX DC 13. Rejuvenation in 1h unless holy water used.",
|
||||||
FireAttacker: true,
|
FireAttacker: true,
|
||||||
},
|
},
|
||||||
"boss_grol_unbroken": {
|
"boss_grol_unbroken": {
|
||||||
@@ -385,37 +385,37 @@ var _ = func() bool {
|
|||||||
"magmin": {
|
"magmin": {
|
||||||
ID: "magmin", Name: "Magmin",
|
ID: "magmin", Name: "Magmin",
|
||||||
CR: 0.5, HP: 9, AC: 14, Attack: 2, AttackBonus: 3, Speed: 10,
|
CR: 0.5, HP: 9, AC: 14, Attack: 2, AttackBonus: 3, Speed: 10,
|
||||||
BlockRate: 0.05,
|
BlockRate: 0.05,
|
||||||
Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1.00, Effect: "death_aoe"},
|
Ability: &MonsterAbility{Name: "Death Burst", Phase: "decisive", ProcChance: 1.00, Effect: "death_aoe"},
|
||||||
XPValue: 100,
|
XPValue: 100,
|
||||||
Notes: "Death Burst: 2d6 fire in 10 ft on death. Ignite flammable objects on touch.",
|
Notes: "Death Burst: 2d6 fire in 10 ft on death. Ignite flammable objects on touch.",
|
||||||
FireAttacker: true,
|
FireAttacker: true,
|
||||||
},
|
},
|
||||||
"azer": {
|
"azer": {
|
||||||
ID: "azer", Name: "Azer",
|
ID: "azer", Name: "Azer",
|
||||||
CR: 2, HP: 39, AC: 17, Attack: 4, AttackBonus: 5, Speed: 12,
|
CR: 2, HP: 39, AC: 17, Attack: 4, AttackBonus: 5, Speed: 12,
|
||||||
BlockRate: 0.20,
|
BlockRate: 0.20,
|
||||||
Ability: &MonsterAbility{Name: "Fire Aura", Phase: "any", ProcChance: 0.50, Effect: "retaliate"},
|
Ability: &MonsterAbility{Name: "Fire Aura", Phase: "any", ProcChance: 0.50, Effect: "retaliate"},
|
||||||
XPValue: 450,
|
XPValue: 450,
|
||||||
Notes: "1d10 fire to melee attackers; immune to fire; weapons deal heated bonus damage.",
|
Notes: "1d10 fire to melee attackers; immune to fire; weapons deal heated bonus damage.",
|
||||||
FireAttacker: true,
|
FireAttacker: true,
|
||||||
},
|
},
|
||||||
"salamander": {
|
"salamander": {
|
||||||
ID: "salamander", Name: "Salamander",
|
ID: "salamander", Name: "Salamander",
|
||||||
CR: 5, HP: 90, AC: 15, Attack: 8, AttackBonus: 7, Speed: 12,
|
CR: 5, HP: 90, AC: 15, Attack: 8, AttackBonus: 7, Speed: 12,
|
||||||
BlockRate: 0.10,
|
BlockRate: 0.10,
|
||||||
Ability: &MonsterAbility{Name: "Constrict", Phase: "any", ProcChance: 0.40, Effect: "stun"},
|
Ability: &MonsterAbility{Name: "Constrict", Phase: "any", ProcChance: 0.40, Effect: "stun"},
|
||||||
XPValue: 1800,
|
XPValue: 1800,
|
||||||
Notes: "Constrict grapple + 2d6 fire/turn; spear multiattack; fire aura.",
|
Notes: "Constrict grapple + 2d6 fire/turn; spear multiattack; fire aura.",
|
||||||
FireAttacker: true,
|
FireAttacker: true,
|
||||||
},
|
},
|
||||||
"fire_elemental": {
|
"fire_elemental": {
|
||||||
ID: "fire_elemental", Name: "Fire Elemental",
|
ID: "fire_elemental", Name: "Fire Elemental",
|
||||||
CR: 5, HP: 102, AC: 13, Attack: 8, AttackBonus: 6, Speed: 14,
|
CR: 5, HP: 102, AC: 13, Attack: 8, AttackBonus: 6, Speed: 14,
|
||||||
BlockRate: 0.05,
|
BlockRate: 0.05,
|
||||||
Ability: &MonsterAbility{Name: "Fire Form", Phase: "any", ProcChance: 0.50, Effect: "retaliate"},
|
Ability: &MonsterAbility{Name: "Fire Form", Phase: "any", ProcChance: 0.50, Effect: "retaliate"},
|
||||||
XPValue: 1800,
|
XPValue: 1800,
|
||||||
Notes: "1d10 contact fire damage; immune to fire; vulnerable to water/cold.",
|
Notes: "1d10 contact fire damage; immune to fire; vulnerable to water/cold.",
|
||||||
FireAttacker: true,
|
FireAttacker: true,
|
||||||
},
|
},
|
||||||
"helmed_horror": {
|
"helmed_horror": {
|
||||||
@@ -429,10 +429,10 @@ var _ = func() bool {
|
|||||||
"boss_emberlord_thyrak": {
|
"boss_emberlord_thyrak": {
|
||||||
ID: "boss_emberlord_thyrak", Name: "Emberlord Thyrak",
|
ID: "boss_emberlord_thyrak", Name: "Emberlord Thyrak",
|
||||||
CR: 9, HP: 178, AC: 18, Attack: 14, AttackBonus: 8, Speed: 11,
|
CR: 9, HP: 178, AC: 18, Attack: 14, AttackBonus: 8, Speed: 11,
|
||||||
BlockRate: 0.20,
|
BlockRate: 0.20,
|
||||||
Ability: &MonsterAbility{Name: "Forge Breath", Phase: "any", ProcChance: 0.30, Effect: "aoe"},
|
Ability: &MonsterAbility{Name: "Forge Breath", Phase: "any", ProcChance: 0.30, Effect: "aoe"},
|
||||||
XPValue: 5000,
|
XPValue: 5000,
|
||||||
Notes: "Underforge boss. Phase 2 below 50% HP — spawns 2 Fire Elementals; breath recharge 4–6.",
|
Notes: "Underforge boss. Phase 2 below 50% HP — spawns 2 Fire Elementals; breath recharge 4–6.",
|
||||||
FireAttacker: true,
|
FireAttacker: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -594,10 +594,10 @@ var _ = func() bool {
|
|||||||
"young_red_dragon": {
|
"young_red_dragon": {
|
||||||
ID: "young_red_dragon", Name: "Young Red Dragon",
|
ID: "young_red_dragon", Name: "Young Red Dragon",
|
||||||
CR: 10, HP: 178, AC: 18, Attack: 16, AttackBonus: 10, Speed: 16,
|
CR: 10, HP: 178, AC: 18, Attack: 16, AttackBonus: 10, Speed: 16,
|
||||||
BlockRate: 0.10,
|
BlockRate: 0.10,
|
||||||
Ability: &MonsterAbility{Name: "Fire Breath", Phase: "any", ProcChance: 0.30, Effect: "aoe_fire"},
|
Ability: &MonsterAbility{Name: "Fire Breath", Phase: "any", ProcChance: 0.30, Effect: "aoe_fire"},
|
||||||
XPValue: 5900,
|
XPValue: 5900,
|
||||||
Notes: "Elite. Fire Breath 16d6 DEX DC 21; Multiattack; Frightful Presence WIS DC 16.",
|
Notes: "Elite. Fire Breath 16d6 DEX DC 21; Multiattack; Frightful Presence WIS DC 16.",
|
||||||
FireAttacker: true,
|
FireAttacker: true,
|
||||||
},
|
},
|
||||||
"boss_infernax": {
|
"boss_infernax": {
|
||||||
@@ -612,10 +612,10 @@ var _ = func() bool {
|
|||||||
// 460/AC21/42 over-corrected → ~40%; this Pass 3 405/AC20/42
|
// 460/AC21/42 over-corrected → ~40%; this Pass 3 405/AC20/42
|
||||||
// lands leaders in band.)
|
// lands leaders in band.)
|
||||||
CR: 24, HP: 405, AC: 20, Attack: 38, AttackBonus: 11, Speed: 18,
|
CR: 24, HP: 405, AC: 20, Attack: 38, AttackBonus: 11, Speed: 18,
|
||||||
BlockRate: 0.15,
|
BlockRate: 0.15,
|
||||||
Ability: &MonsterAbility{Name: "Frightful Presence", Phase: "opening", ProcChance: 0.40, Effect: "stun"},
|
Ability: &MonsterAbility{Name: "Frightful Presence", Phase: "opening", ProcChance: 0.40, Effect: "stun"},
|
||||||
XPValue: 62000,
|
XPValue: 62000,
|
||||||
Notes: "Dragon's Lair boss. Ancient Red Dragon. Legendary Actions; Lair Actions; phase 2 below 50% HP — fire ignores resistance.",
|
Notes: "Dragon's Lair boss. Ancient Red Dragon. Legendary Actions; Lair Actions; phase 2 below 50% HP — fire ignores resistance.",
|
||||||
FireAttacker: true,
|
FireAttacker: true,
|
||||||
},
|
},
|
||||||
// ── Abyss Portal ─────────────────────────────────────────────────
|
// ── Abyss Portal ─────────────────────────────────────────────────
|
||||||
@@ -670,10 +670,10 @@ var _ = func() bool {
|
|||||||
// 360/AC20/45 over-buffed → 19-40% leaders; this is the Pass 2
|
// 360/AC20/45 over-buffed → 19-40% leaders; this is the Pass 2
|
||||||
// correction back down toward band.)
|
// correction back down toward band.)
|
||||||
CR: 19, HP: 300, AC: 20, Attack: 31, AttackBonus: 11, Speed: 14,
|
CR: 19, HP: 300, AC: 20, Attack: 31, AttackBonus: 11, Speed: 14,
|
||||||
BlockRate: 0.20,
|
BlockRate: 0.20,
|
||||||
Ability: &MonsterAbility{Name: "Lightning Discharge", Phase: "decisive", ProcChance: 0.40, Effect: "aoe"},
|
Ability: &MonsterAbility{Name: "Lightning Discharge", Phase: "decisive", ProcChance: 0.40, Effect: "aoe"},
|
||||||
XPValue: 22000,
|
XPValue: 22000,
|
||||||
Notes: "Abyss Portal boss. Balor. Fire Aura; Death Throes on death; Demonic Resilience; Legendary Resistance 3/combat; phase 2 below 40% HP — Huge size, advantage on attacks.",
|
Notes: "Abyss Portal boss. Balor. Fire Aura; Death Throes on death; Demonic Resilience; Legendary Resistance 3/combat; phase 2 below 40% HP — Huge size, advantage on attacks.",
|
||||||
FireAttacker: true,
|
FireAttacker: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -700,13 +700,13 @@ func dndBestiaryByCR(maxCR float32) []DnDMonsterTemplate {
|
|||||||
// named monsters in the existing combat pipeline.
|
// named monsters in the existing combat pipeline.
|
||||||
func (m DnDMonsterTemplate) toCombatStats() (CombatStats, CombatModifiers) {
|
func (m DnDMonsterTemplate) toCombatStats() (CombatStats, CombatModifiers) {
|
||||||
stats := CombatStats{
|
stats := CombatStats{
|
||||||
MaxHP: m.HP,
|
MaxHP: m.HP,
|
||||||
Attack: m.Attack,
|
Attack: m.Attack,
|
||||||
Defense: 0,
|
Defense: 0,
|
||||||
Speed: m.Speed,
|
Speed: m.Speed,
|
||||||
BlockRate: m.BlockRate,
|
BlockRate: m.BlockRate,
|
||||||
AC: m.AC,
|
AC: m.AC,
|
||||||
AttackBonus: m.AttackBonus,
|
AttackBonus: m.AttackBonus,
|
||||||
FireAttacker: m.FireAttacker,
|
FireAttacker: m.FireAttacker,
|
||||||
}
|
}
|
||||||
mods := CombatModifiers{DamageReduct: 1.0}
|
mods := CombatModifiers{DamageReduct: 1.0}
|
||||||
|
|||||||
@@ -90,9 +90,9 @@ func consumeFiredHealingItems(userID id.UserID, fired int) {
|
|||||||
}
|
}
|
||||||
// Filter to heal items, sort cheapest-tier-first.
|
// Filter to heal items, sort cheapest-tier-first.
|
||||||
type entry struct {
|
type entry struct {
|
||||||
id int64
|
id int64
|
||||||
tier int
|
tier int
|
||||||
name string
|
name string
|
||||||
}
|
}
|
||||||
var heals []entry
|
var heals []entry
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
|
|||||||
@@ -36,17 +36,17 @@ var dndClassWeaponBonus = map[DnDClass]int{
|
|||||||
// roughly 70-80% at low tiers and 50-60% at high tiers. Adjust after live
|
// roughly 70-80% at low tiers and 50-60% at high tiers. Adjust after live
|
||||||
// data lands.
|
// data lands.
|
||||||
const (
|
const (
|
||||||
dndArenaACBase = 10
|
dndArenaACBase = 10
|
||||||
dndArenaACPerThreat = 0.25 // ThreatLevel 0..30 → AC bump 0..7
|
dndArenaACPerThreat = 0.25 // ThreatLevel 0..30 → AC bump 0..7
|
||||||
dndDungeonACBase = 9
|
dndDungeonACBase = 9
|
||||||
// Dungeon AC scales linearly with tier: T1=10, T5=14
|
// Dungeon AC scales linearly with tier: T1=10, T5=14
|
||||||
)
|
)
|
||||||
|
|
||||||
// Monster attack bonus. Counters player AC growth. T1 monster +5, T5 +9.
|
// Monster attack bonus. Counters player AC growth. T1 monster +5, T5 +9.
|
||||||
const (
|
const (
|
||||||
dndArenaAtkBase = 4
|
dndArenaAtkBase = 4
|
||||||
dndArenaAtkPerThreat = 0.30
|
dndArenaAtkPerThreat = 0.30
|
||||||
dndDungeonAtkBase = 4
|
dndDungeonAtkBase = 4
|
||||||
// T1 dungeon +5, T5 +9
|
// T1 dungeon +5, T5 +9
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -170,8 +170,8 @@ func applyDnDEquipmentLayer(stats *CombatStats, c *DnDCharacter, equip map[Equip
|
|||||||
// buildHarnessPlayer (expedition / class-balance harnesses) so the
|
// buildHarnessPlayer (expedition / class-balance harnesses) so the
|
||||||
// harness's measurement matches what live players experience.
|
// harness's measurement matches what live players experience.
|
||||||
const (
|
const (
|
||||||
phase5BACBonus = 3
|
phase5BACBonus = 3
|
||||||
phase5BAttackBonus = 3
|
phase5BAttackBonus = 3
|
||||||
phase5BWeaponMagicBonus = 3
|
phase5BWeaponMagicBonus = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ func autoBuildCharacter(userID id.UserID, char *AdventureCharacter) *DnDCharacte
|
|||||||
Class: sug.Class,
|
Class: sug.Class,
|
||||||
Level: level,
|
Level: level,
|
||||||
STR: scores[0], DEX: scores[1], CON: scores[2],
|
STR: scores[0], DEX: scores[1], CON: scores[2],
|
||||||
INT: scores[3], WIS: scores[4], CHA: scores[5],
|
INT: scores[3], WIS: scores[4], CHA: scores[5],
|
||||||
PendingSetup: false,
|
PendingSetup: false,
|
||||||
AutoMigrated: true,
|
AutoMigrated: true,
|
||||||
CreatedAt: time.Now().UTC(),
|
CreatedAt: time.Now().UTC(),
|
||||||
|
|||||||
@@ -44,18 +44,18 @@ const (
|
|||||||
// WeaponProfile mirrors the spec's struct. DamageDie is parsed from "1d8",
|
// WeaponProfile mirrors the spec's struct. DamageDie is parsed from "1d8",
|
||||||
// "2d6", "1d10" etc. into Count/Sides on construction.
|
// "2d6", "1d10" etc. into Count/Sides on construction.
|
||||||
type WeaponProfile struct {
|
type WeaponProfile struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Category WeaponCategory
|
Category WeaponCategory
|
||||||
DamageCount int // dice count
|
DamageCount int // dice count
|
||||||
DamageSides int // die sides
|
DamageSides int // die sides
|
||||||
DamageType string // slashing, piercing, bludgeoning
|
DamageType string // slashing, piercing, bludgeoning
|
||||||
Properties []WeaponProperty
|
Properties []WeaponProperty
|
||||||
VersaCount int // versatile two-handed dice (0 if not versatile)
|
VersaCount int // versatile two-handed dice (0 if not versatile)
|
||||||
VersaSides int
|
VersaSides int
|
||||||
MagicBonus int // 0 for mundane
|
MagicBonus int // 0 for mundane
|
||||||
MagicProp string // empty for mundane
|
MagicProp string // empty for mundane
|
||||||
NamedItem bool // true for §7.2 named magic weapons
|
NamedItem bool // true for §7.2 named magic weapons
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasProperty reports whether the weapon has the given property.
|
// HasProperty reports whether the weapon has the given property.
|
||||||
@@ -137,16 +137,17 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ArmorProfile is the spec's struct. MaxDEXBonus convention:
|
// ArmorProfile is the spec's struct. MaxDEXBonus convention:
|
||||||
// -1 = unlimited (light armor takes full DEX mod)
|
//
|
||||||
// 2 = medium (cap at +2)
|
// -1 = unlimited (light armor takes full DEX mod)
|
||||||
// 0 = heavy (no DEX bonus)
|
// 2 = medium (cap at +2)
|
||||||
// 0 for shields (shields don't add DEX)
|
// 0 = heavy (no DEX bonus)
|
||||||
|
// 0 for shields (shields don't add DEX)
|
||||||
type ArmorProfile struct {
|
type ArmorProfile struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Type ArmorType
|
Type ArmorType
|
||||||
BaseAC int // 0 for shields (shields use ShieldBonus)
|
BaseAC int // 0 for shields (shields use ShieldBonus)
|
||||||
ShieldBonus int // +2 base for shields, 0 for armor
|
ShieldBonus int // +2 base for shields, 0 for armor
|
||||||
MaxDEXBonus int
|
MaxDEXBonus int
|
||||||
STRRequire int
|
STRRequire int
|
||||||
StealthDisad bool
|
StealthDisad bool
|
||||||
|
|||||||
@@ -8,17 +8,17 @@ import (
|
|||||||
|
|
||||||
func TestParseDamageDie(t *testing.T) {
|
func TestParseDamageDie(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
s string
|
s string
|
||||||
count, sides int
|
count, sides int
|
||||||
ok bool
|
ok bool
|
||||||
}{
|
}{
|
||||||
{"1d8", 1, 8, true},
|
{"1d8", 1, 8, true},
|
||||||
{"2d6", 2, 6, true},
|
{"2d6", 2, 6, true},
|
||||||
{"1D10", 1, 10, true},
|
{"1D10", 1, 10, true},
|
||||||
{" 1d12 ", 1, 12, true},
|
{" 1d12 ", 1, 12, true},
|
||||||
{"d8", 0, 0, false}, // no count
|
{"d8", 0, 0, false}, // no count
|
||||||
{"1d", 0, 0, false}, // no sides
|
{"1d", 0, 0, false}, // no sides
|
||||||
{"1d1", 0, 0, false}, // sides < 2
|
{"1d1", 0, 0, false}, // sides < 2
|
||||||
{"banana", 0, 0, false},
|
{"banana", 0, 0, false},
|
||||||
{"", 0, 0, false},
|
{"", 0, 0, false},
|
||||||
}
|
}
|
||||||
@@ -359,16 +359,16 @@ func TestSynthesizeWeaponProfile_NonWeaponSlotReturnsNil(t *testing.T) {
|
|||||||
|
|
||||||
func TestSynthesizeArmorProfile_TierMapping(t *testing.T) {
|
func TestSynthesizeArmorProfile_TierMapping(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
tier int
|
tier int
|
||||||
wantBaseAC int
|
wantBaseAC int
|
||||||
wantType ArmorType
|
wantType ArmorType
|
||||||
}{
|
}{
|
||||||
{1, 11, ArmorTypeLight}, // Padded
|
{1, 11, ArmorTypeLight}, // Padded
|
||||||
{2, 11, ArmorTypeLight}, // Leather
|
{2, 11, ArmorTypeLight}, // Leather
|
||||||
{3, 13, ArmorTypeMedium}, // Chain Shirt
|
{3, 13, ArmorTypeMedium}, // Chain Shirt
|
||||||
{4, 14, ArmorTypeMedium}, // Scale Mail
|
{4, 14, ArmorTypeMedium}, // Scale Mail
|
||||||
{5, 15, ArmorTypeMedium}, // Half Plate
|
{5, 15, ArmorTypeMedium}, // Half Plate
|
||||||
{6, 18, ArmorTypeHeavy}, // Plate
|
{6, 18, ArmorTypeHeavy}, // Plate
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
eq := &AdvEquipment{Slot: SlotArmor, Tier: c.tier, Name: "Armor"}
|
eq := &AdvEquipment{Slot: SlotArmor, Tier: c.tier, Name: "Armor"}
|
||||||
@@ -416,8 +416,8 @@ func TestPickWeaponAbilityMod(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{"wpn_greatsword", 3}, // melee non-finesse → STR
|
{"wpn_greatsword", 3}, // melee non-finesse → STR
|
||||||
{"wpn_longsword", 3},
|
{"wpn_longsword", 3},
|
||||||
{"wpn_dagger", 3}, // finesse melee, but STR > DEX, picks STR
|
{"wpn_dagger", 3}, // finesse melee, but STR > DEX, picks STR
|
||||||
{"wpn_longbow", 2}, // ranged → DEX
|
{"wpn_longbow", 2}, // ranged → DEX
|
||||||
{"wpn_shortbow", 2},
|
{"wpn_shortbow", 2},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
|
|||||||
@@ -535,4 +535,3 @@ func nullableString(s string) any {
|
|||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ import (
|
|||||||
|
|
||||||
func TestResolveCombatInterrupt_Brackets(t *testing.T) {
|
func TestResolveCombatInterrupt_Brackets(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
roll int
|
roll int
|
||||||
threat int
|
threat int
|
||||||
tier int
|
tier int
|
||||||
class DnDClass
|
class DnDClass
|
||||||
zone ZoneID
|
zone ZoneID
|
||||||
want CombatInterruptKind
|
want CombatInterruptKind
|
||||||
}{
|
}{
|
||||||
{"none", 5, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptNone}, // 5+1=6
|
{"none", 5, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptNone}, // 5+1=6
|
||||||
{"noise lower", 9, 0, 0, ClassFighter, ZoneGoblinWarrens, InterruptNoise},
|
{"noise lower", 9, 0, 0, ClassFighter, ZoneGoblinWarrens, InterruptNoise},
|
||||||
{"noise upper", 13, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptNoise}, // 13+1=14
|
{"noise upper", 13, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptNoise}, // 13+1=14
|
||||||
{"standard", 14, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 15
|
{"standard", 14, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 15
|
||||||
{"standard upper", 17, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 18
|
{"standard upper", 17, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 18
|
||||||
// Phase 5-B: elite bracket moved from 19+ to 23+. At totals
|
// Phase 5-B: elite bracket moved from 19+ to 23+. At totals
|
||||||
// 19-21 (formerly Elite) the roll now resolves to Standard;
|
// 19-21 (formerly Elite) the roll now resolves to Standard;
|
||||||
@@ -27,11 +27,11 @@ func TestResolveCombatInterrupt_Brackets(t *testing.T) {
|
|||||||
// via threat-mod, since base d20+tier+ranger maxes at ~21).
|
// via threat-mod, since base d20+tier+ranger maxes at ~21).
|
||||||
{"standard top of band", 18, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 19 → Standard now
|
{"standard top of band", 18, 0, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 19 → Standard now
|
||||||
{"standard band upper", 18, 0, 3, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 21 → Standard now
|
{"standard band upper", 18, 0, 3, ClassFighter, ZoneGoblinWarrens, InterruptStandard}, // 21 → Standard now
|
||||||
{"patrol", 20, 0, 2, ClassFighter, ZoneGoblinWarrens, InterruptPatrol}, // 22
|
{"patrol", 20, 0, 2, ClassFighter, ZoneGoblinWarrens, InterruptPatrol}, // 22
|
||||||
{"elite high threat", 18, 80, 3, ClassFighter, ZoneGoblinWarrens, InterruptElite}, // 18+3+2=23 → Elite
|
{"elite high threat", 18, 80, 3, ClassFighter, ZoneGoblinWarrens, InterruptElite}, // 18+3+2=23 → Elite
|
||||||
{"threat bumps band", 13, 60, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard},
|
{"threat bumps band", 13, 60, 1, ClassFighter, ZoneGoblinWarrens, InterruptStandard},
|
||||||
{"ranger wilderness drops band", 17, 0, 1, ClassRanger, ZoneForestShadows, InterruptStandard}, // 17+1-3=15
|
{"ranger wilderness drops band", 17, 0, 1, ClassRanger, ZoneForestShadows, InterruptStandard}, // 17+1-3=15
|
||||||
{"ranger non-wild no drop", 17, 0, 1, ClassRanger, ZoneGoblinWarrens, InterruptStandard}, // 17+1=18 (Standard)
|
{"ranger non-wild no drop", 17, 0, 1, ClassRanger, ZoneGoblinWarrens, InterruptStandard}, // 17+1=18 (Standard)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
@@ -59,14 +59,14 @@ func TestResolveCombatInterrupt_ThreatModifier(t *testing.T) {
|
|||||||
|
|
||||||
func TestMonsterKillTags_GatesKnownMonsters(t *testing.T) {
|
func TestMonsterKillTags_GatesKnownMonsters(t *testing.T) {
|
||||||
cases := map[string][]string{
|
cases := map[string][]string{
|
||||||
"worg": {"worg", "beast"},
|
"worg": {"worg", "beast"},
|
||||||
"owlbear": {"owlbear", "beast"},
|
"owlbear": {"owlbear", "beast"},
|
||||||
"dire_wolf": {"beast"},
|
"dire_wolf": {"beast"},
|
||||||
"vampire_spawn": {"vampire_spawn"},
|
"vampire_spawn": {"vampire_spawn"},
|
||||||
"young_red_dragon": {"drake"},
|
"young_red_dragon": {"drake"},
|
||||||
"boss_belaxath": {"balor", "demon", "portal_boss"},
|
"boss_belaxath": {"balor", "demon", "portal_boss"},
|
||||||
"boss_thornmother": {"thornmother"},
|
"boss_thornmother": {"thornmother"},
|
||||||
"goblin_sneak": nil, // goblins gate nothing in §3
|
"goblin_sneak": nil, // goblins gate nothing in §3
|
||||||
}
|
}
|
||||||
for id, want := range cases {
|
for id, want := range cases {
|
||||||
got := monsterKillTags(id)
|
got := monsterKillTags(id)
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ func TestSaveHarvestNodes_DropsLegacyRoomKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func TestResolveHarvestOutcome_Brackets(t *testing.T) {
|
func TestResolveHarvestOutcome_Brackets(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
roll, dc int
|
roll, dc int
|
||||||
|
|||||||
@@ -56,11 +56,11 @@ func TestRenderRegionChain_CurrentMarker(t *testing.T) {
|
|||||||
|
|
||||||
func TestRegionGlyph(t *testing.T) {
|
func TestRegionGlyph(t *testing.T) {
|
||||||
cases := map[string]string{
|
cases := map[string]string{
|
||||||
"Surface Tunnels": "ST",
|
"Surface Tunnels": "ST",
|
||||||
"The Deep Throne": "TDT",
|
"The Deep Throne": "TDT",
|
||||||
"Drow Outpost": "DO",
|
"Drow Outpost": "DO",
|
||||||
"Illithid Warren": "IW",
|
"Illithid Warren": "IW",
|
||||||
"Kobold Warrens": "KW",
|
"Kobold Warrens": "KW",
|
||||||
}
|
}
|
||||||
for name, want := range cases {
|
for name, want := range cases {
|
||||||
if got := regionGlyph(name); got != want {
|
if got := regionGlyph(name); got != want {
|
||||||
|
|||||||
@@ -38,9 +38,9 @@ func TestWanderingOutcome_BucketBoundaries(t *testing.T) {
|
|||||||
|
|
||||||
func TestResolveWanderingCheck_RoughCampBumpsRoll(t *testing.T) {
|
func TestResolveWanderingCheck_RoughCampBumpsRoll(t *testing.T) {
|
||||||
exp := &Expedition{
|
exp := &Expedition{
|
||||||
ZoneID: ZoneGoblinWarrens,
|
ZoneID: ZoneGoblinWarrens,
|
||||||
ThreatLevel: 0,
|
ThreatLevel: 0,
|
||||||
Camp: &CampState{Active: true, Type: CampTypeRough},
|
Camp: &CampState{Active: true, Type: CampTypeRough},
|
||||||
}
|
}
|
||||||
// roll 8 → +3 rough → total 11 → Minor (would have been Passage at 8).
|
// roll 8 → +3 rough → total 11 → Minor (would have been Passage at 8).
|
||||||
nc := resolveWanderingCheck(exp, ClassFighter, func() int { return 8 })
|
nc := resolveWanderingCheck(exp, ClassFighter, func() int { return 8 })
|
||||||
@@ -54,9 +54,9 @@ func TestResolveWanderingCheck_RoughCampBumpsRoll(t *testing.T) {
|
|||||||
|
|
||||||
func TestResolveWanderingCheck_FortifiedCampReducesRoll(t *testing.T) {
|
func TestResolveWanderingCheck_FortifiedCampReducesRoll(t *testing.T) {
|
||||||
exp := &Expedition{
|
exp := &Expedition{
|
||||||
ZoneID: ZoneGoblinWarrens,
|
ZoneID: ZoneGoblinWarrens,
|
||||||
ThreatLevel: 0,
|
ThreatLevel: 0,
|
||||||
Camp: &CampState{Active: true, Type: CampTypeFortified},
|
Camp: &CampState{Active: true, Type: CampTypeFortified},
|
||||||
}
|
}
|
||||||
// roll 14 → -4 fortified → total 10 → Passage.
|
// roll 14 → -4 fortified → total 10 → Passage.
|
||||||
nc := resolveWanderingCheck(exp, ClassFighter, func() int { return 14 })
|
nc := resolveWanderingCheck(exp, ClassFighter, func() int { return 14 })
|
||||||
@@ -83,9 +83,9 @@ func TestResolveWanderingCheck_ThreatModAbove30(t *testing.T) {
|
|||||||
|
|
||||||
func TestResolveWanderingCheck_RangerWildernessDiscount(t *testing.T) {
|
func TestResolveWanderingCheck_RangerWildernessDiscount(t *testing.T) {
|
||||||
exp := &Expedition{
|
exp := &Expedition{
|
||||||
ZoneID: ZoneForestShadows,
|
ZoneID: ZoneForestShadows,
|
||||||
ThreatLevel: 0,
|
ThreatLevel: 0,
|
||||||
Camp: &CampState{Active: true, Type: CampTypeStandard},
|
Camp: &CampState{Active: true, Type: CampTypeStandard},
|
||||||
}
|
}
|
||||||
nc := resolveWanderingCheck(exp, ClassRanger, func() int { return 12 })
|
nc := resolveWanderingCheck(exp, ClassRanger, func() int { return 12 })
|
||||||
if nc.ClassMod != -2 {
|
if nc.ClassMod != -2 {
|
||||||
@@ -124,7 +124,7 @@ func TestProcessNightCheck_LogsAndBumpsThreatOnPassage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
nc := NightCheck{
|
nc := NightCheck{
|
||||||
Outcome: NightOutcomePassage, Roll: 8, Total: 8,
|
Outcome: NightOutcomePassage, Roll: 8, Total: 8,
|
||||||
Summary: "Signs of passage near camp; no encounter.",
|
Summary: "Signs of passage near camp; no encounter.",
|
||||||
ThreatBumped: true,
|
ThreatBumped: true,
|
||||||
}
|
}
|
||||||
if err := processNightCheck(exp, nc); err != nil {
|
if err := processNightCheck(exp, nc); err != nil {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ var regionsByZone = map[ZoneID][]ExpeditionRegion{
|
|||||||
{
|
{
|
||||||
ID: "underdark_deep_throne", ZoneID: ZoneUnderdark, Order: 4,
|
ID: "underdark_deep_throne", ZoneID: ZoneUnderdark, Order: 4,
|
||||||
Name: "The Deep Throne", BaseCampSite: false,
|
Name: "The Deep Throne", BaseCampSite: false,
|
||||||
RegionBoss: "Ilvaras Xunyl (Zone Boss)", IsZoneBoss: true,
|
RegionBoss: "Ilvaras Xunyl (Zone Boss)", IsZoneBoss: true,
|
||||||
EnemySubset: []string{"drow", "drow_elite_warrior", "drow_mage"},
|
EnemySubset: []string{"drow", "drow_elite_warrior", "drow_mage"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -87,7 +87,7 @@ var regionsByZone = map[ZoneID][]ExpeditionRegion{
|
|||||||
{
|
{
|
||||||
ID: "dragons_lair_infernax_chamber", ZoneID: ZoneDragonsLair, Order: 4,
|
ID: "dragons_lair_infernax_chamber", ZoneID: ZoneDragonsLair, Order: 4,
|
||||||
Name: "Infernax's Chamber", BaseCampSite: false,
|
Name: "Infernax's Chamber", BaseCampSite: false,
|
||||||
RegionBoss: "Infernax (Zone Boss)", IsZoneBoss: true,
|
RegionBoss: "Infernax (Zone Boss)", IsZoneBoss: true,
|
||||||
EnemySubset: []string{"young_red_dragon"},
|
EnemySubset: []string{"young_red_dragon"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -113,7 +113,7 @@ var regionsByZone = map[ZoneID][]ExpeditionRegion{
|
|||||||
{
|
{
|
||||||
ID: "abyss_the_tear", ZoneID: ZoneAbyssPortal, Order: 4,
|
ID: "abyss_the_tear", ZoneID: ZoneAbyssPortal, Order: 4,
|
||||||
Name: "The Tear", BaseCampSite: false,
|
Name: "The Tear", BaseCampSite: false,
|
||||||
RegionBoss: "Belaxath (Zone Boss)", IsZoneBoss: true,
|
RegionBoss: "Belaxath (Zone Boss)", IsZoneBoss: true,
|
||||||
EnemySubset: []string{"marilith"},
|
EnemySubset: []string{"marilith"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ func addSupplyPurchase(s ExpeditionSupplies, p SupplyPurchase) ExpeditionSupplie
|
|||||||
// - siege overrides everything with a hard 2× floor (even for tier 1
|
// - siege overrides everything with a hard 2× floor (even for tier 1
|
||||||
// where HarshMod is 1×) — the dungeon is actively starving you out.
|
// where HarshMod is 1×) — the dungeon is actively starving you out.
|
||||||
// - otherwise, harshActive applies HarshMod (zone-tier scaled).
|
// - otherwise, harshActive applies HarshMod (zone-tier scaled).
|
||||||
|
//
|
||||||
// phase5BDailyBurnRatePct is the shipped daily-burn multiplier from
|
// phase5BDailyBurnRatePct is the shipped daily-burn multiplier from
|
||||||
// Phase 3-B's sweep + Phase 5-B's post-buff re-validation. 50 means
|
// Phase 3-B's sweep + Phase 5-B's post-buff re-validation. 50 means
|
||||||
// "half live burn" — needed because the Phase 5-B player power floor
|
// "half live burn" — needed because the Phase 5-B player power floor
|
||||||
|
|||||||
@@ -44,11 +44,11 @@ func TestSupplyDepletion_Brackets(t *testing.T) {
|
|||||||
max float32
|
max float32
|
||||||
want SupplyDepletionState
|
want SupplyDepletionState
|
||||||
}{
|
}{
|
||||||
{10, 10, SupplyNormal}, // 100%
|
{10, 10, SupplyNormal}, // 100%
|
||||||
{3, 10, SupplyNormal}, // 30%
|
{3, 10, SupplyNormal}, // 30%
|
||||||
{2.5, 10, SupplyNormal}, // 25% — boundary stays normal
|
{2.5, 10, SupplyNormal}, // 25% — boundary stays normal
|
||||||
{2, 10, SupplyRationing}, // 20%
|
{2, 10, SupplyRationing}, // 20%
|
||||||
{1, 10, SupplyRationing}, // 10% — boundary stays rationing
|
{1, 10, SupplyRationing}, // 10% — boundary stays rationing
|
||||||
{0.9, 10, SupplySevereRationing},
|
{0.9, 10, SupplySevereRationing},
|
||||||
{0.1, 10, SupplySevereRationing},
|
{0.1, 10, SupplySevereRationing},
|
||||||
{0, 10, SupplyStarvation},
|
{0, 10, SupplyStarvation},
|
||||||
|
|||||||
@@ -455,12 +455,13 @@ const AbyssInstabilityPerDay = 5
|
|||||||
const AbyssMaxInstability = 100
|
const AbyssMaxInstability = 100
|
||||||
|
|
||||||
// AbyssInstabilityBandFor classifies the §7.6 instability tiers.
|
// AbyssInstabilityBandFor classifies the §7.6 instability tiers.
|
||||||
// normal: 0–20
|
//
|
||||||
// mild: 21–40 (-1 WIS)
|
// normal: 0–20
|
||||||
// warp: 41–60 (rooms shift order)
|
// mild: 21–40 (-1 WIS)
|
||||||
// surges: 61–80 (12h wandering checks)
|
// warp: 41–60 (rooms shift order)
|
||||||
// unravel: 81–99 (supply ×2, -2 all rolls)
|
// surges: 61–80 (12h wandering checks)
|
||||||
// collapse: 100
|
// unravel: 81–99 (supply ×2, -2 all rolls)
|
||||||
|
// collapse: 100
|
||||||
func AbyssInstabilityBandFor(stack int) string {
|
func AbyssInstabilityBandFor(stack int) string {
|
||||||
switch {
|
switch {
|
||||||
case stack <= 20:
|
case stack <= 20:
|
||||||
|
|||||||
@@ -366,8 +366,8 @@ func TestUnderforge_FortifiedRestReducesHeat(t *testing.T) {
|
|||||||
defer cleanupExpeditions(uid)
|
defer cleanupExpeditions(uid)
|
||||||
|
|
||||||
exp := &Expedition{
|
exp := &Expedition{
|
||||||
ID: "fake-uf",
|
ID: "fake-uf",
|
||||||
ZoneID: ZoneUnderforge,
|
ZoneID: ZoneUnderforge,
|
||||||
TemporalStack: 5,
|
TemporalStack: 5,
|
||||||
}
|
}
|
||||||
// Insert minimal row so updateTemporalStack works.
|
// Insert minimal row so updateTemporalStack works.
|
||||||
|
|||||||
@@ -39,14 +39,14 @@ const (
|
|||||||
// currentBurn() (already wired); ShortRest disabled is checked by the
|
// currentBurn() (already wired); ShortRest disabled is checked by the
|
||||||
// rest plumbing once expedition combat is wired.
|
// rest plumbing once expedition combat is wired.
|
||||||
type ThreatBandInfo struct {
|
type ThreatBandInfo struct {
|
||||||
Band ThreatBand
|
Band ThreatBand
|
||||||
Label string
|
Label string
|
||||||
Perception int // enemy passive perception bonus
|
Perception int // enemy passive perception bonus
|
||||||
AC int // enemy AC bonus
|
AC int // enemy AC bonus
|
||||||
InitAdv bool // enemy initiative advantage
|
InitAdv bool // enemy initiative advantage
|
||||||
TrapsArm bool // re-arm cleared traps each day
|
TrapsArm bool // re-arm cleared traps each day
|
||||||
SupplyMult float32 // multiplier on daily supply burn
|
SupplyMult float32 // multiplier on daily supply burn
|
||||||
NoShortRest bool // siege-mode only
|
NoShortRest bool // siege-mode only
|
||||||
}
|
}
|
||||||
|
|
||||||
// threatBandFor returns the band for a given level, honouring siege override.
|
// threatBandFor returns the band for a given level, honouring siege override.
|
||||||
|
|||||||
@@ -11,25 +11,25 @@ import (
|
|||||||
// flavor files that would silently produce empty narrative.
|
// flavor files that would silently produce empty narrative.
|
||||||
func TestDnDFlavorPoolsNonEmpty(t *testing.T) {
|
func TestDnDFlavorPoolsNonEmpty(t *testing.T) {
|
||||||
pools := map[string][]string{
|
pools := map[string][]string{
|
||||||
"CombatStart": flavor.CombatStart,
|
"CombatStart": flavor.CombatStart,
|
||||||
"CombatVictory": flavor.CombatVictory,
|
"CombatVictory": flavor.CombatVictory,
|
||||||
"BossDeath": flavor.BossDeath,
|
"BossDeath": flavor.BossDeath,
|
||||||
"PlayerDeath": flavor.PlayerDeath,
|
"PlayerDeath": flavor.PlayerDeath,
|
||||||
"ZoneComplete": flavor.ZoneComplete,
|
"ZoneComplete": flavor.ZoneComplete,
|
||||||
"Nat20": flavor.Nat20,
|
"Nat20": flavor.Nat20,
|
||||||
"Nat1": flavor.Nat1,
|
"Nat1": flavor.Nat1,
|
||||||
"LevelUp": flavor.LevelUp,
|
"LevelUp": flavor.LevelUp,
|
||||||
"ItemFound": flavor.ItemFound,
|
"ItemFound": flavor.ItemFound,
|
||||||
"RestShort": flavor.RestShort,
|
"RestShort": flavor.RestShort,
|
||||||
"RestLong": flavor.RestLong,
|
"RestLong": flavor.RestLong,
|
||||||
"HomeLongRest": flavor.HomeLongRest,
|
"HomeLongRest": flavor.HomeLongRest,
|
||||||
"MistyGreeting": flavor.MistyGreeting,
|
"MistyGreeting": flavor.MistyGreeting,
|
||||||
"MistyInsightSuccess": flavor.MistyInsightSuccess,
|
"MistyInsightSuccess": flavor.MistyInsightSuccess,
|
||||||
"MistySkillFail": flavor.MistySkillFail,
|
"MistySkillFail": flavor.MistySkillFail,
|
||||||
"ArinaGreeting": flavor.ArinaGreeting,
|
"ArinaGreeting": flavor.ArinaGreeting,
|
||||||
"ArinaArcanaSuccess": flavor.ArinaArcanaSuccess,
|
"ArinaArcanaSuccess": flavor.ArinaArcanaSuccess,
|
||||||
"ArinaSkillFail": flavor.ArinaSkillFail,
|
"ArinaSkillFail": flavor.ArinaSkillFail,
|
||||||
"ExpeditionStart": flavor.ExpeditionStart,
|
"ExpeditionStart": flavor.ExpeditionStart,
|
||||||
}
|
}
|
||||||
for name, pool := range pools {
|
for name, pool := range pools {
|
||||||
if len(pool) == 0 {
|
if len(pool) == 0 {
|
||||||
@@ -47,18 +47,18 @@ func TestDnDFlavorPoolsNonEmpty(t *testing.T) {
|
|||||||
// string when its underlying pool has entries.
|
// string when its underlying pool has entries.
|
||||||
func TestDnDFlavorHelpersReturnNonEmpty(t *testing.T) {
|
func TestDnDFlavorHelpersReturnNonEmpty(t *testing.T) {
|
||||||
cases := map[string]func() string{
|
cases := map[string]func() string{
|
||||||
"dndCombatOpeningLine": dndCombatOpeningLine,
|
"dndCombatOpeningLine": dndCombatOpeningLine,
|
||||||
"dndZoneCompleteLine": dndZoneCompleteLine,
|
"dndZoneCompleteLine": dndZoneCompleteLine,
|
||||||
"dndNat20Line": dndNat20Line,
|
"dndNat20Line": dndNat20Line,
|
||||||
"dndNat1Line": dndNat1Line,
|
"dndNat1Line": dndNat1Line,
|
||||||
"dndLevelUpFlavorLine": dndLevelUpFlavorLine,
|
"dndLevelUpFlavorLine": dndLevelUpFlavorLine,
|
||||||
"dndItemFoundLine": dndItemFoundLine,
|
"dndItemFoundLine": dndItemFoundLine,
|
||||||
"dndRestShortFlavorLine": dndRestShortFlavorLine,
|
"dndRestShortFlavorLine": dndRestShortFlavorLine,
|
||||||
"dndCombatClosingLine_Win": func() string { return dndCombatClosingLine(true, false) },
|
"dndCombatClosingLine_Win": func() string { return dndCombatClosingLine(true, false) },
|
||||||
"dndCombatClosingLine_Boss": func() string { return dndCombatClosingLine(true, true) },
|
"dndCombatClosingLine_Boss": func() string { return dndCombatClosingLine(true, true) },
|
||||||
"dndCombatClosingLine_Death": func() string { return dndCombatClosingLine(false, false) },
|
"dndCombatClosingLine_Death": func() string { return dndCombatClosingLine(false, false) },
|
||||||
"dndRestLongFlavorLine_Home": func() string { return dndRestLongFlavorLine(true) },
|
"dndRestLongFlavorLine_Home": func() string { return dndRestLongFlavorLine(true) },
|
||||||
"dndRestLongFlavorLine_Inn": func() string { return dndRestLongFlavorLine(false) },
|
"dndRestLongFlavorLine_Inn": func() string { return dndRestLongFlavorLine(false) },
|
||||||
}
|
}
|
||||||
for name, fn := range cases {
|
for name, fn := range cases {
|
||||||
// Run several times — flavor pulls a random pool member, so a single
|
// Run several times — flavor pulls a random pool member, so a single
|
||||||
|
|||||||
@@ -102,9 +102,10 @@ func applyRacePassives(stats *CombatStats, mods *CombatModifiers, c *DnDCharacte
|
|||||||
// BEFORE consumable application (so consumables can stack on top).
|
// BEFORE consumable application (so consumables can stack on top).
|
||||||
//
|
//
|
||||||
// Some passives ride on existing CombatModifiers fields:
|
// Some passives ride on existing CombatModifiers fields:
|
||||||
// Rogue's Sneak Attack reuses AutoCritFirst (the consumable Crystal Berry
|
//
|
||||||
// field) — the engine already implements first-hit-auto-crit semantics.
|
// Rogue's Sneak Attack reuses AutoCritFirst (the consumable Crystal Berry
|
||||||
// Cleric's Divine Favor reuses HealItem, the under-50%-HP heal trigger.
|
// field) — the engine already implements first-hit-auto-crit semantics.
|
||||||
|
// Cleric's Divine Favor reuses HealItem, the under-50%-HP heal trigger.
|
||||||
//
|
//
|
||||||
// This means:
|
// This means:
|
||||||
// - A Rogue carrying a Crystal Berry doesn't get *two* auto-crits;
|
// - A Rogue carrying a Crystal Berry doesn't get *two* auto-crits;
|
||||||
|
|||||||
@@ -112,9 +112,9 @@ func TestDnDRollSummaryLine_HighestNonNat20(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseDice(t *testing.T) {
|
func TestParseDice(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
in string
|
in string
|
||||||
count, sides, mod int
|
count, sides, mod int
|
||||||
ok bool
|
ok bool
|
||||||
}{
|
}{
|
||||||
{"d20", 1, 20, 0, true},
|
{"d20", 1, 20, 0, true},
|
||||||
{"1d20", 1, 20, 0, true},
|
{"1d20", 1, 20, 0, true},
|
||||||
|
|||||||
@@ -105,13 +105,13 @@ func (p *AdventurePlugin) handleDnDSetupCmd(ctx MessageContext, args string) err
|
|||||||
case lower == "cancel":
|
case lower == "cancel":
|
||||||
return p.dndSetupCancel(ctx)
|
return p.dndSetupCancel(ctx)
|
||||||
}
|
}
|
||||||
return p.SendDM(ctx.Sender,"Unknown !setup subcommand. Try !setup with no args for instructions.")
|
return p.SendDM(ctx.Sender, "Unknown !setup subcommand. Try !setup with no args for instructions.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AdventurePlugin) dndSetupStatus(ctx MessageContext) error {
|
func (p *AdventurePlugin) dndSetupStatus(ctx MessageContext) error {
|
||||||
c, err := LoadDnDCharacter(ctx.Sender)
|
c, err := LoadDnDCharacter(ctx.Sender)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p.SendDM(ctx.Sender,"Couldn't load your Adv 2.0 draft: "+err.Error())
|
return p.SendDM(ctx.Sender, "Couldn't load your Adv 2.0 draft: "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// No row yet → fresh setup. Show suggestion + race menu.
|
// No row yet → fresh setup. Show suggestion + race menu.
|
||||||
@@ -125,7 +125,7 @@ func (p *AdventurePlugin) dndSetupStatus(ctx MessageContext) error {
|
|||||||
b.WriteString("**Step 1 — Race.** Pick one of:\n")
|
b.WriteString("**Step 1 — Race.** Pick one of:\n")
|
||||||
b.WriteString(renderRaceMenu())
|
b.WriteString(renderRaceMenu())
|
||||||
b.WriteString("\nReply: `!setup race <name>` (e.g. `!setup race elf`)\n")
|
b.WriteString("\nReply: `!setup race <name>` (e.g. `!setup race elf`)\n")
|
||||||
return p.SendDM(ctx.Sender,b.String())
|
return p.SendDM(ctx.Sender, b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirmed already → reroute to !sheet, unless this is an auto-migrated
|
// Confirmed already → reroute to !sheet, unless this is an auto-migrated
|
||||||
@@ -152,62 +152,62 @@ func (p *AdventurePlugin) dndSetupStatus(ctx MessageContext) error {
|
|||||||
b.WriteString("**Next: Step 1 — Race.**\n")
|
b.WriteString("**Next: Step 1 — Race.**\n")
|
||||||
b.WriteString(renderRaceMenu())
|
b.WriteString(renderRaceMenu())
|
||||||
b.WriteString("\nReply: `!setup race <name>`\n")
|
b.WriteString("\nReply: `!setup race <name>`\n")
|
||||||
return p.SendDM(ctx.Sender,b.String())
|
return p.SendDM(ctx.Sender, b.String())
|
||||||
}
|
}
|
||||||
b.WriteString(fmt.Sprintf("Race: **%s** ✓\n", titleRace(c.Race)))
|
b.WriteString(fmt.Sprintf("Race: **%s** ✓\n", titleRace(c.Race)))
|
||||||
if c.Class == "" {
|
if c.Class == "" {
|
||||||
b.WriteString("\n**Next: Step 2 — Class.**\n")
|
b.WriteString("\n**Next: Step 2 — Class.**\n")
|
||||||
b.WriteString(renderClassMenu())
|
b.WriteString(renderClassMenu())
|
||||||
b.WriteString("\nReply: `!setup class <name>`\n")
|
b.WriteString("\nReply: `!setup class <name>`\n")
|
||||||
return p.SendDM(ctx.Sender,b.String())
|
return p.SendDM(ctx.Sender, b.String())
|
||||||
}
|
}
|
||||||
b.WriteString(fmt.Sprintf("Class: **%s** ✓\n", titleClass(c.Class)))
|
b.WriteString(fmt.Sprintf("Class: **%s** ✓\n", titleClass(c.Class)))
|
||||||
if !statsAssigned(c) {
|
if !statsAssigned(c) {
|
||||||
b.WriteString("\n**Next: Step 3 — Ability Scores.**\n")
|
b.WriteString("\n**Next: Step 3 — Ability Scores.**\n")
|
||||||
b.WriteString("Assign these six values — **15 14 13 12 10 8** — to STR, DEX, CON, INT, WIS, CHA. Each value used exactly once.\n\n")
|
b.WriteString("Assign these six values — **15 14 13 12 10 8** — to STR, DEX, CON, INT, WIS, CHA. Each value used exactly once.\n\n")
|
||||||
b.WriteString("Reply: `!setup stats 15 14 13 12 10 8` (rearrange to taste).\n")
|
b.WriteString("Reply: `!setup stats 15 14 13 12 10 8` (rearrange to taste).\n")
|
||||||
return p.SendDM(ctx.Sender,b.String())
|
return p.SendDM(ctx.Sender, b.String())
|
||||||
}
|
}
|
||||||
b.WriteString(fmt.Sprintf("Stats (pre-racial): STR %d DEX %d CON %d INT %d WIS %d CHA %d ✓\n",
|
b.WriteString(fmt.Sprintf("Stats (pre-racial): STR %d DEX %d CON %d INT %d WIS %d CHA %d ✓\n",
|
||||||
c.STR, c.DEX, c.CON, c.INT, c.WIS, c.CHA))
|
c.STR, c.DEX, c.CON, c.INT, c.WIS, c.CHA))
|
||||||
b.WriteString("\n**Step 4 — Confirm.** Reply `!setup confirm` to finalize, or `!setup cancel` to scrap and start over.\n")
|
b.WriteString("\n**Step 4 — Confirm.** Reply `!setup confirm` to finalize, or `!setup cancel` to scrap and start over.\n")
|
||||||
b.WriteString(renderConfirmPreview(c))
|
b.WriteString(renderConfirmPreview(c))
|
||||||
return p.SendDM(ctx.Sender,b.String())
|
return p.SendDM(ctx.Sender, b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AdventurePlugin) dndSetupRace(ctx MessageContext, raceArg string) error {
|
func (p *AdventurePlugin) dndSetupRace(ctx MessageContext, raceArg string) error {
|
||||||
r, ok := parseRace(raceArg)
|
r, ok := parseRace(raceArg)
|
||||||
if !ok {
|
if !ok {
|
||||||
return p.SendDM(ctx.Sender,"Unknown race. Options: human, elf, dwarf, halfling, orc, tiefling, half-elf")
|
return p.SendDM(ctx.Sender, "Unknown race. Options: human, elf, dwarf, halfling, orc, tiefling, half-elf")
|
||||||
}
|
}
|
||||||
c, err := loadOrInitDraft(ctx.Sender)
|
c, err := loadOrInitDraft(ctx.Sender)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p.SendDM(ctx.Sender,"Couldn't open your draft: "+err.Error())
|
return p.SendDM(ctx.Sender, "Couldn't open your draft: "+err.Error())
|
||||||
}
|
}
|
||||||
c.Race = r
|
c.Race = r
|
||||||
if err := SaveDnDCharacter(c); err != nil {
|
if err := SaveDnDCharacter(c); err != nil {
|
||||||
return p.SendDM(ctx.Sender,"Couldn't save: "+err.Error())
|
return p.SendDM(ctx.Sender, "Couldn't save: "+err.Error())
|
||||||
}
|
}
|
||||||
ri, _ := raceInfo(r)
|
ri, _ := raceInfo(r)
|
||||||
return p.SendDM(ctx.Sender,fmt.Sprintf("Race set: **%s**. _%s_\n\nNext: `!setup class <name>` — see options with `!setup`.",
|
return p.SendDM(ctx.Sender, fmt.Sprintf("Race set: **%s**. _%s_\n\nNext: `!setup class <name>` — see options with `!setup`.",
|
||||||
ri.Display, ri.Passive))
|
ri.Display, ri.Passive))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AdventurePlugin) dndSetupClass(ctx MessageContext, classArg string) error {
|
func (p *AdventurePlugin) dndSetupClass(ctx MessageContext, classArg string) error {
|
||||||
cl, ok := parseClass(classArg)
|
cl, ok := parseClass(classArg)
|
||||||
if !ok {
|
if !ok {
|
||||||
return p.SendDM(ctx.Sender,"Unknown class. Options: fighter, rogue, mage, cleric, ranger")
|
return p.SendDM(ctx.Sender, "Unknown class. Options: fighter, rogue, mage, cleric, ranger")
|
||||||
}
|
}
|
||||||
c, err := loadOrInitDraft(ctx.Sender)
|
c, err := loadOrInitDraft(ctx.Sender)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p.SendDM(ctx.Sender,"Couldn't open your draft: "+err.Error())
|
return p.SendDM(ctx.Sender, "Couldn't open your draft: "+err.Error())
|
||||||
}
|
}
|
||||||
c.Class = cl
|
c.Class = cl
|
||||||
if err := SaveDnDCharacter(c); err != nil {
|
if err := SaveDnDCharacter(c); err != nil {
|
||||||
return p.SendDM(ctx.Sender,"Couldn't save: "+err.Error())
|
return p.SendDM(ctx.Sender, "Couldn't save: "+err.Error())
|
||||||
}
|
}
|
||||||
ci, _ := classInfo(cl)
|
ci, _ := classInfo(cl)
|
||||||
return p.SendDM(ctx.Sender,fmt.Sprintf("Class set: **%s** — leans on %s & %s.\n\n"+
|
return p.SendDM(ctx.Sender, fmt.Sprintf("Class set: **%s** — leans on %s & %s.\n\n"+
|
||||||
"Next: assign your stats. The standard array is **15 14 13 12 10 8** — six numbers, each used once.\n"+
|
"Next: assign your stats. The standard array is **15 14 13 12 10 8** — six numbers, each used once.\n"+
|
||||||
"Order: STR DEX CON INT WIS CHA.\n\n"+
|
"Order: STR DEX CON INT WIS CHA.\n\n"+
|
||||||
"Example: `!setup stats 15 14 13 12 10 8` (all rolled into STR-first; rearrange to taste).",
|
"Example: `!setup stats 15 14 13 12 10 8` (all rolled into STR-first; rearrange to taste).",
|
||||||
@@ -227,29 +227,29 @@ func (p *AdventurePlugin) dndSetupStats(ctx MessageContext, statsArg string) err
|
|||||||
}
|
}
|
||||||
c, err := loadOrInitDraft(ctx.Sender)
|
c, err := loadOrInitDraft(ctx.Sender)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return p.SendDM(ctx.Sender,"Couldn't open your draft: "+err.Error())
|
return p.SendDM(ctx.Sender, "Couldn't open your draft: "+err.Error())
|
||||||
}
|
}
|
||||||
c.STR, c.DEX, c.CON, c.INT, c.WIS, c.CHA = scores[0], scores[1], scores[2], scores[3], scores[4], scores[5]
|
c.STR, c.DEX, c.CON, c.INT, c.WIS, c.CHA = scores[0], scores[1], scores[2], scores[3], scores[4], scores[5]
|
||||||
if err := SaveDnDCharacter(c); err != nil {
|
if err := SaveDnDCharacter(c); err != nil {
|
||||||
return p.SendDM(ctx.Sender,"Couldn't save: "+err.Error())
|
return p.SendDM(ctx.Sender, "Couldn't save: "+err.Error())
|
||||||
}
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("Stats saved (pre-racial bonuses).\n")
|
b.WriteString("Stats saved (pre-racial bonuses).\n")
|
||||||
b.WriteString(renderConfirmPreview(c))
|
b.WriteString(renderConfirmPreview(c))
|
||||||
b.WriteString("\nReply `!setup confirm` to finalize.")
|
b.WriteString("\nReply `!setup confirm` to finalize.")
|
||||||
return p.SendDM(ctx.Sender,b.String())
|
return p.SendDM(ctx.Sender, b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AdventurePlugin) dndSetupConfirm(ctx MessageContext) error {
|
func (p *AdventurePlugin) dndSetupConfirm(ctx MessageContext) error {
|
||||||
c, err := LoadDnDCharacter(ctx.Sender)
|
c, err := LoadDnDCharacter(ctx.Sender)
|
||||||
if err != nil || c == nil {
|
if err != nil || c == nil {
|
||||||
return p.SendDM(ctx.Sender,"No setup draft found. Run `!setup` to start.")
|
return p.SendDM(ctx.Sender, "No setup draft found. Run `!setup` to start.")
|
||||||
}
|
}
|
||||||
if !c.PendingSetup {
|
if !c.PendingSetup {
|
||||||
return p.SendDM(ctx.Sender,"Already confirmed. Use `!sheet` to view your character.")
|
return p.SendDM(ctx.Sender, "Already confirmed. Use `!sheet` to view your character.")
|
||||||
}
|
}
|
||||||
if c.Race == "" || c.Class == "" || !statsAssigned(c) {
|
if c.Race == "" || c.Class == "" || !statsAssigned(c) {
|
||||||
return p.SendDM(ctx.Sender,"Draft incomplete. Run `!setup` to see what's missing.")
|
return p.SendDM(ctx.Sender, "Draft incomplete. Run `!setup` to see what's missing.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply racial modifiers to the assigned scores.
|
// Apply racial modifiers to the assigned scores.
|
||||||
@@ -283,28 +283,28 @@ func (p *AdventurePlugin) dndSetupConfirm(ctx MessageContext) error {
|
|||||||
c.UpdatedAt = time.Now().UTC()
|
c.UpdatedAt = time.Now().UTC()
|
||||||
|
|
||||||
if err := SaveDnDCharacter(c); err != nil {
|
if err := SaveDnDCharacter(c); err != nil {
|
||||||
return p.SendDM(ctx.Sender,"Couldn't save: "+err.Error())
|
return p.SendDM(ctx.Sender, "Couldn't save: "+err.Error())
|
||||||
}
|
}
|
||||||
_ = initResources(ctx.Sender, c.Class)
|
_ = initResources(ctx.Sender, c.Class)
|
||||||
// Phase 9: caster classes get a default known-spell list and slot pool.
|
// Phase 9: caster classes get a default known-spell list and slot pool.
|
||||||
// Idempotent — !setup confirm after a respec wipe will repopulate.
|
// Idempotent — !setup confirm after a respec wipe will repopulate.
|
||||||
_ = ensureSpellsForCharacter(c)
|
_ = ensureSpellsForCharacter(c)
|
||||||
|
|
||||||
return p.SendDM(ctx.Sender,renderSetupComplete(c))
|
return p.SendDM(ctx.Sender, renderSetupComplete(c))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AdventurePlugin) dndSetupCancel(ctx MessageContext) error {
|
func (p *AdventurePlugin) dndSetupCancel(ctx MessageContext) error {
|
||||||
c, err := LoadDnDCharacter(ctx.Sender)
|
c, err := LoadDnDCharacter(ctx.Sender)
|
||||||
if err != nil || c == nil {
|
if err != nil || c == nil {
|
||||||
return p.SendDM(ctx.Sender,"No draft to cancel.")
|
return p.SendDM(ctx.Sender, "No draft to cancel.")
|
||||||
}
|
}
|
||||||
if !c.PendingSetup {
|
if !c.PendingSetup {
|
||||||
return p.SendDM(ctx.Sender,"Your character is already finalized — use `!respec` instead.")
|
return p.SendDM(ctx.Sender, "Your character is already finalized — use `!respec` instead.")
|
||||||
}
|
}
|
||||||
if _, err := dbExecCancel(ctx.Sender); err != nil {
|
if _, err := dbExecCancel(ctx.Sender); err != nil {
|
||||||
return p.SendDM(ctx.Sender,"Couldn't cancel: "+err.Error())
|
return p.SendDM(ctx.Sender, "Couldn't cancel: "+err.Error())
|
||||||
}
|
}
|
||||||
return p.SendDM(ctx.Sender,"Draft scrapped. Run `!setup` to start over.")
|
return p.SendDM(ctx.Sender, "Draft scrapped. Run `!setup` to start over.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── !respec ──────────────────────────────────────────────────────────────────
|
// ── !respec ──────────────────────────────────────────────────────────────────
|
||||||
@@ -480,11 +480,11 @@ func dbExecCancel(userID id.UserID) (int64, error) {
|
|||||||
// parseStatsArg accepts the six standard-array values in any of several
|
// parseStatsArg accepts the six standard-array values in any of several
|
||||||
// natural forms. All of these work:
|
// natural forms. All of these work:
|
||||||
//
|
//
|
||||||
// 15 14 13 12 10 8
|
// 15 14 13 12 10 8
|
||||||
// 15, 14, 13, 12, 10, 8
|
// 15, 14, 13, 12, 10, 8
|
||||||
// (15, 14, 13, 12, 10, 8)
|
// (15, 14, 13, 12, 10, 8)
|
||||||
// {15 14 13 12 10 8}
|
// {15 14 13 12 10 8}
|
||||||
// [15,14,13,12,10,8]
|
// [15,14,13,12,10,8]
|
||||||
//
|
//
|
||||||
// Returns the scores in input order. Validation that they're a permutation
|
// Returns the scores in input order. Validation that they're a permutation
|
||||||
// of the standard array happens in the caller via isStandardArray.
|
// of the standard array happens in the caller via isStandardArray.
|
||||||
|
|||||||
@@ -21,17 +21,17 @@ import (
|
|||||||
type DnDSkill string
|
type DnDSkill string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SkillAthletics DnDSkill = "athletics"
|
SkillAthletics DnDSkill = "athletics"
|
||||||
SkillAcrobatics DnDSkill = "acrobatics"
|
SkillAcrobatics DnDSkill = "acrobatics"
|
||||||
SkillStealth DnDSkill = "stealth"
|
SkillStealth DnDSkill = "stealth"
|
||||||
SkillSleightOfHand DnDSkill = "sleight_of_hand"
|
SkillSleightOfHand DnDSkill = "sleight_of_hand"
|
||||||
SkillArcana DnDSkill = "arcana"
|
SkillArcana DnDSkill = "arcana"
|
||||||
SkillInvestigation DnDSkill = "investigation"
|
SkillInvestigation DnDSkill = "investigation"
|
||||||
SkillPerception DnDSkill = "perception"
|
SkillPerception DnDSkill = "perception"
|
||||||
SkillInsight DnDSkill = "insight"
|
SkillInsight DnDSkill = "insight"
|
||||||
SkillPersuasion DnDSkill = "persuasion"
|
SkillPersuasion DnDSkill = "persuasion"
|
||||||
SkillIntimidation DnDSkill = "intimidation"
|
SkillIntimidation DnDSkill = "intimidation"
|
||||||
SkillDeception DnDSkill = "deception"
|
SkillDeception DnDSkill = "deception"
|
||||||
)
|
)
|
||||||
|
|
||||||
type dndSkillInfo struct {
|
type dndSkillInfo struct {
|
||||||
|
|||||||
@@ -459,20 +459,20 @@ func (d turnBuffDelta) any() bool {
|
|||||||
// numerically identical to the auto-resolve engine.
|
// numerically identical to the auto-resolve engine.
|
||||||
func diffTurnBuff(bs, as CombatStats, bm, am CombatModifiers) turnBuffDelta {
|
func diffTurnBuff(bs, as CombatStats, bm, am CombatModifiers) turnBuffDelta {
|
||||||
d := turnBuffDelta{
|
d := turnBuffDelta{
|
||||||
dAC: as.AC - bs.AC,
|
dAC: as.AC - bs.AC,
|
||||||
dAtk: as.AttackBonus - bs.AttackBonus,
|
dAtk: as.AttackBonus - bs.AttackBonus,
|
||||||
dSpeed: as.Speed - bs.Speed,
|
dSpeed: as.Speed - bs.Speed,
|
||||||
dPetDmg: am.PetAttackDmg - bm.PetAttackDmg,
|
dPetDmg: am.PetAttackDmg - bm.PetAttackDmg,
|
||||||
dCrit: as.CritRate - bs.CritRate,
|
dCrit: as.CritRate - bs.CritRate,
|
||||||
dDmgBonus: am.DamageBonus - bm.DamageBonus,
|
dDmgBonus: am.DamageBonus - bm.DamageBonus,
|
||||||
dPetProc: am.PetAttackProc - bm.PetAttackProc,
|
dPetProc: am.PetAttackProc - bm.PetAttackProc,
|
||||||
ward: am.WardCharges - bm.WardCharges,
|
ward: am.WardCharges - bm.WardCharges,
|
||||||
spore: am.SporeCloud - bm.SporeCloud,
|
spore: am.SporeCloud - bm.SporeCloud,
|
||||||
reflect: am.ReflectNext - bm.ReflectNext,
|
reflect: am.ReflectNext - bm.ReflectNext,
|
||||||
arcaneWard: am.ArcaneWardHP - bm.ArcaneWardHP,
|
arcaneWard: am.ArcaneWardHP - bm.ArcaneWardHP,
|
||||||
autoCrit: am.AutoCritFirst && !bm.AutoCritFirst,
|
autoCrit: am.AutoCritFirst && !bm.AutoCritFirst,
|
||||||
enemySkip: am.SpellEnemySkipFirst && !bm.SpellEnemySkipFirst,
|
enemySkip: am.SpellEnemySkipFirst && !bm.SpellEnemySkipFirst,
|
||||||
heal: as.MaxHP - bs.MaxHP,
|
heal: as.MaxHP - bs.MaxHP,
|
||||||
dSpiritProc: am.SpiritWeaponProc - bm.SpiritWeaponProc,
|
dSpiritProc: am.SpiritWeaponProc - bm.SpiritWeaponProc,
|
||||||
dSpiritDmg: am.SpiritWeaponDmg - bm.SpiritWeaponDmg,
|
dSpiritDmg: am.SpiritWeaponDmg - bm.SpiritWeaponDmg,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ func TestEnemySpellSaveMod_Scales(t *testing.T) {
|
|||||||
atk int
|
atk int
|
||||||
want int
|
want int
|
||||||
}{
|
}{
|
||||||
{0, 0}, // unstatted enemy
|
{0, 0}, // unstatted enemy
|
||||||
{2, 1}, // T1
|
{2, 1}, // T1
|
||||||
{4, 2}, // T2
|
{4, 2}, // T2
|
||||||
{6, 3}, // T3
|
{6, 3}, // T3
|
||||||
{8, 4}, // T4
|
{8, 4}, // T4
|
||||||
{10, 5}, // T5 floor
|
{10, 5}, // T5 floor
|
||||||
{12, 6}, // T5 boss
|
{12, 6}, // T5 boss
|
||||||
{20, 10}, // outsized boss
|
{20, 10}, // outsized boss
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
enemy := &CombatStats{AttackBonus: c.atk}
|
enemy := &CombatStats{AttackBonus: c.atk}
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ func buildSpellList() []SpellDefinition {
|
|||||||
{ID: "spirit_guardians", Name: "Spirit Guardians", Level: 3, School: "conjuration",
|
{ID: "spirit_guardians", Name: "Spirit Guardians", Level: 3, School: "conjuration",
|
||||||
Classes: cleric, Effect: EffectDamageSave, CastTime: CastAction,
|
Classes: cleric, Effect: EffectDamageSave, CastTime: CastAction,
|
||||||
Concentration: true, SaveStat: "WIS", DamageDice: "3d8",
|
Concentration: true, SaveStat: "WIS", DamageDice: "3d8",
|
||||||
DamageType: "radiant", AOE: true,
|
DamageType: "radiant", AOE: true,
|
||||||
Description: "Ghostly guardians swarm around you, slashing at anything hostile that comes close.",
|
Description: "Ghostly guardians swarm around you, slashing at anything hostile that comes close.",
|
||||||
Upcast: "more guardians per slot above 3rd"},
|
Upcast: "more guardians per slot above 3rd"},
|
||||||
{ID: "revivify", Name: "Revivify", Level: 3, School: "necromancy",
|
{ID: "revivify", Name: "Revivify", Level: 3, School: "necromancy",
|
||||||
@@ -349,7 +349,7 @@ func buildSpellList() []SpellDefinition {
|
|||||||
{ID: "wall_of_fire", Name: "Wall of Fire", Level: 4, School: "evocation",
|
{ID: "wall_of_fire", Name: "Wall of Fire", Level: 4, School: "evocation",
|
||||||
Classes: mage, Effect: EffectDamageSave, CastTime: CastAction,
|
Classes: mage, Effect: EffectDamageSave, CastTime: CastAction,
|
||||||
Concentration: true, SaveStat: "DEX", DamageDice: "5d8",
|
Concentration: true, SaveStat: "DEX", DamageDice: "5d8",
|
||||||
DamageType: "fire", AOE: true,
|
DamageType: "fire", AOE: true,
|
||||||
Description: "A long curtain of flame roars up from the ground. Anyone trying to cross regrets it loudly."},
|
Description: "A long curtain of flame roars up from the ground. Anyone trying to cross regrets it loudly."},
|
||||||
{ID: "greater_invisibility", Name: "Greater Invisibility", Level: 4, School: "illusion",
|
{ID: "greater_invisibility", Name: "Greater Invisibility", Level: 4, School: "illusion",
|
||||||
Classes: mage, Effect: EffectBuffSelf, CastTime: CastAction,
|
Classes: mage, Effect: EffectBuffSelf, CastTime: CastAction,
|
||||||
@@ -422,7 +422,7 @@ func buildSpellList() []SpellDefinition {
|
|||||||
Description: "Call up a small spirit-companion in animal shape — cat, raven, frog, the usual. It scouts, fetches, and judges you silently."},
|
Description: "Call up a small spirit-companion in animal shape — cat, raven, frog, the usual. It scouts, fetches, and judges you silently."},
|
||||||
{ID: "clairvoyance", Name: "Clairvoyance", Level: 3, School: "divination",
|
{ID: "clairvoyance", Name: "Clairvoyance", Level: 3, School: "divination",
|
||||||
Classes: []DnDClass{ClassMage, ClassCleric, ClassBard, ClassSorcerer, ClassWarlock},
|
Classes: []DnDClass{ClassMage, ClassCleric, ClassBard, ClassSorcerer, ClassWarlock},
|
||||||
Effect: EffectBuffSelf, CastTime: CastAction, Concentration: true, MaterialCost: 100,
|
Effect: EffectBuffSelf, CastTime: CastAction, Concentration: true, MaterialCost: 100,
|
||||||
Description: "Plant an invisible eye-or-ear in a place you know or can describe, and watch the world through it from afar."},
|
Description: "Plant an invisible eye-or-ear in a place you know or can describe, and watch the world through it from afar."},
|
||||||
{ID: "glyph_of_warding", Name: "Glyph of Warding", Level: 3, School: "abjuration",
|
{ID: "glyph_of_warding", Name: "Glyph of Warding", Level: 3, School: "abjuration",
|
||||||
Classes: []DnDClass{ClassMage, ClassCleric, ClassBard}, Effect: EffectDamageSave, CastTime: CastAction,
|
Classes: []DnDClass{ClassMage, ClassCleric, ClassBard}, Effect: EffectDamageSave, CastTime: CastAction,
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ func ragingPlayer() Combatant {
|
|||||||
CritRate: 0.05, DodgeRate: 0.05, BlockRate: 0.05,
|
CritRate: 0.05, DodgeRate: 0.05, BlockRate: 0.05,
|
||||||
},
|
},
|
||||||
Mods: CombatModifiers{
|
Mods: CombatModifiers{
|
||||||
DamageReduct: 1.0,
|
DamageReduct: 1.0,
|
||||||
BerserkerRage: true, RageMeleeDmg: 2,
|
BerserkerRage: true, RageMeleeDmg: 2,
|
||||||
PhysicalResistRage: true, FrenzyDmgBonus: 0.5,
|
PhysicalResistRage: true, FrenzyDmgBonus: 0.5,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -205,11 +205,11 @@ func TestParseStatsArg(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bad := []string{
|
bad := []string{
|
||||||
"", // empty
|
"", // empty
|
||||||
"15 14 13 12 10", // 5 numbers
|
"15 14 13 12 10", // 5 numbers
|
||||||
"15 14 13 12 10 8 7", // 7 numbers
|
"15 14 13 12 10 8 7", // 7 numbers
|
||||||
"15 14 13 12 10 abc", // non-number
|
"15 14 13 12 10 abc", // non-number
|
||||||
"banana", // garbage
|
"banana", // garbage
|
||||||
}
|
}
|
||||||
for _, in := range bad {
|
for _, in := range bad {
|
||||||
if _, err := parseStatsArg(in); err == nil {
|
if _, err := parseStatsArg(in); err == nil {
|
||||||
@@ -231,20 +231,20 @@ func TestParseStatsArg(t *testing.T) {
|
|||||||
|
|
||||||
func TestDnDLevelFromCombatLevel(t *testing.T) {
|
func TestDnDLevelFromCombatLevel(t *testing.T) {
|
||||||
cases := []struct{ combat, want int }{
|
cases := []struct{ combat, want int }{
|
||||||
{0, 1}, // floor
|
{0, 1}, // floor
|
||||||
{1, 1},
|
{1, 1},
|
||||||
{4, 1},
|
{4, 1},
|
||||||
{5, 1},
|
{5, 1},
|
||||||
{9, 1},
|
{9, 1},
|
||||||
{10, 2},
|
{10, 2},
|
||||||
{15, 3},
|
{15, 3},
|
||||||
{20, 4}, // nonk
|
{20, 4}, // nonk
|
||||||
{24, 4}, // quack
|
{24, 4}, // quack
|
||||||
{25, 5},
|
{25, 5},
|
||||||
{28, 5}, // prosolis
|
{28, 5}, // prosolis
|
||||||
{30, 6},
|
{30, 6},
|
||||||
{45, 9},
|
{45, 9},
|
||||||
{49, 9}, // holymachina
|
{49, 9}, // holymachina
|
||||||
{50, 10},
|
{50, 10},
|
||||||
{99, 19},
|
{99, 19},
|
||||||
{100, 20}, // clamp
|
{100, 20}, // clamp
|
||||||
|
|||||||
@@ -25,26 +25,26 @@ import (
|
|||||||
// dndXPTable[L] = cumulative XP needed to reach level L from level 1.
|
// dndXPTable[L] = cumulative XP needed to reach level L from level 1.
|
||||||
// L=0 is unused (D&D characters start at L1).
|
// L=0 is unused (D&D characters start at L1).
|
||||||
var dndXPTable = [...]int{
|
var dndXPTable = [...]int{
|
||||||
0, // L1 (start)
|
0, // L1 (start)
|
||||||
300, // L2
|
300, // L2
|
||||||
900, // L3
|
900, // L3
|
||||||
1700, // L4
|
1700, // L4
|
||||||
2700, // L5 (anchor: doc)
|
2700, // L5 (anchor: doc)
|
||||||
4300, // L6
|
4300, // L6
|
||||||
6500, // L7 (anchor: doc)
|
6500, // L7 (anchor: doc)
|
||||||
9000, // L8
|
9000, // L8
|
||||||
11500, // L9
|
11500, // L9
|
||||||
14000, // L10 (anchor: doc)
|
14000, // L10 (anchor: doc)
|
||||||
18000, // L11
|
18000, // L11
|
||||||
22000, // L12
|
22000, // L12
|
||||||
26000, // L13
|
26000, // L13
|
||||||
30000, // L14
|
30000, // L14
|
||||||
34000, // L15 (anchor: doc)
|
34000, // L15 (anchor: doc)
|
||||||
44000, // L16
|
44000, // L16
|
||||||
54000, // L17
|
54000, // L17
|
||||||
64000, // L18
|
64000, // L18
|
||||||
74000, // L19
|
74000, // L19
|
||||||
85000, // L20 (anchor: doc)
|
85000, // L20 (anchor: doc)
|
||||||
}
|
}
|
||||||
|
|
||||||
const dndMaxLevel = 20
|
const dndMaxLevel = 20
|
||||||
@@ -223,11 +223,11 @@ func buildLevelUpMessage(c *DnDCharacter, events []LevelUpEvent, flavor string)
|
|||||||
// Combat XP formulas. Tuned so a L1 player needs ~3 wins per level early
|
// Combat XP formulas. Tuned so a L1 player needs ~3 wins per level early
|
||||||
// and many more at high levels.
|
// and many more at high levels.
|
||||||
const (
|
const (
|
||||||
dndArenaXPPerThreat = 12 // arena win XP = threat * this
|
dndArenaXPPerThreat = 12 // arena win XP = threat * this
|
||||||
dndArenaXPMin = 30 // arena win XP floor (low-threat fights still pay)
|
dndArenaXPMin = 30 // arena win XP floor (low-threat fights still pay)
|
||||||
dndDungeonXPPerTier = 60 // dungeon win XP = tier^1.4 * this, roughly
|
dndDungeonXPPerTier = 60 // dungeon win XP = tier^1.4 * this, roughly
|
||||||
dndLossXPFraction = 0.25
|
dndLossXPFraction = 0.25
|
||||||
dndNearDeathXPBonus = 1.25 // multiplier for clutch wins
|
dndNearDeathXPBonus = 1.25 // multiplier for clutch wins
|
||||||
)
|
)
|
||||||
|
|
||||||
// arenaCombatXP returns the D&D XP to grant for an arena combat outcome.
|
// arenaCombatXP returns the D&D XP to grant for an arena combat outcome.
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ import (
|
|||||||
|
|
||||||
func TestDnDXPToNextLevel(t *testing.T) {
|
func TestDnDXPToNextLevel(t *testing.T) {
|
||||||
cases := []struct{ level, want int }{
|
cases := []struct{ level, want int }{
|
||||||
{1, 300}, // L1→L2: 300
|
{1, 300}, // L1→L2: 300
|
||||||
{2, 600}, // L2→L3: 900-300
|
{2, 600}, // L2→L3: 900-300
|
||||||
{4, 1000}, // L4→L5: 2700-1700
|
{4, 1000}, // L4→L5: 2700-1700
|
||||||
{6, 2200}, // L6→L7: 6500-4300
|
{6, 2200}, // L6→L7: 6500-4300
|
||||||
{19, 11000}, // L19→L20: 85000-74000
|
{19, 11000}, // L19→L20: 85000-74000
|
||||||
{20, 0}, // capped
|
{20, 0}, // capped
|
||||||
{0, 300}, // clamp to 1
|
{0, 300}, // clamp to 1
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
if got := dndXPToNextLevel(c.level); got != c.want {
|
if got := dndXPToNextLevel(c.level); got != c.want {
|
||||||
|
|||||||
@@ -1010,10 +1010,10 @@ func (p *AdventurePlugin) resolveRoom(userID id.UserID, run *DungeonRun, zone Zo
|
|||||||
// runs combat, persists side effects, fires nat-1/nat-20 mood deltas,
|
// runs combat, persists side effects, fires nat-1/nat-20 mood deltas,
|
||||||
// and renders the staged narration. Returns:
|
// and renders the staged narration. Returns:
|
||||||
//
|
//
|
||||||
// intro — pre-combat block (TwinBee combat-start + monster stat block)
|
// intro — pre-combat block (TwinBee combat-start + monster stat block)
|
||||||
// phases — RenderCombatLog output, streamed with delays by the caller
|
// phases — RenderCombatLog output, streamed with delays by the caller
|
||||||
// outcome — post-combat block: nat20/nat1 flavor, kill line, loot, d20 summary
|
// outcome — post-combat block: nat20/nat1 flavor, kill line, loot, d20 summary
|
||||||
// ended — true when the player went down (caller skips next-room teaser)
|
// ended — true when the player went down (caller skips next-room teaser)
|
||||||
//
|
//
|
||||||
// Phases will be nil only on a "no roster" skip — caller treats that as a
|
// Phases will be nil only on a "no roster" skip — caller treats that as a
|
||||||
// non-paced fallthrough.
|
// non-paced fallthrough.
|
||||||
|
|||||||
@@ -56,13 +56,13 @@ func TestPassiveDecayMood_DriftsTowardFifty(t *testing.T) {
|
|||||||
hoursAgo int
|
hoursAgo int
|
||||||
want int
|
want int
|
||||||
}{
|
}{
|
||||||
{"high-score-decays-down", 80, 5, 70}, // -2/hr * 5
|
{"high-score-decays-down", 80, 5, 70}, // -2/hr * 5
|
||||||
{"high-clamps-at-50", 80, 100, 50}, // would overshoot
|
{"high-clamps-at-50", 80, 100, 50}, // would overshoot
|
||||||
{"low-score-decays-up", 10, 5, 20}, // +2/hr * 5
|
{"low-score-decays-up", 10, 5, 20}, // +2/hr * 5
|
||||||
{"low-clamps-at-50", 10, 100, 50}, // would overshoot
|
{"low-clamps-at-50", 10, 100, 50}, // would overshoot
|
||||||
{"already-50-no-change", 50, 100, 50}, // neutral stays neutral
|
{"already-50-no-change", 50, 100, 50}, // neutral stays neutral
|
||||||
{"fresh-no-decay", 80, 0, 80}, // no time elapsed
|
{"fresh-no-decay", 80, 0, 80}, // no time elapsed
|
||||||
{"sub-hour-no-decay", 80, 0, 80}, // hours==0 → no drift
|
{"sub-hour-no-decay", 80, 0, 80}, // hours==0 → no drift
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
t.Run(c.name, func(t *testing.T) {
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
|||||||
@@ -73,9 +73,9 @@ type DungeonRun struct {
|
|||||||
// path of node_ids the player has resolved. NodeChoices stores
|
// path of node_ids the player has resolved. NodeChoices stores
|
||||||
// pending fork-prompt state (G5 surface) — populated when the player
|
// pending fork-prompt state (G5 surface) — populated when the player
|
||||||
// arrives at a fork with 2+ unlocked outgoing edges.
|
// arrives at a fork with 2+ unlocked outgoing edges.
|
||||||
CurrentNode string
|
CurrentNode string
|
||||||
VisitedNodes []string
|
VisitedNodes []string
|
||||||
NodeChoices map[string]any
|
NodeChoices map[string]any
|
||||||
|
|
||||||
// Revisit R1 — monotonic count of node entries, including the entry
|
// Revisit R1 — monotonic count of node entries, including the entry
|
||||||
// room. Distinct from CurrentRoom: CurrentRoom answers "where am I on
|
// room. Distinct from CurrentRoom: CurrentRoom answers "where am I on
|
||||||
@@ -623,4 +623,3 @@ func addLoot(runID string, itemID string) error {
|
|||||||
WHERE run_id = ?`, string(lootJSON), runID)
|
WHERE run_id = ?`, string(lootJSON), runID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,18 +37,18 @@ const (
|
|||||||
// (count, sides) form; AlertsEnemies flags traps that have no damage
|
// (count, sides) form; AlertsEnemies flags traps that have no damage
|
||||||
// but raise the alarm. Weight biases the room-level random pick.
|
// but raise the alarm. Weight biases the room-level random pick.
|
||||||
type zoneTrapDef struct {
|
type zoneTrapDef struct {
|
||||||
Kind ZoneTrapKind
|
Kind ZoneTrapKind
|
||||||
Display string
|
Display string
|
||||||
Trigger string // short flavor descriptor for the trip narration
|
Trigger string // short flavor descriptor for the trip narration
|
||||||
DetectSkill DnDSkill
|
DetectSkill DnDSkill
|
||||||
DetectDC int
|
DetectDC int
|
||||||
DamageDiceN int // 0 if no direct damage
|
DamageDiceN int // 0 if no direct damage
|
||||||
DamageDiceD int // sides
|
DamageDiceD int // sides
|
||||||
DamageType string // "bludgeoning", "piercing", "poison", ...
|
DamageType string // "bludgeoning", "piercing", "poison", ...
|
||||||
AlertsEnemies bool // tripwire-class: 0 damage, raises alarm
|
AlertsEnemies bool // tripwire-class: 0 damage, raises alarm
|
||||||
SuppressesMagic bool // antimagic-class: 0 damage, narrative suppression
|
SuppressesMagic bool // antimagic-class: 0 damage, narrative suppression
|
||||||
Tier ZoneTier
|
Tier ZoneTier
|
||||||
Weight int // 1..10, default 5
|
Weight int // 1..10, default 5
|
||||||
}
|
}
|
||||||
|
|
||||||
// tier1TrapCatalog — design doc §6 Tier 1 table.
|
// tier1TrapCatalog — design doc §6 Tier 1 table.
|
||||||
|
|||||||
@@ -583,4 +583,3 @@ func (p *EuroPlugin) handleGrant(ctx MessageContext) error {
|
|||||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||||
fmt.Sprintf("✅ Granted €%d to **%s**. New balance: €%d.", int(amount), targetName, int(newBalance)))
|
fmt.Sprintf("✅ Granted €%d to **%s**. New balance: €%d.", int(amount), targetName, int(newBalance)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,9 +209,9 @@ func (p *AdventurePlugin) deliverAmbient(e *Expedition, now time.Time) error {
|
|||||||
// ambientEvent — one entry in the ambient pool. Weight is relative; the
|
// ambientEvent — one entry in the ambient pool. Weight is relative; the
|
||||||
// resolver normalizes over eligible events at pick time.
|
// resolver normalizes over eligible events at pick time.
|
||||||
type ambientEvent struct {
|
type ambientEvent struct {
|
||||||
Kind string
|
Kind string
|
||||||
Pool []string
|
Pool []string
|
||||||
Weight int
|
Weight int
|
||||||
Eligible func(*Expedition) bool
|
Eligible func(*Expedition) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,9 +219,9 @@ type ambientEvent struct {
|
|||||||
// out so deliverAmbient can degrade to it if a chosen pool is empty.
|
// out so deliverAmbient can degrade to it if a chosen pool is empty.
|
||||||
func ambientEventMonologue() ambientEvent {
|
func ambientEventMonologue() ambientEvent {
|
||||||
return ambientEvent{
|
return ambientEvent{
|
||||||
Kind: "monologue",
|
Kind: "monologue",
|
||||||
Pool: flavor.AmbientMonologue,
|
Pool: flavor.AmbientMonologue,
|
||||||
Weight: 35,
|
Weight: 35,
|
||||||
Eligible: func(*Expedition) bool { return true },
|
Eligible: func(*Expedition) bool { return true },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,15 +233,15 @@ func ambientEvents() []ambientEvent {
|
|||||||
return []ambientEvent{
|
return []ambientEvent{
|
||||||
ambientEventMonologue(),
|
ambientEventMonologue(),
|
||||||
{
|
{
|
||||||
Kind: "small_find",
|
Kind: "small_find",
|
||||||
Pool: flavor.AmbientSmallFind,
|
Pool: flavor.AmbientSmallFind,
|
||||||
Weight: 10,
|
Weight: 10,
|
||||||
Eligible: always,
|
Eligible: always,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Kind: "noise",
|
Kind: "noise",
|
||||||
Pool: flavor.AmbientNoise,
|
Pool: flavor.AmbientNoise,
|
||||||
Weight: 15,
|
Weight: 15,
|
||||||
Eligible: always,
|
Eligible: always,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -256,9 +256,9 @@ func ambientEvents() []ambientEvent {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Kind: "lucky_find",
|
Kind: "lucky_find",
|
||||||
Pool: flavor.AmbientLuckyFind,
|
Pool: flavor.AmbientLuckyFind,
|
||||||
Weight: 10,
|
Weight: 10,
|
||||||
Eligible: always,
|
Eligible: always,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import (
|
|||||||
|
|
||||||
func TestDecideAutopilotCamp_SkipsWhenCamped(t *testing.T) {
|
func TestDecideAutopilotCamp_SkipsWhenCamped(t *testing.T) {
|
||||||
in := autoCampInputs{
|
in := autoCampInputs{
|
||||||
Camp: &CampState{Active: true, Type: CampTypeRough},
|
Camp: &CampState{Active: true, Type: CampTypeRough},
|
||||||
Run: &DungeonRun{CurrentRoom: 0, RoomsCleared: []int{0}},
|
Run: &DungeonRun{CurrentRoom: 0, RoomsCleared: []int{0}},
|
||||||
HPCur: 1, HPMax: 20, // very low — would otherwise camp
|
HPCur: 1, HPMax: 20, // very low — would otherwise camp
|
||||||
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
|
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
|
||||||
}
|
}
|
||||||
if _, ok := decideAutopilotCamp(in); ok {
|
if _, ok := decideAutopilotCamp(in); ok {
|
||||||
@@ -21,8 +21,8 @@ func TestDecideAutopilotCamp_SkipsWhenCamped(t *testing.T) {
|
|||||||
|
|
||||||
func TestDecideAutopilotCamp_SkipsHealthyHP(t *testing.T) {
|
func TestDecideAutopilotCamp_SkipsHealthyHP(t *testing.T) {
|
||||||
in := autoCampInputs{
|
in := autoCampInputs{
|
||||||
Run: &DungeonRun{CurrentRoom: 0, RoomsCleared: []int{0}},
|
Run: &DungeonRun{CurrentRoom: 0, RoomsCleared: []int{0}},
|
||||||
HPCur: 20, HPMax: 20,
|
HPCur: 20, HPMax: 20,
|
||||||
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
|
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
|
||||||
}
|
}
|
||||||
if _, ok := decideAutopilotCamp(in); ok {
|
if _, ok := decideAutopilotCamp(in); ok {
|
||||||
@@ -32,8 +32,8 @@ func TestDecideAutopilotCamp_SkipsHealthyHP(t *testing.T) {
|
|||||||
|
|
||||||
func TestDecideAutopilotCamp_LowHPClearedPitchesStandard(t *testing.T) {
|
func TestDecideAutopilotCamp_LowHPClearedPitchesStandard(t *testing.T) {
|
||||||
in := autoCampInputs{
|
in := autoCampInputs{
|
||||||
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
|
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
|
||||||
HPCur: 5, HPMax: 20, // 25% — below 55% threshold
|
HPCur: 5, HPMax: 20, // 25% — below 55% threshold
|
||||||
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
|
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
|
||||||
}
|
}
|
||||||
d, ok := decideAutopilotCamp(in)
|
d, ok := decideAutopilotCamp(in)
|
||||||
@@ -44,8 +44,8 @@ func TestDecideAutopilotCamp_LowHPClearedPitchesStandard(t *testing.T) {
|
|||||||
|
|
||||||
func TestDecideAutopilotCamp_LowHPUnclearedPitchesRough(t *testing.T) {
|
func TestDecideAutopilotCamp_LowHPUnclearedPitchesRough(t *testing.T) {
|
||||||
in := autoCampInputs{
|
in := autoCampInputs{
|
||||||
Run: &DungeonRun{CurrentRoom: 2, RoomsCleared: []int{0, 1}},
|
Run: &DungeonRun{CurrentRoom: 2, RoomsCleared: []int{0, 1}},
|
||||||
HPCur: 5, HPMax: 20,
|
HPCur: 5, HPMax: 20,
|
||||||
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
|
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
|
||||||
}
|
}
|
||||||
d, ok := decideAutopilotCamp(in)
|
d, ok := decideAutopilotCamp(in)
|
||||||
@@ -56,8 +56,8 @@ func TestDecideAutopilotCamp_LowHPUnclearedPitchesRough(t *testing.T) {
|
|||||||
|
|
||||||
func TestDecideAutopilotCamp_DowngradesWhenSuppliesTight(t *testing.T) {
|
func TestDecideAutopilotCamp_DowngradesWhenSuppliesTight(t *testing.T) {
|
||||||
in := autoCampInputs{
|
in := autoCampInputs{
|
||||||
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
|
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
|
||||||
HPCur: 5, HPMax: 20,
|
HPCur: 5, HPMax: 20,
|
||||||
Supplies: ExpeditionSupplies{Current: 0.6, Max: 5, DailyBurn: 1}, // can't afford 1.0
|
Supplies: ExpeditionSupplies{Current: 0.6, Max: 5, DailyBurn: 1}, // can't afford 1.0
|
||||||
}
|
}
|
||||||
d, ok := decideAutopilotCamp(in)
|
d, ok := decideAutopilotCamp(in)
|
||||||
@@ -227,8 +227,8 @@ func TestDecideAutopilotCamp_NightTriggerOnEventAnchored(t *testing.T) {
|
|||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
stale := now.Add(-(nightCampWindow + time.Hour))
|
stale := now.Add(-(nightCampWindow + time.Hour))
|
||||||
in := autoCampInputs{
|
in := autoCampInputs{
|
||||||
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
|
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
|
||||||
HPCur: 20, HPMax: 20, // healthy — only the night window should pitch
|
HPCur: 20, HPMax: 20, // healthy — only the night window should pitch
|
||||||
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
|
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
|
||||||
Now: now,
|
Now: now,
|
||||||
EventAnchored: true,
|
EventAnchored: true,
|
||||||
@@ -250,8 +250,8 @@ func TestDecideAutopilotCamp_NightSkippedOnLegacy(t *testing.T) {
|
|||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
stale := now.Add(-24 * time.Hour)
|
stale := now.Add(-24 * time.Hour)
|
||||||
in := autoCampInputs{
|
in := autoCampInputs{
|
||||||
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
|
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
|
||||||
HPCur: 20, HPMax: 20,
|
HPCur: 20, HPMax: 20,
|
||||||
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
|
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
|
||||||
Now: now,
|
Now: now,
|
||||||
EventAnchored: false, // legacy — only HP-low triggers
|
EventAnchored: false, // legacy — only HP-low triggers
|
||||||
@@ -266,8 +266,8 @@ func TestDecideAutopilotCamp_NightFlagSetEvenOnHPLow(t *testing.T) {
|
|||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
stale := now.Add(-(nightCampWindow + time.Hour))
|
stale := now.Add(-(nightCampWindow + time.Hour))
|
||||||
in := autoCampInputs{
|
in := autoCampInputs{
|
||||||
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
|
Run: &DungeonRun{CurrentRoom: 1, RoomsCleared: []int{0, 1}},
|
||||||
HPCur: 5, HPMax: 20, // also low
|
HPCur: 5, HPMax: 20, // also low
|
||||||
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
|
Supplies: ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1},
|
||||||
Now: now,
|
Now: now,
|
||||||
EventAnchored: true,
|
EventAnchored: true,
|
||||||
|
|||||||
@@ -329,9 +329,9 @@ func (p *AdventurePlugin) tryAutoRun(e *Expedition, now time.Time) error {
|
|||||||
// - expID: the expedition row id (for the EoD digest log fetch).
|
// - expID: the expedition row id (for the EoD digest log fetch).
|
||||||
// - r: walk result, including reason + accumulated stream.
|
// - r: walk result, including reason + accumulated stream.
|
||||||
// - camp: rendered camp block from maybeAutoCamp / pitchBossSafetyCamp,
|
// - camp: rendered camp block from maybeAutoCamp / pitchBossSafetyCamp,
|
||||||
// or "" when no camp was pitched this tick.
|
// or "" when no camp was pitched this tick.
|
||||||
// - dec: the camp decision; dec.Night is the trigger for the EoD
|
// - dec: the camp decision; dec.Night is the trigger for the EoD
|
||||||
// digest variant. Zero-value when no pitch happened.
|
// digest variant. Zero-value when no pitch happened.
|
||||||
//
|
//
|
||||||
// Surface rules:
|
// Surface rules:
|
||||||
// - stopFork / stopEnded / stopComplete → render the walk DM. These
|
// - stopFork / stopEnded / stopComplete → render the walk DM. These
|
||||||
|
|||||||
@@ -192,14 +192,14 @@ type expeditionTrialResult struct {
|
|||||||
// expeditionBalanceResult is the aggregated win-rate per cell. Mirrors
|
// expeditionBalanceResult is the aggregated win-rate per cell. Mirrors
|
||||||
// classBalanceResult — same shape, different lens.
|
// classBalanceResult — same shape, different lens.
|
||||||
type expeditionBalanceResult struct {
|
type expeditionBalanceResult struct {
|
||||||
Profile expeditionBalanceProfile
|
Profile expeditionBalanceProfile
|
||||||
Trials int
|
Trials int
|
||||||
Completions int
|
Completions int
|
||||||
Deaths int
|
Deaths int
|
||||||
StarvedOuts int
|
StarvedOuts int
|
||||||
MedianDays int
|
MedianDays int
|
||||||
MedianThreatEnd int
|
MedianThreatEnd int
|
||||||
AvgEncounters float64
|
AvgEncounters float64
|
||||||
AvgHPRemainingPct float64 // mean across survivors
|
AvgHPRemainingPct float64 // mean across survivors
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -655,11 +655,11 @@ func buildHarnessZoneEnemy(monster DnDMonsterTemplate, tier int) Combatant {
|
|||||||
// expeditionHarness threads per-trial mutable state (RNG, expedition,
|
// expeditionHarness threads per-trial mutable state (RNG, expedition,
|
||||||
// character HP carryover, encounter count) through the day loop.
|
// character HP carryover, encounter count) through the day loop.
|
||||||
type expeditionHarness struct {
|
type expeditionHarness struct {
|
||||||
exp *Expedition
|
exp *Expedition
|
||||||
char *DnDCharacter
|
char *DnDCharacter
|
||||||
rng *harnessRNG
|
rng *harnessRNG
|
||||||
encounters int
|
encounters int
|
||||||
rollsPerDay int // resolved from profile + default; never zero
|
rollsPerDay int // resolved from profile + default; never zero
|
||||||
// Lever overrides for the Phase 2 sweep. Zero on both fields ⇒
|
// Lever overrides for the Phase 2 sweep. Zero on both fields ⇒
|
||||||
// live behavior (retreatThreatBump=5, /5 surprise-nick divisor).
|
// live behavior (retreatThreatBump=5, /5 surprise-nick divisor).
|
||||||
retreatThreatBumpOverride int
|
retreatThreatBumpOverride int
|
||||||
@@ -743,12 +743,12 @@ func runExpeditionBalanceTrial(p expeditionBalanceProfile, seed uint64) expediti
|
|||||||
rolls = harnessHarvestRollsPerDay
|
rolls = harnessHarvestRollsPerDay
|
||||||
}
|
}
|
||||||
h := &expeditionHarness{
|
h := &expeditionHarness{
|
||||||
exp: exp,
|
exp: exp,
|
||||||
char: char,
|
char: char,
|
||||||
rng: newHarnessRNG(seed),
|
rng: newHarnessRNG(seed),
|
||||||
rollsPerDay: rolls,
|
rollsPerDay: rolls,
|
||||||
retreatThreatBumpOverride: p.RetreatThreatBumpOverride,
|
retreatThreatBumpOverride: p.RetreatThreatBumpOverride,
|
||||||
surpriseNickDivisorOverride: p.SurpriseNickDivisorOverride,
|
surpriseNickDivisorOverride: p.SurpriseNickDivisorOverride,
|
||||||
eliteInterruptThresholdOverride: p.EliteInterruptThresholdOverride,
|
eliteInterruptThresholdOverride: p.EliteInterruptThresholdOverride,
|
||||||
threatDriftBaseOverride: p.ThreatDriftBaseOverride,
|
threatDriftBaseOverride: p.ThreatDriftBaseOverride,
|
||||||
surpriseNickFloorOverride: p.SurpriseNickFloorOverride,
|
surpriseNickFloorOverride: p.SurpriseNickFloorOverride,
|
||||||
|
|||||||
@@ -528,8 +528,8 @@ func TestExpeditionBalance_Phase2_LeverSweep(t *testing.T) {
|
|||||||
// sweep walks two settings tighter (gentler) and one looser
|
// sweep walks two settings tighter (gentler) and one looser
|
||||||
// (harsher) for each knob so the live point sits in the middle of
|
// (harsher) for each knob so the live point sits in the middle of
|
||||||
// the grid and we can read the slope in both directions.
|
// the grid and we can read the slope in both directions.
|
||||||
bumps := []int{2, 5, 10} // threat-bump per retreat
|
bumps := []int{2, 5, 10} // threat-bump per retreat
|
||||||
divisors := []int{3, 5, 8, 12} // /N wounded-nick divisor; bigger = gentler
|
divisors := []int{3, 5, 8, 12} // /N wounded-nick divisor; bigger = gentler
|
||||||
|
|
||||||
t.Logf("phase2 lever sweep — %d zones × %d bump × %d divisor × %d trials, Fighter @ tier centerline (rolls=4)",
|
t.Logf("phase2 lever sweep — %d zones × %d bump × %d divisor × %d trials, Fighter @ tier centerline (rolls=4)",
|
||||||
len(zoneOrder), len(bumps), len(divisors), trialsPerCell)
|
len(zoneOrder), len(bumps), len(divisors), trialsPerCell)
|
||||||
@@ -622,16 +622,16 @@ func TestExpeditionBalance_Phase2_LeverSweep(t *testing.T) {
|
|||||||
// This sweep walks two of the global knobs called out in the plan-doc
|
// This sweep walks two of the global knobs called out in the plan-doc
|
||||||
// "Phase 3 — global lever tuning" section:
|
// "Phase 3 — global lever tuning" section:
|
||||||
//
|
//
|
||||||
// eliteInterruptThreshold (live=19, total roll cutoff for Elite
|
// eliteInterruptThreshold (live=19, total roll cutoff for Elite
|
||||||
// bracket) — directly controls how often fresh-entry elite fights
|
// bracket) — directly controls how often fresh-entry elite fights
|
||||||
// trigger during the daytime harvest pipeline. Sweep {17, 19, 23}:
|
// trigger during the daytime harvest pipeline. Sweep {17, 19, 23}:
|
||||||
// 17 = more elites (slope check below live), 19 = live baseline,
|
// 17 = more elites (slope check below live), 19 = live baseline,
|
||||||
// 23 = elites only when roll+mod ≥ 23 (rare even at T5).
|
// 23 = elites only when roll+mod ≥ 23 (rare even at T5).
|
||||||
//
|
//
|
||||||
// threatDriftBase (live=3, daily threat clock drift before mood-mod)
|
// threatDriftBase (live=3, daily threat clock drift before mood-mod)
|
||||||
// — slows the multi-day AC/init/supply-burn creep that compounds
|
// — slows the multi-day AC/init/supply-burn creep that compounds
|
||||||
// over a 14-day expedition. Sweep {1, 3, 5}: 1 = nearly flat, 3 =
|
// over a 14-day expedition. Sweep {1, 3, 5}: 1 = nearly flat, 3 =
|
||||||
// live, 5 = harsher.
|
// live, 5 = harsher.
|
||||||
//
|
//
|
||||||
// 3×3 = 9 combos × 10 zones × 200 trials/cell. Diagnostic-only — no
|
// 3×3 = 9 combos × 10 zones × 200 trials/cell. Diagnostic-only — no
|
||||||
// gates beyond the Phase 1 wiring sanity. -short skips.
|
// gates beyond the Phase 1 wiring sanity. -short skips.
|
||||||
@@ -736,19 +736,19 @@ func TestExpeditionBalance_Phase3_GlobalLeverSweep(t *testing.T) {
|
|||||||
// Phase 3-B holds the Phase 3-A best cell (eliteInterruptThreshold=23,
|
// Phase 3-B holds the Phase 3-A best cell (eliteInterruptThreshold=23,
|
||||||
// threatDriftBase=1) and walks two more global knobs:
|
// threatDriftBase=1) and walks two more global knobs:
|
||||||
//
|
//
|
||||||
// surpriseNickFloor (live=tier, i.e. 1..5 by zone tier) — the raw
|
// surpriseNickFloor (live=tier, i.e. 1..5 by zone tier) — the raw
|
||||||
// surprise-round nick on a fresh-HP entry. Lower floor = less
|
// surprise-round nick on a fresh-HP entry. Lower floor = less
|
||||||
// per-fight chip on T4/T5 standard fights, where the live tier=4-5
|
// per-fight chip on T4/T5 standard fights, where the live tier=4-5
|
||||||
// floor is the biggest single contributor to the wear-down curve
|
// floor is the biggest single contributor to the wear-down curve
|
||||||
// that funnels survivors into starvation. Sweep {-1 (disable, =0),
|
// that funnels survivors into starvation. Sweep {-1 (disable, =0),
|
||||||
// 1 (flat), 0-sentinel (live tier)}.
|
// 1 (flat), 0-sentinel (live tier)}.
|
||||||
//
|
//
|
||||||
// supplyBurnRatePct (live=100, %) — daily supply burn scalar. Lower
|
// supplyBurnRatePct (live=100, %) — daily supply burn scalar. Lower
|
||||||
// = more days of margin. T4 and T5 burns are 3×/4× the T1 baseline;
|
// = more days of margin. T4 and T5 burns are 3×/4× the T1 baseline;
|
||||||
// halving them ought to convert the starvation outs we saw in
|
// halving them ought to convert the starvation outs we saw in
|
||||||
// Phase 3-A into actual completions if survivability is the only
|
// Phase 3-A into actual completions if survivability is the only
|
||||||
// remaining blocker, or leave them dying in combat if it isn't.
|
// remaining blocker, or leave them dying in combat if it isn't.
|
||||||
// Sweep {100 (live), 75, 50}.
|
// Sweep {100 (live), 75, 50}.
|
||||||
//
|
//
|
||||||
// 3×3 = 9 combos × 10 zones × 200 trials/cell, all on top of the
|
// 3×3 = 9 combos × 10 zones × 200 trials/cell, all on top of the
|
||||||
// Phase 3-A best cell. Diagnostic-only — no gates beyond Phase 1
|
// Phase 3-A best cell. Diagnostic-only — no gates beyond Phase 1
|
||||||
@@ -925,14 +925,14 @@ func TestExpeditionBalance_Phase4A_OutlierDiagnostic(t *testing.T) {
|
|||||||
killAttributed int // last fight of a died_combat trial
|
killAttributed int // last fight of a died_combat trial
|
||||||
}
|
}
|
||||||
type zoneAgg struct {
|
type zoneAgg struct {
|
||||||
profile expeditionBalanceProfile
|
profile expeditionBalanceProfile
|
||||||
completions int
|
completions int
|
||||||
deaths int
|
deaths int
|
||||||
starves int
|
starves int
|
||||||
monsters map[string]*monsterStat
|
monsters map[string]*monsterStat
|
||||||
dayHist [16]int // 1..14 + overflow bucket; index 0 unused
|
dayHist [16]int // 1..14 + overflow bucket; index 0 unused
|
||||||
eliteFights int
|
eliteFights int
|
||||||
stdFights int
|
stdFights int
|
||||||
// for the trial currently in flight (mutated during traceFightStruct)
|
// for the trial currently in flight (mutated during traceFightStruct)
|
||||||
lastFight harnessFightTrace
|
lastFight harnessFightTrace
|
||||||
}
|
}
|
||||||
@@ -1094,9 +1094,9 @@ func TestExpeditionBalance_Phase4A_OutlierDiagnostic(t *testing.T) {
|
|||||||
// the lever has to be tier-wide. The plan doc names three candidate
|
// the lever has to be tier-wide. The plan doc names three candidate
|
||||||
// moves:
|
// moves:
|
||||||
//
|
//
|
||||||
// 1. Gear-tier centerline remap (phase1TierCenterline) → axis L
|
// 1. Gear-tier centerline remap (phase1TierCenterline) → axis L
|
||||||
// 2. Per-tier elite-bracket threshold (vs global) → axis E
|
// 2. Per-tier elite-bracket threshold (vs global) → axis E
|
||||||
// 3. Ship burn=75 globally (Phase 3-B's high-burn cell) → axis B
|
// 3. Ship burn=75 globally (Phase 3-B's high-burn cell) → axis B
|
||||||
//
|
//
|
||||||
// This diagnostic does *not* pick the lever. It runs a one-axis-at-a-
|
// This diagnostic does *not* pick the lever. It runs a one-axis-at-a-
|
||||||
// time sensitivity sweep on the six under-band zones and lets the
|
// time sensitivity sweep on the six under-band zones and lets the
|
||||||
@@ -1106,9 +1106,9 @@ func TestExpeditionBalance_Phase4A_OutlierDiagnostic(t *testing.T) {
|
|||||||
// means the lever is inert for that tier, monotone climb means it's
|
// means the lever is inert for that tier, monotone climb means it's
|
||||||
// the lever, non-monotone means a confound.
|
// the lever, non-monotone means a confound.
|
||||||
//
|
//
|
||||||
// L: player level = {centerline-2, centerline, centerline+2}
|
// L: player level = {centerline-2, centerline, centerline+2}
|
||||||
// E: elite threshold = {18, 23, 28}
|
// E: elite threshold = {18, 23, 28}
|
||||||
// B: supply burn pct = {40, 50, 60}
|
// B: supply burn pct = {40, 50, 60}
|
||||||
//
|
//
|
||||||
// Per cell we log comp%/death%/starve% plus the elite-vs-standard
|
// Per cell we log comp%/death%/starve% plus the elite-vs-standard
|
||||||
// fight share (so axis E's slope can be cross-checked against the
|
// fight share (so axis E's slope can be cross-checked against the
|
||||||
@@ -1145,7 +1145,7 @@ func TestExpeditionBalance_Phase5A_TierWideSensitivity(t *testing.T) {
|
|||||||
// Returns comp/death/starve counts plus elite/standard fight share
|
// Returns comp/death/starve counts plus elite/standard fight share
|
||||||
// so the caller can attribute the slope across axis values.
|
// so the caller can attribute the slope across axis values.
|
||||||
type cellOut struct {
|
type cellOut struct {
|
||||||
comp, death, starve int
|
comp, death, starve int
|
||||||
eliteFights, stdFights int
|
eliteFights, stdFights int
|
||||||
}
|
}
|
||||||
runCell := func(zid ZoneID, tier ZoneTier, level, elite, burn int) cellOut {
|
runCell := func(zid ZoneID, tier ZoneTier, level, elite, burn int) cellOut {
|
||||||
@@ -1281,8 +1281,9 @@ func TestExpeditionBalance_Phase5A_TierWideSensitivity(t *testing.T) {
|
|||||||
// what flat delta on top of the ladder lands T1-T5 in band.
|
// what flat delta on top of the ladder lands T1-T5 in band.
|
||||||
//
|
//
|
||||||
// Bands (gogobee_expedition_difficulty.md):
|
// Bands (gogobee_expedition_difficulty.md):
|
||||||
// T1 80% (70-90%) T2 72% (62-82%) T3 65% (55-75%)
|
//
|
||||||
// T4 55% (45-65%) T5 45% (35-55%)
|
// T1 80% (70-90%) T2 72% (62-82%) T3 65% (55-75%)
|
||||||
|
// T4 55% (45-65%) T5 45% (35-55%)
|
||||||
//
|
//
|
||||||
// 10 zones × 3 deltas × 200 trials = 6k trials; runs in ~1s.
|
// 10 zones × 3 deltas × 200 trials = 6k trials; runs in ~1s.
|
||||||
// Diagnostic-only — picks the delta Phase 5-B ships in
|
// Diagnostic-only — picks the delta Phase 5-B ships in
|
||||||
|
|||||||
@@ -171,8 +171,6 @@ func recordBotDefeat(userID id.UserID, game string) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// redirectToGamesRoom returns the room ID for games-restricted redirect.
|
// redirectToGamesRoom returns the room ID for games-restricted redirect.
|
||||||
func redirectToGamesRoom(sender id.UserID) string {
|
func redirectToGamesRoom(sender id.UserID) string {
|
||||||
_ = sender
|
_ = sender
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ func BenchmarkComputeSignalFromRates_CrossPairSize(b *testing.B) {
|
|||||||
baseRates := make([]float64, 260)
|
baseRates := make([]float64, 260)
|
||||||
quoteRates := make([]float64, 260)
|
quoteRates := make([]float64, 260)
|
||||||
for i := range baseRates {
|
for i := range baseRates {
|
||||||
baseRates[i] = 0.90 + rand.Float64()*0.10 // EUR/USD ~0.90-1.00
|
baseRates[i] = 0.90 + rand.Float64()*0.10 // EUR/USD ~0.90-1.00
|
||||||
quoteRates[i] = 145 + rand.Float64()*10 // JPY/USD ~145-155
|
quoteRates[i] = 145 + rand.Float64()*10 // JPY/USD ~145-155
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute cross-rates (simulating the join)
|
// Compute cross-rates (simulating the join)
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ func TestFxParseConvert(t *testing.T) {
|
|||||||
{[]string{"1500", "EUR/USD"}, 1500, "EUR", "USD", false},
|
{[]string{"1500", "EUR/USD"}, 1500, "EUR", "USD", false},
|
||||||
{[]string{"1500", "JPY", "USD"}, 1500, "JPY", "USD", false},
|
{[]string{"1500", "JPY", "USD"}, 1500, "JPY", "USD", false},
|
||||||
|
|
||||||
{[]string{"USD", "EUR"}, 0, "", "", true}, // no amount
|
{[]string{"USD", "EUR"}, 0, "", "", true}, // no amount
|
||||||
{[]string{"1500", "USD"}, 0, "", "", true}, // missing quote
|
{[]string{"1500", "USD"}, 0, "", "", true}, // missing quote
|
||||||
{[]string{"1500", "USD", "USD"}, 0, "", "", true}, // same currency
|
{[]string{"1500", "USD", "USD"}, 0, "", "", true}, // same currency
|
||||||
{[]string{"1500", "ZZZ", "EUR"}, 0, "", "", true}, // unknown currency
|
{[]string{"1500", "ZZZ", "EUR"}, 0, "", "", true}, // unknown currency
|
||||||
{[]string{"1500", "USD", "EUR", "JPY"}, 0, "", "", true}, // too many currencies
|
{[]string{"1500", "USD", "EUR", "JPY"}, 0, "", "", true}, // too many currencies
|
||||||
{[]string{"1500", "100", "USD", "EUR"}, 0, "", "", true}, // multiple amounts
|
{[]string{"1500", "100", "USD", "EUR"}, 0, "", "", true}, // multiple amounts
|
||||||
{[]string{"-100", "USD", "EUR"}, 0, "", "", true}, // negative
|
{[]string{"-100", "USD", "EUR"}, 0, "", "", true}, // negative
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
|
|||||||
@@ -23,45 +23,45 @@ import (
|
|||||||
|
|
||||||
// cityTimezones maps common city names to IANA timezone strings.
|
// cityTimezones maps common city names to IANA timezone strings.
|
||||||
var cityTimezones = map[string]string{
|
var cityTimezones = map[string]string{
|
||||||
"new york": "America/New_York",
|
"new york": "America/New_York",
|
||||||
"nyc": "America/New_York",
|
"nyc": "America/New_York",
|
||||||
"los angeles": "America/Los_Angeles",
|
"los angeles": "America/Los_Angeles",
|
||||||
"la": "America/Los_Angeles",
|
"la": "America/Los_Angeles",
|
||||||
"chicago": "America/Chicago",
|
"chicago": "America/Chicago",
|
||||||
"denver": "America/Denver",
|
"denver": "America/Denver",
|
||||||
"london": "Europe/London",
|
"london": "Europe/London",
|
||||||
"paris": "Europe/Paris",
|
"paris": "Europe/Paris",
|
||||||
"berlin": "Europe/Berlin",
|
"berlin": "Europe/Berlin",
|
||||||
"tokyo": "Asia/Tokyo",
|
"tokyo": "Asia/Tokyo",
|
||||||
"sydney": "Australia/Sydney",
|
"sydney": "Australia/Sydney",
|
||||||
"dubai": "Asia/Dubai",
|
"dubai": "Asia/Dubai",
|
||||||
"moscow": "Europe/Moscow",
|
"moscow": "Europe/Moscow",
|
||||||
"mumbai": "Asia/Kolkata",
|
"mumbai": "Asia/Kolkata",
|
||||||
"beijing": "Asia/Shanghai",
|
"beijing": "Asia/Shanghai",
|
||||||
"shanghai": "Asia/Shanghai",
|
"shanghai": "Asia/Shanghai",
|
||||||
"singapore": "Asia/Singapore",
|
"singapore": "Asia/Singapore",
|
||||||
"hong kong": "Asia/Hong_Kong",
|
"hong kong": "Asia/Hong_Kong",
|
||||||
"seoul": "Asia/Seoul",
|
"seoul": "Asia/Seoul",
|
||||||
"toronto": "America/Toronto",
|
"toronto": "America/Toronto",
|
||||||
"vancouver": "America/Vancouver",
|
"vancouver": "America/Vancouver",
|
||||||
"sao paulo": "America/Sao_Paulo",
|
"sao paulo": "America/Sao_Paulo",
|
||||||
"mexico city": "America/Mexico_City",
|
"mexico city": "America/Mexico_City",
|
||||||
"cairo": "Africa/Cairo",
|
"cairo": "Africa/Cairo",
|
||||||
"johannesburg": "Africa/Johannesburg",
|
"johannesburg": "Africa/Johannesburg",
|
||||||
"auckland": "Pacific/Auckland",
|
"auckland": "Pacific/Auckland",
|
||||||
"honolulu": "Pacific/Honolulu",
|
"honolulu": "Pacific/Honolulu",
|
||||||
"hawaii": "Pacific/Honolulu",
|
"hawaii": "Pacific/Honolulu",
|
||||||
"anchorage": "America/Anchorage",
|
"anchorage": "America/Anchorage",
|
||||||
"amsterdam": "Europe/Amsterdam",
|
"amsterdam": "Europe/Amsterdam",
|
||||||
"rome": "Europe/Rome",
|
"rome": "Europe/Rome",
|
||||||
"madrid": "Europe/Madrid",
|
"madrid": "Europe/Madrid",
|
||||||
"lisbon": "Europe/Lisbon",
|
"lisbon": "Europe/Lisbon",
|
||||||
"bangkok": "Asia/Bangkok",
|
"bangkok": "Asia/Bangkok",
|
||||||
"jakarta": "Asia/Jakarta",
|
"jakarta": "Asia/Jakarta",
|
||||||
"manila": "Asia/Manila",
|
"manila": "Asia/Manila",
|
||||||
"taipei": "Asia/Taipei",
|
"taipei": "Asia/Taipei",
|
||||||
"utc": "UTC",
|
"utc": "UTC",
|
||||||
"gmt": "UTC",
|
"gmt": "UTC",
|
||||||
}
|
}
|
||||||
|
|
||||||
var eightBallResponses = []string{
|
var eightBallResponses = []string{
|
||||||
@@ -382,10 +382,10 @@ func scrapeHLTB(gameName string) (string, error) {
|
|||||||
"rangeYear": map[string]interface{}{"min": "", "max": ""},
|
"rangeYear": map[string]interface{}{"min": "", "max": ""},
|
||||||
"modifier": "",
|
"modifier": "",
|
||||||
},
|
},
|
||||||
"users": map[string]interface{}{"sortCategory": "postcount"},
|
"users": map[string]interface{}{"sortCategory": "postcount"},
|
||||||
"lists": map[string]interface{}{"sortCategory": "follows"},
|
"lists": map[string]interface{}{"sortCategory": "follows"},
|
||||||
"filter": "",
|
"filter": "",
|
||||||
"sort": 0,
|
"sort": 0,
|
||||||
"randomizer": 0,
|
"randomizer": 0,
|
||||||
},
|
},
|
||||||
"useCache": true,
|
"useCache": true,
|
||||||
|
|||||||
@@ -19,18 +19,18 @@ import (
|
|||||||
|
|
||||||
// releaseListResponse is the top-level RAWG API response for game releases.
|
// releaseListResponse is the top-level RAWG API response for game releases.
|
||||||
type releaseListResponse struct {
|
type releaseListResponse struct {
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
Results []releaseEntry `json:"results"`
|
Results []releaseEntry `json:"results"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// releaseEntry is a game entry from the RAWG releases API.
|
// releaseEntry is a game entry from the RAWG releases API.
|
||||||
type releaseEntry struct {
|
type releaseEntry struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Slug string `json:"slug"`
|
Slug string `json:"slug"`
|
||||||
Released string `json:"released"`
|
Released string `json:"released"`
|
||||||
Rating float64 `json:"rating"`
|
Rating float64 `json:"rating"`
|
||||||
Metacritic int `json:"metacritic"`
|
Metacritic int `json:"metacritic"`
|
||||||
Platforms []struct {
|
Platforms []struct {
|
||||||
Platform struct {
|
Platform struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
} `json:"platform"`
|
} `json:"platform"`
|
||||||
|
|||||||
@@ -973,4 +973,3 @@ func (p *HangmanPlugin) recordHangmanScore(userID id.UserID, earned float64) {
|
|||||||
string(userID), earned, earned,
|
string(userID), earned, earned,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import (
|
|||||||
// ── formatNumber (xp.go) ───────────────────────────────────────────────────
|
// ── formatNumber (xp.go) ───────────────────────────────────────────────────
|
||||||
|
|
||||||
func TestFormatNumber_Small(t *testing.T) {
|
func TestFormatNumber_Small(t *testing.T) {
|
||||||
cases := []struct{ in int; want string }{
|
cases := []struct {
|
||||||
|
in int
|
||||||
|
want string
|
||||||
|
}{
|
||||||
{0, "0"},
|
{0, "0"},
|
||||||
{1, "1"},
|
{1, "1"},
|
||||||
{999, "999"},
|
{999, "999"},
|
||||||
@@ -21,7 +24,10 @@ func TestFormatNumber_Small(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFormatNumber_WithCommas(t *testing.T) {
|
func TestFormatNumber_WithCommas(t *testing.T) {
|
||||||
cases := []struct{ in int; want string }{
|
cases := []struct {
|
||||||
|
in int
|
||||||
|
want string
|
||||||
|
}{
|
||||||
{1000, "1,000"},
|
{1000, "1,000"},
|
||||||
{10000, "10,000"},
|
{10000, "10,000"},
|
||||||
{1000000, "1,000,000"},
|
{1000000, "1,000,000"},
|
||||||
|
|||||||
@@ -1249,7 +1249,6 @@ func (p *HoldemPlugin) removePlayer(game *HoldemGame, uid id.UserID) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (p *HoldemPlugin) getNPCBalance() int64 {
|
func (p *HoldemPlugin) getNPCBalance() int64 {
|
||||||
d := db.Get()
|
d := db.Get()
|
||||||
var balance int64
|
var balance int64
|
||||||
|
|||||||
@@ -74,9 +74,9 @@ func (g *HoldemGame) firstToActPostflop() int {
|
|||||||
// ActionResult describes what happened after an action.
|
// ActionResult describes what happened after an action.
|
||||||
type ActionResult struct {
|
type ActionResult struct {
|
||||||
Announcement string
|
Announcement string
|
||||||
HandOver bool // only 1 player remains
|
HandOver bool // only 1 player remains
|
||||||
StreetOver bool // street betting is complete
|
StreetOver bool // street betting is complete
|
||||||
AllAllIn bool // all remaining players are all-in
|
AllAllIn bool // all remaining players are all-in
|
||||||
}
|
}
|
||||||
|
|
||||||
// doFold processes a fold action.
|
// doFold processes a fold action.
|
||||||
@@ -381,4 +381,3 @@ func (g *HoldemGame) returnUncalledBet() (name string, amount int64) {
|
|||||||
|
|
||||||
return "", 0
|
return "", 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -355,9 +355,9 @@ type CFRData struct {
|
|||||||
|
|
||||||
func packInfoSetKey(street Street, posIP bool, eqBucket, sprBkt, boardTex int, history string) InfoSetKey {
|
func packInfoSetKey(street Street, posIP bool, eqBucket, sprBkt, boardTex int, history string) InfoSetKey {
|
||||||
var key uint64
|
var key uint64
|
||||||
key |= uint64(street) & 0x7 // bits 0-2
|
key |= uint64(street) & 0x7 // bits 0-2
|
||||||
if posIP {
|
if posIP {
|
||||||
key |= 1 << 3 // bit 3
|
key |= 1 << 3 // bit 3
|
||||||
}
|
}
|
||||||
key |= (uint64(eqBucket) & 0xF) << 4 // bits 4-7 (4 bits for 12 buckets)
|
key |= (uint64(eqBucket) & 0xF) << 4 // bits 4-7 (4 bits for 12 buckets)
|
||||||
key |= (uint64(sprBkt) & 0x7) << 8 // bits 8-10
|
key |= (uint64(sprBkt) & 0x7) << 8 // bits 8-10
|
||||||
|
|||||||
@@ -72,13 +72,13 @@ type holdemTipContext struct {
|
|||||||
EquityVsShove EquityResult
|
EquityVsShove EquityResult
|
||||||
|
|
||||||
// Derived fields for the rules engine.
|
// Derived fields for the rules engine.
|
||||||
HoleRanks [2]int // 0-12, for preflop classification
|
HoleRanks [2]int // 0-12, for preflop classification
|
||||||
HoleSuited bool
|
HoleSuited bool
|
||||||
PreflopTier string // "premium", "strong", "speculative", "playable", "trash"
|
PreflopTier string // "premium", "strong", "speculative", "playable", "trash"
|
||||||
BoardPaired bool
|
BoardPaired bool
|
||||||
BoardMaxSuit int // largest same-suit count on the board
|
BoardMaxSuit int // largest same-suit count on the board
|
||||||
BoardStraightRisk int // 0-4, rough straight-possibility score
|
BoardStraightRisk int // 0-4, rough straight-possibility score
|
||||||
BoardHighRank int // highest rank on board, -1 preflop
|
BoardHighRank int // highest rank on board, -1 preflop
|
||||||
}
|
}
|
||||||
|
|
||||||
// tipSnapshot holds game data captured under lock for async tip generation.
|
// tipSnapshot holds game data captured under lock for async tip generation.
|
||||||
|
|||||||
@@ -137,11 +137,11 @@ func TestHasStraight(t *testing.T) {
|
|||||||
ranks uint16
|
ranks uint16
|
||||||
want bool
|
want bool
|
||||||
}{
|
}{
|
||||||
{"A-high straight (T-J-Q-K-A)", 0x1F00, true}, // bits 8-12
|
{"A-high straight (T-J-Q-K-A)", 0x1F00, true}, // bits 8-12
|
||||||
{"low straight (A-2-3-4-5)", 0x100F, true}, // bits 0,1,2,3,12
|
{"low straight (A-2-3-4-5)", 0x100F, true}, // bits 0,1,2,3,12
|
||||||
{"5-6-7-8-9", 0x00F8, true}, // bits 3-7
|
{"5-6-7-8-9", 0x00F8, true}, // bits 3-7
|
||||||
{"four consecutive", 0x000F, false}, // bits 0-3
|
{"four consecutive", 0x000F, false}, // bits 0-3
|
||||||
{"scattered", 0x1111, false}, // bits 0,4,8,12
|
{"scattered", 0x1111, false}, // bits 0,4,8,12
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@@ -254,14 +254,14 @@ func TestBuildTipUserPrompt_NoDrawWhenMadeHand(t *testing.T) {
|
|||||||
|
|
||||||
func TestBuildTipUserPrompt_PotOddsWhenFacingBet(t *testing.T) {
|
func TestBuildTipUserPrompt_PotOddsWhenFacingBet(t *testing.T) {
|
||||||
ctx := holdemTipContext{
|
ctx := holdemTipContext{
|
||||||
Hole: [2]string{"A♠", "K♠"},
|
Hole: [2]string{"A♠", "K♠"},
|
||||||
Community: "2♣ 7♦ J♠",
|
Community: "2♣ 7♦ J♠",
|
||||||
Street: StreetFlop,
|
Street: StreetFlop,
|
||||||
Position: "CO",
|
Position: "CO",
|
||||||
NumActive: 4,
|
NumActive: 4,
|
||||||
Equity: EquityResult{Win: 0.45, Tie: 0.02, Loss: 0.53},
|
Equity: EquityResult{Win: 0.45, Tie: 0.02, Loss: 0.53},
|
||||||
ToCall: 50,
|
ToCall: 50,
|
||||||
PotOddsPct: 25.0,
|
PotOddsPct: 25.0,
|
||||||
HandCategory: "High Card",
|
HandCategory: "High Card",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -168,11 +168,11 @@ func (p *HolidaysPlugin) Prefetch() error {
|
|||||||
slog.Error("holidays: aladhan fetch failed", "err", err)
|
slog.Error("holidays: aladhan fetch failed", "err", err)
|
||||||
} else if islamicInfo != "" {
|
} else if islamicInfo != "" {
|
||||||
allHolidays = append(allHolidays, Holiday{
|
allHolidays = append(allHolidays, Holiday{
|
||||||
Name: "Islamic Date",
|
Name: "Islamic Date",
|
||||||
Description: islamicInfo,
|
Description: islamicInfo,
|
||||||
Country: "International",
|
Country: "International",
|
||||||
Type: "islamic-calendar",
|
Type: "islamic-calendar",
|
||||||
Date: dateStr,
|
Date: dateStr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,9 +471,9 @@ func (p *HolidaysPlugin) fetchCalendarific(date time.Time, country string) ([]Ho
|
|||||||
|
|
||||||
// Types to exclude — too regional to be interesting
|
// Types to exclude — too regional to be interesting
|
||||||
skipTypes := map[string]bool{
|
skipTypes := map[string]bool{
|
||||||
"Local holiday": true,
|
"Local holiday": true,
|
||||||
"Common local holiday": true,
|
"Common local holiday": true,
|
||||||
"Local observance": true,
|
"Local observance": true,
|
||||||
"Clock change/Daylight Saving Time": true,
|
"Clock change/Daylight Saving Time": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ type queueItem struct {
|
|||||||
// LLMPassivePlugin classifies messages using Ollama and reacts accordingly.
|
// LLMPassivePlugin classifies messages using Ollama and reacts accordingly.
|
||||||
type LLMPassivePlugin struct {
|
type LLMPassivePlugin struct {
|
||||||
Base
|
Base
|
||||||
xp *XPPlugin
|
xp *XPPlugin
|
||||||
dict *dreamclient.Client
|
dict *dreamclient.Client
|
||||||
ollamaHost string
|
ollamaHost string
|
||||||
ollamaModel string
|
ollamaModel string
|
||||||
sampleRate float64
|
sampleRate float64
|
||||||
|
|||||||
@@ -40,15 +40,15 @@ const (
|
|||||||
// MilkCartonPlugin generates milk carton style "missing person" images.
|
// MilkCartonPlugin generates milk carton style "missing person" images.
|
||||||
type MilkCartonPlugin struct {
|
type MilkCartonPlugin struct {
|
||||||
Base
|
Base
|
||||||
rateLimiter *RateLimitsPlugin
|
rateLimiter *RateLimitsPlugin
|
||||||
thresholdDays int
|
thresholdDays int
|
||||||
maxDays int
|
maxDays int
|
||||||
minMessages int
|
minMessages int
|
||||||
excludeUsers map[string]bool
|
excludeUsers map[string]bool
|
||||||
regularFont font.Face
|
regularFont font.Face
|
||||||
boldFont font.Face
|
boldFont font.Face
|
||||||
smallFont font.Face
|
smallFont font.Face
|
||||||
headerFont font.Face
|
headerFont font.Face
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMilkCartonPlugin creates a new milk carton plugin.
|
// NewMilkCartonPlugin creates a new milk carton plugin.
|
||||||
|
|||||||
@@ -53,13 +53,13 @@ type modConfig struct {
|
|||||||
RepeatSimilarityThreshold float64
|
RepeatSimilarityThreshold float64
|
||||||
|
|
||||||
// Mention flood
|
// Mention flood
|
||||||
MentionMax int
|
MentionMax int
|
||||||
MentionFloodCount int
|
MentionFloodCount int
|
||||||
MentionFloodWindowSeconds int
|
MentionFloodWindowSeconds int
|
||||||
|
|
||||||
// Link rate (new members)
|
// Link rate (new members)
|
||||||
LinkRateNewMember int
|
LinkRateNewMember int
|
||||||
LinkRateWindowSeconds int
|
LinkRateWindowSeconds int
|
||||||
|
|
||||||
// Invite flooding
|
// Invite flooding
|
||||||
InviteMaxPerHour int
|
InviteMaxPerHour int
|
||||||
@@ -123,16 +123,16 @@ func loadModConfig() modConfig {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
var dmTemplates = map[string]string{
|
var dmTemplates = map[string]string{
|
||||||
"strike1_word": "Hey — that word isn't something we allow in this community. Your message has been removed. This has been noted. Please keep it in mind going forward.",
|
"strike1_word": "Hey — that word isn't something we allow in this community. Your message has been removed. This has been noted. Please keep it in mind going forward.",
|
||||||
"strike1_flood": "Hey — looks like you might have gotten a bit trigger-happy there. Slow it down a little. Your messages have been removed and this has been noted.",
|
"strike1_flood": "Hey — looks like you might have gotten a bit trigger-happy there. Slow it down a little. Your messages have been removed and this has been noted.",
|
||||||
"strike1_wall": "Hey — that message was way too long for this room. It's been removed and noted. Please keep it shorter.",
|
"strike1_wall": "Hey — that message was way too long for this room. It's been removed and noted. Please keep it shorter.",
|
||||||
"strike1_repeat": "Hey — repeating the same message isn't cool. Your messages have been removed and this has been noted.",
|
"strike1_repeat": "Hey — repeating the same message isn't cool. Your messages have been removed and this has been noted.",
|
||||||
"strike1_mention": "Hey — too many mentions in a short time. Please don't mass-ping. This has been noted.",
|
"strike1_mention": "Hey — too many mentions in a short time. Please don't mass-ping. This has been noted.",
|
||||||
"strike1_link": "Hey — slow down on the links. New members have a limit. This has been noted.",
|
"strike1_link": "Hey — slow down on the links. New members have a limit. This has been noted.",
|
||||||
"strike1_invite": "Hey — you've been sending too many invites. This has been flagged and noted.",
|
"strike1_invite": "Hey — you've been sending too many invites. This has been flagged and noted.",
|
||||||
"strike2": "This is the second time we've had to step in. You've been muted for %s. When you're back, please keep the community guidelines in mind. This is your last warning before a permanent ban.",
|
"strike2": "This is the second time we've had to step in. You've been muted for %s. When you're back, please keep the community guidelines in mind. This is your last warning before a permanent ban.",
|
||||||
"strike3": "This is the third strike. You're being removed from the community permanently. Take care.",
|
"strike3": "This is the third strike. You're being removed from the community permanently. Take care.",
|
||||||
"manual_warn": "A community admin has issued you a formal warning: %s. Please keep this in mind.",
|
"manual_warn": "A community admin has issued you a formal warning: %s. Please keep this in mind.",
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -316,12 +316,12 @@ func petStateFromAdvChar(c *AdventureCharacter) PetState {
|
|||||||
// payment, autopay toggle, weekly tick, rate refresh), so the helper takes
|
// payment, autopay toggle, weekly tick, rate refresh), so the helper takes
|
||||||
// the whole struct rather than splitting per-field.
|
// the whole struct rather than splitting per-field.
|
||||||
type HouseState struct {
|
type HouseState struct {
|
||||||
Tier int
|
Tier int
|
||||||
LoanBalance int
|
LoanBalance int
|
||||||
LoanFrozen bool
|
LoanFrozen bool
|
||||||
MissedPayments int
|
MissedPayments int
|
||||||
Autopay bool
|
Autopay bool
|
||||||
CurrentRate float64
|
CurrentRate float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasHouse mirrors AdventureCharacter.HasHouse: a player counts as housed
|
// HasHouse mirrors AdventureCharacter.HasHouse: a player counts as housed
|
||||||
@@ -830,9 +830,9 @@ func upsertPlayerMetaLifecycleState(userID id.UserID, s LifecycleState) error {
|
|||||||
// loadLifecycleState returns lifecycle state from player_meta.
|
// loadLifecycleState returns lifecycle state from player_meta.
|
||||||
func loadLifecycleState(userID id.UserID) (LifecycleState, error) {
|
func loadLifecycleState(userID id.UserID) (LifecycleState, error) {
|
||||||
var (
|
var (
|
||||||
s LifecycleState
|
s LifecycleState
|
||||||
streakDecayed, actionTaken, holidayTaken int
|
streakDecayed, actionTaken, holidayTaken int
|
||||||
createdAt, lastActiveAt sql.NullTime
|
createdAt, lastActiveAt sql.NullTime
|
||||||
)
|
)
|
||||||
err := db.Get().QueryRow(
|
err := db.Get().QueryRow(
|
||||||
`SELECT current_streak, best_streak, last_action_date, streak_decayed,
|
`SELECT current_streak, best_streak, last_action_date, streak_decayed,
|
||||||
@@ -1038,9 +1038,9 @@ func deathStateFromAdvChar(c *AdventureCharacter) DeathState {
|
|||||||
// `saveAdvCharacter` alongside the L5e death state, since these fields
|
// `saveAdvCharacter` alongside the L5e death state, since these fields
|
||||||
// either rarely mutate or already mutate at every save site.
|
// either rarely mutate or already mutate at every save site.
|
||||||
type MiscState struct {
|
type MiscState struct {
|
||||||
Title string
|
Title string
|
||||||
TreasuresLocked bool
|
TreasuresLocked bool
|
||||||
CraftsSucceeded int
|
CraftsSucceeded int
|
||||||
}
|
}
|
||||||
|
|
||||||
func upsertPlayerMetaMiscState(userID id.UserID, s MiscState) error {
|
func upsertPlayerMetaMiscState(userID id.UserID, s MiscState) error {
|
||||||
|
|||||||
@@ -259,8 +259,8 @@ func TestScenario_MagicItemPlumbing(t *testing.T) {
|
|||||||
// in the slot the fix intended. Slots are baked into the generated
|
// in the slot the fix intended. Slots are baked into the generated
|
||||||
// data file by the importer's classifier; the lookup is a fixed table.
|
// data file by the importer's classifier; the lookup is a fixed table.
|
||||||
type slotCheck struct {
|
type slotCheck struct {
|
||||||
id string
|
id string
|
||||||
wantSlot DnDSlot
|
wantSlot DnDSlot
|
||||||
mustNotBeRingForSubstring string // sanity vs word-boundary regressions
|
mustNotBeRingForSubstring string // sanity vs word-boundary regressions
|
||||||
}
|
}
|
||||||
checks := []slotCheck{
|
checks := []slotCheck{
|
||||||
@@ -399,8 +399,8 @@ func TestScenario_SpellJargonRegression(t *testing.T) {
|
|||||||
bannedRegexes := []*regexp.Regexp{
|
bannedRegexes := []*regexp.Regexp{
|
||||||
regexp.MustCompile(`\bd(4|6|8|10|12|20|100)\b`),
|
regexp.MustCompile(`\bd(4|6|8|10|12|20|100)\b`),
|
||||||
regexp.MustCompile(`\b\d+d\d+\b`),
|
regexp.MustCompile(`\b\d+d\d+\b`),
|
||||||
regexp.MustCompile(`\bWhatever [a-z]`), // placeholder signature
|
regexp.MustCompile(`\bWhatever [a-z]`), // placeholder signature
|
||||||
regexp.MustCompile(`(?:\.\.\.|…)\s*$`), // trailing ellipsis
|
regexp.MustCompile(`(?:\.\.\.|…)\s*$`), // trailing ellipsis
|
||||||
regexp.MustCompile(`\b[a-z]{1,3}(?:\.\.\.|…)\s*$`), // truncated word
|
regexp.MustCompile(`\b[a-z]{1,3}(?:\.\.\.|…)\s*$`), // truncated word
|
||||||
regexp.MustCompile(`(?i)\bno larger than in any\b`),
|
regexp.MustCompile(`(?i)\bno larger than in any\b`),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ func NewSpaceInviterPlugin(client *mautrix.Client) *SpaceInviterPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SpaceInviterPlugin) Name() string { return "space_inviter" }
|
func (p *SpaceInviterPlugin) Name() string { return "space_inviter" }
|
||||||
func (p *SpaceInviterPlugin) Commands() []CommandDef { return nil }
|
func (p *SpaceInviterPlugin) Commands() []CommandDef { return nil }
|
||||||
func (p *SpaceInviterPlugin) OnReaction(ReactionContext) error { return nil }
|
func (p *SpaceInviterPlugin) OnReaction(ReactionContext) error { return nil }
|
||||||
|
|
||||||
func (p *SpaceInviterPlugin) Init() error {
|
func (p *SpaceInviterPlugin) Init() error {
|
||||||
|
|||||||
@@ -17,24 +17,24 @@ import (
|
|||||||
|
|
||||||
// finnhubQuote is the response from the Finnhub quote endpoint.
|
// finnhubQuote is the response from the Finnhub quote endpoint.
|
||||||
type finnhubQuote struct {
|
type finnhubQuote struct {
|
||||||
Current float64 `json:"c"` // Current price
|
Current float64 `json:"c"` // Current price
|
||||||
Change float64 `json:"d"` // Change
|
Change float64 `json:"d"` // Change
|
||||||
ChangePct float64 `json:"dp"` // Percent change
|
ChangePct float64 `json:"dp"` // Percent change
|
||||||
High float64 `json:"h"` // High price of the day
|
High float64 `json:"h"` // High price of the day
|
||||||
Low float64 `json:"l"` // Low price of the day
|
Low float64 `json:"l"` // Low price of the day
|
||||||
Open float64 `json:"o"` // Open price of the day
|
Open float64 `json:"o"` // Open price of the day
|
||||||
PrevClose float64 `json:"pc"` // Previous close price
|
PrevClose float64 `json:"pc"` // Previous close price
|
||||||
Timestamp int64 `json:"t"` // Timestamp
|
Timestamp int64 `json:"t"` // Timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
// finnhubProfile is the response from the Finnhub company profile endpoint.
|
// finnhubProfile is the response from the Finnhub company profile endpoint.
|
||||||
type finnhubProfile struct {
|
type finnhubProfile struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Ticker string `json:"ticker"`
|
Ticker string `json:"ticker"`
|
||||||
Exchange string `json:"exchange"`
|
Exchange string `json:"exchange"`
|
||||||
Industry string `json:"finnhubIndustry"`
|
Industry string `json:"finnhubIndustry"`
|
||||||
MarketCap float64 `json:"marketCapitalization"` // in millions
|
MarketCap float64 `json:"marketCapitalization"` // in millions
|
||||||
Currency string `json:"currency"`
|
Currency string `json:"currency"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// stockCacheEntry holds cached stock data.
|
// stockCacheEntry holds cached stock data.
|
||||||
|
|||||||
@@ -68,15 +68,15 @@ type openTDBResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type activeSession struct {
|
type activeSession struct {
|
||||||
RoomID id.RoomID
|
RoomID id.RoomID
|
||||||
QuestionEventID id.EventID
|
QuestionEventID id.EventID
|
||||||
Question string
|
Question string
|
||||||
CorrectAnswer string
|
CorrectAnswer string
|
||||||
AllAnswers []string
|
AllAnswers []string
|
||||||
CorrectIndex int
|
CorrectIndex int
|
||||||
StartedAt time.Time
|
StartedAt time.Time
|
||||||
Difficulty string
|
Difficulty string
|
||||||
Category string
|
Category string
|
||||||
}
|
}
|
||||||
|
|
||||||
// TriviaPlugin handles trivia game sessions.
|
// TriviaPlugin handles trivia game sessions.
|
||||||
|
|||||||
@@ -2039,5 +2039,3 @@ func (p *UnoPlugin) recordGame(game *unoGame, result string, potBefore float64)
|
|||||||
time.Now().UTC().Format("2006-01-02 15:04:05"),
|
time.Now().UTC().Format("2006-01-02 15:04:05"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,28 +39,28 @@ type unoMultiPlayer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type unoMultiGame struct {
|
type unoMultiGame struct {
|
||||||
id string
|
id string
|
||||||
roomID id.RoomID // games room
|
roomID id.RoomID // games room
|
||||||
ante float64
|
ante float64
|
||||||
players []*unoMultiPlayer // in turn order (includes bot)
|
players []*unoMultiPlayer // in turn order (includes bot)
|
||||||
currentIdx int
|
currentIdx int
|
||||||
direction int // +1 or -1
|
direction int // +1 or -1
|
||||||
drawPile []unoCard
|
drawPile []unoCard
|
||||||
discardTop unoCard
|
discardTop unoCard
|
||||||
topColor unoColor
|
topColor unoColor
|
||||||
phase unoMultiPhase
|
phase unoMultiPhase
|
||||||
drawnCard *unoCard // card drawn this turn
|
drawnCard *unoCard // card drawn this turn
|
||||||
pendingCard *unoCard // wild waiting for color
|
pendingCard *unoCard // wild waiting for color
|
||||||
|
|
||||||
// Wild Draw Four challenge state
|
// Wild Draw Four challenge state
|
||||||
wd4Player *unoMultiPlayer // who played the WD4
|
wd4Player *unoMultiPlayer // who played the WD4
|
||||||
wd4Victim *unoMultiPlayer // who can challenge
|
wd4Victim *unoMultiPlayer // who can challenge
|
||||||
wd4PrevColor unoColor // color before the wild was played
|
wd4PrevColor unoColor // color before the wild was played
|
||||||
turns int
|
turns int
|
||||||
turnID int // monotonic, used to invalidate stale timers
|
turnID int // monotonic, used to invalidate stale timers
|
||||||
startedAt time.Time
|
startedAt time.Time
|
||||||
done bool
|
done bool
|
||||||
bookDown bool
|
bookDown bool
|
||||||
|
|
||||||
// No Mercy mode
|
// No Mercy mode
|
||||||
noMercy bool
|
noMercy bool
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user