Adv 2.0 D&D layer: Phases 1-8 + Phase 9 SP1+SP2

Combines all uncommitted D&D work into one checkpoint:

Phases 1-8 (per gogobee_dnd_session_summary.md):
- Race/class/stats system, !setup flow, !sheet, !respec
- d20-vs-AC combat with race/class passives, auto-migration
- XP/level-up, skill checks, NPC refund hooks
- Equipment slot mapping, rarity, !rest short/long
- Pre-arm active abilities, resource pools
- Bestiary, !roll/!stats/!level, onboarding DM
- Audit fixes A-I, flavor wiring under internal/flavor/
- Phase 8 weapon dice + armor AC tables (gogobee_equipment_appendix)

Phase 9 SP1 — spell system foundations:
- 79 SpellDefinitions (76 in-scope + 3 reaction-deferred)
- dnd_known_spells, dnd_spell_slots tables
- pending_cast / concentration_* columns on dnd_character
- Slot tables for Mage/Cleric/Ranger, DC + attack-bonus math
- Long-rest refreshes slots; respec wipes spell state
- Auto-grant default known list on !setup confirm and auto-migration

Phase 9 SP2 — !cast / !spells / !prepare commands:
- Out-of-combat HEAL and UTILITY resolve immediately
- Damage/control/buff queue as pending_cast for next combat
- Audit-style save-then-debit, slot refund on save failure
- !cast --drop, concentration supersession, prep-cap enforcement
- Reaction spells refused with Phase 11 note

SP3 (combat-time resolution of pending_cast) and SP4 (full prep/learn
tests) are next. Build clean, full test suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-08 08:21:44 -07:00
parent 8e0fe0230c
commit 9e1a1f606c
80 changed files with 19088 additions and 210 deletions

View File

