Adv 2.0 D&D Phase 12 E5: Extraction & Resume

Voluntary !extract burns 1 day, flips status to 'extracting', keeps
all rewards, resumable for 7 real days. Forced extraction (Abyss
collapse, future HP-0/supply-0 hooks) flips to 'abandoned' and applies
the §10.2 20% coin tax via the cycle layer's euro handle. !resume
within the 7-day window repurchases supplies and restores threat &
temporal stacks as-is; expired rows demote to failed.

getActiveExpedition / briefing / recap loaders narrow to status='active'
so 'extracting' rows don't get phantom day rollovers. Reuses
flavor.ExtractionVoluntary, ExtractionForced, ExpeditionResume from
twinbee_expedition_flavor.go.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-08 16:36:05 -07:00
parent a199efd773
commit d179ca27a7
8 changed files with 461 additions and 9 deletions

View File

@@ -105,7 +105,7 @@ func loadExpeditionsNeedingBriefing(threshold time.Time) ([]*Expedition, error)
xp_earned, coins_earned, gm_mood,
last_briefing_at, last_recap_at, last_activity, completed_at
FROM dnd_expedition
WHERE status IN ('active', 'extracting')
WHERE status = 'active'
AND start_date < ?
AND (last_briefing_at IS NULL OR last_briefing_at < ?)`,
threshold, threshold)
@@ -126,7 +126,7 @@ func loadExpeditionsNeedingRecap(threshold time.Time) ([]*Expedition, error) {
xp_earned, coins_earned, gm_mood,
last_briefing_at, last_recap_at, last_activity, completed_at
FROM dnd_expedition
WHERE status IN ('active', 'extracting')
WHERE status = 'active'
AND (last_recap_at IS NULL OR last_recap_at < ?)`, threshold)
if err != nil {
return nil, err
@@ -202,6 +202,17 @@ func (p *AdventurePlugin) deliverBriefing(e *Expedition, now time.Time) error {
// has advanced, so e.CurrentDay reflects the new day).
temporalLines := applyZoneTemporalPostRollover(e)
// E5b: if a temporal event forced extraction (abyss collapse, etc.),
// apply the §10.2 coin tax. The temporal layer flips the row to
// 'abandoned'; the cycle holds the euro handle to do the debit.
if e.Status == ExpeditionStatusAbandoned && p.euro != nil {
tax := int(float64(e.CoinsEarned) * forcedExtractCoinTaxFrac)
if tax > 0 {
p.euro.Debit(id.UserID(e.UserID), float64(tax),
"forced extraction tax")
}
}
line := pickMorningBriefing(e.CurrentDay)
body := renderMorningBriefing(e, line, burn)
if restSummary != "" {