mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
Review follow-ups A + B: armed abilities survive the fight, supply pool serialized
A. An armed ability lasted one round of a turn-based fight.
buildZoneCombatants called applyArmedAbility, which applies an ability's mods
*and* clears ArmedAbility and saves the sheet. The turn engine calls that
builder again on every !attack / !cast / !consume, so round 1 fired the ability
and disarmed the character, and every later round rebuilt them with none of its
mods. A Berserker paid stamina for a single round of BerserkerRage /
RageMeleeDmg / PhysicalResistRage / FrenzyDmgBonus. Every entry in
dndActiveAbilities had the same shape. mods.BerserkerRage was not merely unread
at close-out — by then it no longer existed.
Split arming into its two halves:
consumeArmedAbility(c) mutates: disarms, saves, returns the id. Once,
at fight start.
applyAbilityByID(c, id, mods) pure: no DB write, no disarm. Safe on every
rebuild. (No ability's Apply writes to the
character, so this really is pure.)
armAbilityForFight(c, mods) consume + apply, for the auto-resolve callers
that build and fight in one breath.
buildZoneCombatants now takes the already-consumed id and re-applies it. The id
rides on ActorStatuses.ArmedAbility, seeded per seat at fight start, so
partyCombatantsForSession reproduces the ability every rebuild and the close-out
can still see that a rage fired.
The close-out itself: postCombatBookkeeping now carries grantCombatAchievements
+ persistDnDPostCombatSubclass, and all four close-outs route through it —
runDungeonCombat, runZoneCombatRoster, finishCombatSession,
finishPartyCombatSession. It fires on every terminal status, not just a win: a
Berserker who rages and loses is still exhausted, which is what auto-resolve
always did.
Also: buildFightSeats and runZoneCombatRoster consumed the ability before the
checks that could sit a seat out, so a downed member was disarmed for a fight
they never joined. The refusals now run first.
B. Six unlocked read-modify-writes against the shared supply pool.
updateSupplies rewrites supplies_json wholesale, so a caller folding its delta
onto an *Expedition it read earlier discards whatever landed in between.
Handlers run one goroutine per event, so those writers genuinely interleave.
All six now go through withExpeditionSupplies, which takes advExpeditionLock,
re-reads the row, hands the closure the fresh copy and persists what it returns:
nightRolloverBurn (forage + burn in one write), grantTwoWeeksCache,
advanceToNextRegion's transit burn, campPitch, pitchAutopilotCamp, and the
ambient pack-rat drain. expeditionCmdAccept's hand-rolled lock folds onto the
same helper. expedition_sim.go is left alone: single-threaded, takes no locks.
Known consequence, for the balance track: trySimAutoArm used to live inside the
rebuild, so a simulated Fighter (second_wind) or Cleric (healing_word) re-armed
and re-spent a resource every round of every elite/boss fight. expedition-sim
drives those through the turn engine, so every prior expedition-sim corpus
overstates those two classes. Re-baseline after this, not before.
Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
This commit is contained in:
@@ -82,34 +82,39 @@ type nightRolloverResult struct {
|
||||
// to finish the rollover; legacy deliverBriefing interleaves processOvernightCamp
|
||||
// between the two so a fortified camp's −5 lands before drift's +3.
|
||||
func (p *AdventurePlugin) nightRolloverBurn(e *Expedition) (float32, error) {
|
||||
// D5-c: Ranger forage runs before the daily burn so the +SU lands on
|
||||
// today's supplies, not tomorrow's. Logged so the end-of-day digest
|
||||
// can surface the gain; pure no-op for non-Ranger characters.
|
||||
if c, err := LoadDnDCharacter(id.UserID(e.UserID)); err == nil && c != nil {
|
||||
if gain := applyRangerForage(e, c, nil); gain > 0 {
|
||||
_ = appendExpeditionLog(e.ID, e.CurrentDay, "forage",
|
||||
fmt.Sprintf("ranger forage +%g SU", gain),
|
||||
flavor.Pick(flavor.HarvestForageSuccess))
|
||||
}
|
||||
}
|
||||
burnOverride := applyZoneTemporalPreBurn(e, e.CurrentDay+1)
|
||||
var newSupplies ExpeditionSupplies
|
||||
var burn float32
|
||||
if burnOverride.Multiplier > 0 {
|
||||
// The temporal override replaces the harsh/siege multiplier, not the
|
||||
// roster's: a party still eats N × 0.8 rations inside a time-warped zone.
|
||||
burn = e.Supplies.DailyBurn * burnOverride.Multiplier * float32(expeditionBurnRatePct(e.ID)) / 100
|
||||
newSupplies = e.Supplies
|
||||
newSupplies.Current -= burn
|
||||
if newSupplies.Current < 0 {
|
||||
newSupplies.Current = 0
|
||||
// Forage and burn land in one write, so they resolve together against the
|
||||
// pool as it stands now — not as `e` last saw it.
|
||||
newSupplies, err := p.withExpeditionSupplies(e.ID, func(fresh *Expedition) (ExpeditionSupplies, error) {
|
||||
// D5-c: Ranger forage runs before the daily burn so the +SU lands on
|
||||
// today's supplies, not tomorrow's. Logged so the end-of-day digest
|
||||
// can surface the gain; pure no-op for non-Ranger characters.
|
||||
if c, cerr := LoadDnDCharacter(id.UserID(fresh.UserID)); cerr == nil && c != nil {
|
||||
if gain := applyRangerForage(fresh, c, nil); gain > 0 {
|
||||
_ = appendExpeditionLog(fresh.ID, fresh.CurrentDay, "forage",
|
||||
fmt.Sprintf("ranger forage +%g SU", gain),
|
||||
flavor.Pick(flavor.HarvestForageSuccess))
|
||||
}
|
||||
}
|
||||
newSupplies.ForagedToday = false
|
||||
} else {
|
||||
harsh := e.ThreatLevel > 60 || zoneTemporalHarsh(e)
|
||||
newSupplies, burn = applyExpeditionDailyBurn(e, harsh, e.SiegeMode)
|
||||
}
|
||||
if err := updateSupplies(e.ID, newSupplies); err != nil {
|
||||
burnOverride := applyZoneTemporalPreBurn(fresh, fresh.CurrentDay+1)
|
||||
if burnOverride.Multiplier > 0 {
|
||||
// The temporal override replaces the harsh/siege multiplier, not the
|
||||
// roster's: a party still eats N × 0.8 rations inside a time-warped zone.
|
||||
burn = fresh.Supplies.DailyBurn * burnOverride.Multiplier * float32(expeditionBurnRatePct(fresh.ID)) / 100
|
||||
next := fresh.Supplies
|
||||
next.Current -= burn
|
||||
if next.Current < 0 {
|
||||
next.Current = 0
|
||||
}
|
||||
next.ForagedToday = false
|
||||
return next, nil
|
||||
}
|
||||
harsh := fresh.ThreatLevel > 60 || zoneTemporalHarsh(fresh)
|
||||
next, b := applyExpeditionDailyBurn(fresh, harsh, fresh.SiegeMode)
|
||||
burn = b
|
||||
return next, nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := advanceExpeditionDay(e.ID); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user