@@ -94,6 +94,8 @@ type AdventureCharacter struct {
PetLevel10Date string
PetMorningDefense bool
AutoBabysit bool
AutoBabysitFocus string // mining|fishing|foraging — preferred skill for auto-babysit; "" defaults to weakest
TreasuresLocked bool // when true, treasure drops at cap are refused instead of auto-swapping
StreakDecayed bool
CraftsSucceeded int
DeathSource string // "adventure" | "arena" | "" (legacy/unknown — treated as adventure)
@@ -378,7 +380,17 @@ func xpToNextLevel(level int) int {
// checkAdvLevelUp checks if a character leveled up in the given skill and applies it.
// Returns whether a level-up occurred and the new level.
//
// Combat is special-cased: once a player has confirmed a D&D character,
// combat_level freezes. dnd_level (driven by dnd_xp via grantDnDXP) is
// canonical going forward. Combat XP still accrues into combat_xp for
// historical display, but never causes combat_level to advance.
// Skill levels (mining/foraging/fishing) are unaffected and continue to
// progress on their own track per v1.1 §4.
func checkAdvLevelUp(char *AdventureCharacter, skill string) (bool, int) {
if skill == "combat" && HasCompletedSetup(char.UserID) {
return false, char.CombatLevel
}
var xp *int
var level *int
switch skill {
@@ -426,7 +438,7 @@ func loadAdvCharacter(userID id.UserID) (*AdventureCharacter, error) {
var houseFrozen, houseAutopay int
var petChasedAway, petReactivated, petArrived, thomAnimalLine, petSupplyUnlocked, petMorningDef int
var autoBabysit, streakDecayed int
var autoBabysit, streakDecayed, treasuresLocked int
err := d.QueryRow(`
SELECT user_id, display_name,
@@ -453,7 +465,7 @@ func loadAdvCharacter(userID id.UserID) (*AdventureCharacter, error) {
misty_encounter_count, misty_donated_count,
thom_animal_line_fired, pet_supply_shop_unlocked, pet_level10_date,
pet_morning_defense, auto_babysit, streak_decayed, crafts_succeeded,
death_source, death_location
death_source, death_location, auto_babysit_focus, treasures_locked
FROM adventure_characters WHERE user_id = ?`, string(userID)).Scan(
&c.UserID, &c.DisplayName,
&c.CombatLevel, &c.MiningSkill, &c.ForagingSkill, &c.FishingSkill,
@@ -479,7 +491,7 @@ func loadAdvCharacter(userID id.UserID) (*AdventureCharacter, error) {
&c.MistyEncounterCount, &c.MistyDonatedCount,
&thomAnimalLine, &petSupplyUnlocked, &c.PetLevel10Date,
&petMorningDef, &autoBabysit, &streakDecayed, &c.CraftsSucceeded,
&c.DeathSource, &c.DeathLocation,
&c.DeathSource, &c.DeathLocation, &c.AutoBabysitFocus, &treasuresLocked,
)
if err != nil {
return nil, err
@@ -491,6 +503,7 @@ func loadAdvCharacter(userID id.UserID) (*AdventureCharacter, error) {
c.BabysitActive = babysitAct == 1
c.PetMorningDefense = petMorningDef == 1
c.AutoBabysit = autoBabysit == 1
c.TreasuresLocked = treasuresLocked == 1
c.StreakDecayed = streakDecayed == 1
if deadUntil.Valid {
c.DeadUntil = &deadUntil.Time
@@ -650,7 +663,9 @@ func saveAdvCharacter(char *AdventureCharacter) error {
streak_decayed = ?,
crafts_succeeded = ?,
death_source = ?,
death_location = ?
death_location = ?,
auto_babysit_focus = ?,
treasures_locked = ?
WHERE user_id = ?`,
char.DisplayName, char.CombatLevel, char.MiningSkill, char.ForagingSkill, char.FishingSkill,
char.CombatXP, char.MiningXP, char.ForagingXP, char.FishingXP,
@@ -678,11 +693,20 @@ func saveAdvCharacter(char *AdventureCharacter) error {
streakDecayed,
char.CraftsSucceeded,
char.DeathSource, char.DeathLocation,
char.AutoBabysitFocus,
boolToInt(char.TreasuresLocked),
string(char.UserID),
)
return err
}
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}
func loadAdvEquipment(userID id.UserID) (map[EquipmentSlot]*AdvEquipment, error) {
d := db.Get()
rows, err := d.Query(`
@@ -809,7 +833,7 @@ func loadAllAdvCharacters() ([]AdventureCharacter, error) {
misty_encounter_count, misty_donated_count,
thom_animal_line_fired, pet_supply_shop_unlocked, pet_level10_date,
pet_morning_defense, auto_babysit, streak_decayed, crafts_succeeded,
death_source, death_location
death_source, death_location, auto_babysit_focus, treasures_locked
FROM adventure_characters`)
if err != nil {
return nil, err
@@ -824,7 +848,7 @@ func loadAllAdvCharacters() ([]AdventureCharacter, error) {
var mistyLastSeen, arinaLastSeen, mistyBuffExp, mistyDebuffExp, arinaBuffExp sql.NullTime
var houseFrozen, houseAutopay int
var petChasedAway, petReactivated, petArrived, thomAnimalLine, petSupplyUnlocked, petMorningDef int
var autoBabysit, streakDecayed int
var autoBabysit, streakDecayed, treasuresLocked int
if err := rows.Scan(
&c.UserID, &c.DisplayName,
&c.CombatLevel, &c.MiningSkill, &c.ForagingSkill, &c.FishingSkill,
@@ -850,7 +874,7 @@ func loadAllAdvCharacters() ([]AdventureCharacter, error) {
&c.MistyEncounterCount, &c.MistyDonatedCount,
&thomAnimalLine, &petSupplyUnlocked, &c.PetLevel10Date,
&petMorningDef, &autoBabysit, &streakDecayed, &c.CraftsSucceeded,
&c.DeathSource, &c.DeathLocation,
&c.DeathSource, &c.DeathLocation, &c.AutoBabysitFocus, &treasuresLocked,
); err != nil {
return nil, err
}
@@ -861,6 +885,7 @@ func loadAllAdvCharacters() ([]AdventureCharacter, error) {
c.BabysitActive = babysitAct == 1
c.PetMorningDefense = petMorningDef == 1
c.AutoBabysit = autoBabysit == 1
c.TreasuresLocked = treasuresLocked == 1
c.StreakDecayed = streakDecayed == 1
c.HouseLoanFrozen = houseFrozen == 1
c.HouseAutopay = houseAutopay == 1