Adv 2.0 Phase L1: Babysit pivot + legacy resolver retire

Babysit pivots from harvest service to pet-care + safe-rest perk:
- runBabysitDaily/runAutoBabysitDay/runBabysitAction deleted; replaced
  by runBabysitDailyTrickle (3 pet XP/day while subscribed).
- BabysitSafeRest predicate promotes Standard camps to Fortified-tier
  rest (HP+1d6, threat -5, resources refresh) and reduces wandering
  monster check campMod from 0 to -4. Hooks live in
  dnd_expedition_camp.go and dnd_expedition_night.go.
- !adventure babysit auto/focus subcommands removed; status/purchase
  DMs reflect the new perks.

Scheduler drops the auto-babysit fallback block (no more silent
debits + harvest-day for missed logins). Babysit subscribers still
skip the morning DM; trickle runs in the background.

TwinBee daily simulator self-contained: simulateTwinBeeOutcome +
simulateTwinBeeLoot replace the resolveAdvAction call against a
fake CombatLevel=35 character. Outcome distribution and reward
shape preserved; gold-share to active players unchanged.

Legacy activity resolver retired: resolveAdvAction,
advEligibleLocations, AdvEligibleLocation, resolveAdvEmptyOutcome
deleted from adventure_activities.go (zero production callers
post-babysit/twinbee). AdvActionResult kept — combat_bridge still
produces it for arena/dungeon flows; dies in L2/L3.

Migration plan doc gogobee_legacy_migration.md added: five-phase
L1-L5 plan covering arena (Adv 2.0 boss flow rebuild), co-op
deletion, perk-gate sweep, and final teardown of AdventureCharacter
+ CombatLevel + combat_engine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-09 00:10:21 -07:00
parent 45863bd5f3
commit 1953eec3b5
10 changed files with 743 additions and 781 deletions

View File

