From 8e3b8377c036c8828a74a86a9fb4fef4c5e234ad Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Tue, 28 Apr 2026 23:19:06 -0700 Subject: [PATCH] Adventure crafting: grant foraging XP per attempt, track lifetime crafts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Crafting was a downstream consumer of Foraging skill but never gave any XP back, so the feedback loop was one-directional — only gathering contributed to the level that gates better recipes. Now: each auto-craft attempt grants Foraging XP (success > failure). Per-tier values: tier 1: +12 / +3 tier 2: +25 / +5 tier 3: +40 / +8 tier 4: +60 / +12 tier 5: +90 / +18 Successful T1 ≈ 30% of a Foraging Success haul; T5 ≈ 40%. Failures get a small consolation grant — the attempt still produced practical knowledge (and lost ingredients). Schema: adventure_characters.crafts_succeeded INT for lifetime tracking. Migration entry included. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/db/db.go | 1 + internal/plugin/adventure_character.go | 13 ++++++++----- internal/plugin/adventure_consumables.go | 23 +++++++++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/internal/db/db.go b/internal/db/db.go index b55e7a5..b1a16ac 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -141,6 +141,7 @@ func runMigrations(d *sql.DB) error { `ALTER TABLE coop_dungeon_gifts ADD COLUMN expires_at DATETIME`, `ALTER TABLE coop_dungeon_gifts ADD COLUMN applied_at DATETIME`, `ALTER TABLE coop_dungeon_gifts ADD COLUMN stack_lead_id INTEGER`, + `ALTER TABLE adventure_characters ADD COLUMN crafts_succeeded INTEGER NOT NULL DEFAULT 0`, } for _, stmt := range columnMigrations { if _, err := d.Exec(stmt); err != nil { diff --git a/internal/plugin/adventure_character.go b/internal/plugin/adventure_character.go index 515ba42..98ff61c 100644 --- a/internal/plugin/adventure_character.go +++ b/internal/plugin/adventure_character.go @@ -95,6 +95,7 @@ type AdventureCharacter struct { PetMorningDefense bool AutoBabysit bool StreakDecayed bool + CraftsSucceeded int } type AdvEquipment struct { @@ -445,7 +446,7 @@ func loadAdvCharacter(userID id.UserID) (*AdventureCharacter, error) { pet_chased_away, pet_reactivated, pet_arrived, misty_encounter_count, misty_donated_count, thom_animal_line_fired, pet_supply_shop_unlocked, pet_level10_date, - pet_morning_defense, auto_babysit, streak_decayed + pet_morning_defense, auto_babysit, streak_decayed, crafts_succeeded FROM adventure_characters WHERE user_id = ?`, string(userID)).Scan( &c.UserID, &c.DisplayName, &c.CombatLevel, &c.MiningSkill, &c.ForagingSkill, &c.FishingSkill, @@ -470,7 +471,7 @@ func loadAdvCharacter(userID id.UserID) (*AdventureCharacter, error) { &petChasedAway, &petReactivated, &petArrived, &c.MistyEncounterCount, &c.MistyDonatedCount, &thomAnimalLine, &petSupplyUnlocked, &c.PetLevel10Date, - &petMorningDef, &autoBabysit, &streakDecayed, + &petMorningDef, &autoBabysit, &streakDecayed, &c.CraftsSucceeded, ) if err != nil { return nil, err @@ -638,7 +639,8 @@ func saveAdvCharacter(char *AdventureCharacter) error { thom_animal_line_fired = ?, pet_supply_shop_unlocked = ?, pet_level10_date = ?, pet_morning_defense = ?, auto_babysit = ?, - streak_decayed = ? + streak_decayed = ?, + crafts_succeeded = ? WHERE user_id = ?`, char.DisplayName, char.CombatLevel, char.MiningSkill, char.ForagingSkill, char.FishingSkill, char.CombatXP, char.MiningXP, char.ForagingXP, char.FishingXP, @@ -664,6 +666,7 @@ func saveAdvCharacter(char *AdventureCharacter) error { petMorningDef, autoBabysit, streakDecayed, + char.CraftsSucceeded, string(char.UserID), ) return err @@ -794,7 +797,7 @@ func loadAllAdvCharacters() ([]AdventureCharacter, error) { pet_chased_away, pet_reactivated, pet_arrived, misty_encounter_count, misty_donated_count, thom_animal_line_fired, pet_supply_shop_unlocked, pet_level10_date, - pet_morning_defense, auto_babysit, streak_decayed + pet_morning_defense, auto_babysit, streak_decayed, crafts_succeeded FROM adventure_characters`) if err != nil { return nil, err @@ -834,7 +837,7 @@ func loadAllAdvCharacters() ([]AdventureCharacter, error) { &petChasedAway, &petReactivated, &petArrived, &c.MistyEncounterCount, &c.MistyDonatedCount, &thomAnimalLine, &petSupplyUnlocked, &c.PetLevel10Date, - &petMorningDef, &autoBabysit, &streakDecayed, + &petMorningDef, &autoBabysit, &streakDecayed, &c.CraftsSucceeded, ); err != nil { return nil, err } diff --git a/internal/plugin/adventure_consumables.go b/internal/plugin/adventure_consumables.go index 970bb51..fe6d048 100644 --- a/internal/plugin/adventure_consumables.go +++ b/internal/plugin/adventure_consumables.go @@ -339,9 +339,19 @@ type CraftResult struct { Success bool } +// craftXPRewards: per-tier foraging XP granted on successful (and tiny on +// failed) auto-crafts. Successful T1 ≈ 30% of a Foraging Success haul, scaling +// up so T5 grants are meaningful. Failures get a token consolation grant — +// you tried. +var craftXPSuccess = map[int]int{1: 12, 2: 25, 3: 40, 4: 60, 5: 90} +var craftXPFailure = map[int]int{1: 3, 2: 5, 3: 8, 4: 12, 5: 18} + // autoCraftConsumables scans the player's inventory for craftable recipes and // attempts to craft the highest-tier recipe available. Consumes ingredients on // attempt; on failure one ingredient is lost. Returns crafted items and results. +// +// Side effects: grants foraging XP per attempt (success > failure) and bumps +// the player's CraftsSucceeded counter on each success. func autoCraftConsumables(userID id.UserID, items []AdvItem, foragingLevel int) ([]CraftResult, []AdvItem) { if foragingLevel < 10 { return nil, items @@ -393,6 +403,13 @@ func autoCraftConsumables(userID id.UserID, items []AdvItem, foragingLevel int) } results = append(results, CraftResult{Recipe: bestRecipe, Success: true}) crafted++ + // Foraging XP + lifetime counter bump. + if char, err := loadAdvCharacter(userID); err == nil { + char.ForagingXP += craftXPSuccess[bestRecipe.Tier] + char.CraftsSucceeded++ + checkAdvLevelUp(char, "foraging") + _ = saveAdvCharacter(char) + } } else { // Failure: all ingredients destroyed for _, id := range ingredientIDs { @@ -400,6 +417,12 @@ func autoCraftConsumables(userID id.UserID, items []AdvItem, foragingLevel int) } remaining = removeItemsByIDs(remaining, ingredientIDs) results = append(results, CraftResult{Recipe: bestRecipe, Success: false}) + // Token foraging XP for the attempt. + if char, err := loadAdvCharacter(userID); err == nil { + char.ForagingXP += craftXPFailure[bestRecipe.Tier] + checkAdvLevelUp(char, "foraging") + _ = saveAdvCharacter(char) + } } }