@@ -779,173 +779,6 @@ func advEquipmentMasteryBonus(equip map[EquipmentSlot]*AdvEquipment) float64 {
return total
}
func resolveAdvAction(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEquipment, loc *AdvLocation, bonuses *AdvBonusSummary, inPenaltyZone bool) *AdvActionResult {
result := &AdvActionResult{
Location: loc,
XPSkill: advXPSkill(loc.Activity),
}
// Apply equipment-mastery loot bonus on a local copy so we don't mutate
// the caller's bonuses struct (it's reused across multiple actions).
localBonuses := *bonuses
localBonuses.LootQuality += advEquipmentMasteryBonus(equip)
bonuses = &localBonuses
probs := calculateAdvProbabilities(char, equip, loc, bonuses, inPenaltyZone)
// Overlevel penalty — reduces loot and XP for farming low-tier content
skillLevel := advEffectiveSkill(char, loc.Activity, bonuses)
overlevelMult := advOverlevelMultiplier(skillLevel, loc)
// Roll outcome
roll := rand.Float64() * 100
switch {
case roll < probs.DeathPct:
result.Outcome = AdvOutcomeDeath
case roll < probs.DeathPct+probs.EmptyPct:
// Activity-specific empty outcomes
result.Outcome = resolveAdvEmptyOutcome(loc, roll)
case roll < probs.DeathPct+probs.EmptyPct+probs.SuccessPct:
result.Outcome = AdvOutcomeSuccess
default:
result.Outcome = AdvOutcomeExceptional
}
// Near-death check: survived within 2% of death threshold
if result.Outcome != AdvOutcomeDeath && roll < probs.DeathPct+2 && roll >= probs.DeathPct {
result.NearDeath = true
}
// Generate loot for success/exceptional
if result.Outcome == AdvOutcomeSuccess || result.Outcome == AdvOutcomeExceptional {
result.LootItems = generateAdvLoot(loc, result.Outcome == AdvOutcomeExceptional, bonuses.LootQuality)
// Apply overlevel penalty to loot values
if overlevelMult < 1.0 {
for i := range result.LootItems {
result.LootItems[i].Value = max(1, int64(float64(result.LootItems[i].Value)*overlevelMult))
}
}
for _, item := range result.LootItems {
result.TotalLootValue += item.Value
}
}
// XP calculation
xp := advXPForOutcome(loc.Activity, loc.Tier, result.Outcome)
if result.Outcome == AdvOutcomeSuccess || result.Outcome == AdvOutcomeExceptional {
xp = advXPTable[loc.Activity][loc.Tier].Success
if result.Outcome == AdvOutcomeExceptional {
xp = advXPTable[loc.Activity][loc.Tier].Exceptional
}
}
xpResult := applyXPBonuses(XPBonusParams{
BaseXP: xp,
NearDeath: result.NearDeath,
BonusMult: bonuses.XPMultiplier,
Ironclad: advEquippedArenaSets(equip)["ironclad"],
OverlevelMult: overlevelMult,
})
result.XPGained = xpResult.Total
result.XPBreakdown = xpResult.Breakdown
// Equipment degradation on bad outcomes
if result.Outcome == AdvOutcomeDeath || result.Outcome == AdvOutcomeEmpty ||
result.Outcome == AdvOutcomeCaveIn || result.Outcome == AdvOutcomeBear ||
result.Outcome == AdvOutcomeRiver {
result.EquipDamage = applyAdvEquipDegradation(equip, result.Outcome)
result.EquipBroken = advCheckBrokenEquipment(equip)
}
// Increment actions_used for equipment mastery only on slots relevant
// to the activity — a foraging trip shouldn't master a sword, and a
// dungeon run shouldn't master a pickaxe. Detect threshold crossings
// so the caller can DM a celebration.
for slot, eq := range equip {
if eq == nil {
continue
}
if !advSlotRelevantToActivity(slot, loc.Activity) {
continue
}
before := eq.ActionsUsed
eq.ActionsUsed++
for _, t := range advMasteryThresholds {
if before < t && eq.ActionsUsed >= t {
result.MasteryCrossings = append(result.MasteryCrossings, AdvMasteryCrossing{
Slot: slot,
ItemName: eq.Name,
Threshold: t,
})
}
}
}
return result
}
// resolveAdvEmptyOutcome returns an activity-specific "empty" outcome.
func resolveAdvEmptyOutcome(loc *AdvLocation, _ float64) AdvOutcomeType {
switch loc.Activity {
case AdvActivityMining:
// 40% chance of cave-in on "empty" result
if rand.Float64() < 0.4 {
return AdvOutcomeCaveIn
}
return AdvOutcomeEmpty
case AdvActivityForaging:
// Split empty into specific outcomes
r := rand.Float64()
switch {
case r < 0.35:
return AdvOutcomeHornets
case r < 0.55:
return AdvOutcomeBear
case r < 0.70:
return AdvOutcomeRiver
default:
return AdvOutcomeEmpty
}
case AdvActivityFishing:
// Fishing empty is just empty — no sub-outcomes
return AdvOutcomeEmpty
default:
return AdvOutcomeEmpty
}
}
// ── Eligible Locations for DM Menu ───────────────────────────────────────────
type AdvEligibleLocation struct {
Location *AdvLocation
InPenaltyZone bool
DeathPct float64
ExceptionalPct float64
}
func advEligibleLocations(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEquipment, activity AdvActivityType, bonuses *AdvBonusSummary) []AdvEligibleLocation {
var eligible []AdvEligibleLocation
for _, loc := range allAdvLocations(activity) {
loc := loc
ok, penalty := advIsEligible(char, equip, &loc, bonuses)
if !ok {
continue
}
probs := calculateAdvProbabilities(char, equip, &loc, bonuses, penalty)
eligible = append(eligible, AdvEligibleLocation{
Location: &loc,
InPenaltyZone: penalty,
DeathPct: probs.DeathPct,
ExceptionalPct: probs.ExceptionalPct,
})
}
return eligible
}
// ── Party Bonus Check ────────────────────────────────────────────────────────
// advCheckPartyBonus checks if other players visited the same location today.