Review follow-up D: unify solo/party close-out effects

Both the solo (finishCombatSession) and party (finishPartyWin/Loss) close-outs
carried hand-copied lists of the same terminal effects. Item A drifted exactly
there. Hoist the effects into three shared helpers so the lists can't diverge:

- applyOwnerWinEffects: kill record + room threat + boss-defeat threat, once
  through the owner; returns bossOnExpedition.
- grantSeatWinXP: near-death calc + XP grant, per seat.
- endRunOnLoss: mood event (death only) + run/expedition teardown, shared by
  the Lost and Fled paths.

Player-facing text stays divergent (it legitimately differs) and the
roster-size death-on-win rule (item E) is untouched. Pure extract-and-call;
full plugin suite green.

Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
This commit is contained in:
prosolis
2026-07-10 08:50:15 -07:00
parent 91eeee0826
commit d7a5333048
2 changed files with 68 additions and 50 deletions

View File

@@ -354,28 +354,12 @@ func (p *AdventurePlugin) finishCombatSession(userID id.UserID, sess *CombatSess
var b strings.Builder
switch sess.Status {
case CombatStatusWon:
recordZoneKillForUser(userID, sess.EnemyID)
applyRoomCombatThreatForUser(userID, elite)
// zoneCombatXP only reads PlayerWon + NearDeath off the result.
nearDeath := sess.PlayerHPMax > 0 && sess.PlayerHP*5 < sess.PlayerHPMax
tier := 1
if run != nil {
tier = int(zone.Tier)
}
if xp := zoneCombatXP(CombatResult{PlayerWon: true, NearDeath: nearDeath}, monster.CR, tier); xp > 0 {
if _, err := p.grantDnDXP(userID, xp); err != nil {
slog.Error("combat: grantDnDXP turn-based", "user", userID, "err", err)
}
}
bossOnExpedition := false
if !elite {
// §8.1 — zone boss defeat drops expedition threat. Silent no-op
// for standalone zone runs (no active expedition).
if exp, eerr := getActiveExpedition(userID); eerr == nil && exp != nil {
_ = applyBossDefeatThreat(exp.ID)
bossOnExpedition = true
}
}
bossOnExpedition := p.applyOwnerWinEffects(userID, sess.EnemyID, elite)
p.grantSeatWinXP(userID, sess.PlayerHP, sess.PlayerHPMax, monster, tier)
if line := twinBeeLine(zone.ID, DMCombatEnd, sess.RunID, cadence); line != "" {
b.WriteString(line + "\n")
}
@@ -399,11 +383,7 @@ func (p *AdventurePlugin) finishCombatSession(userID id.UserID, sess *CombatSess
}
case CombatStatusLost:
if run != nil {
_, _ = applyMoodEvent(sess.RunID, MoodEventPlayerDeath)
}
_ = abandonZoneRun(userID)
forceExtractExpeditionForRunLoss(userID, "combat death")
endRunOnLoss(userID, sess.RunID, true)
markAdventureDead(userID, "zone", zone.Display)
if line := twinBeeLine(zone.ID, DMPlayerDeath, sess.RunID, cadence); line != "" {
b.WriteString(line + "\n")
@@ -414,8 +394,7 @@ func (p *AdventurePlugin) finishCombatSession(userID id.UserID, sess *CombatSess
// Flee = run ends, light penalty: wounds persist (HP already saved),
// but no death timer. Chosen candidate from the migration plan's
// open question on flee outcome.
_ = abandonZoneRun(userID)
forceExtractExpeditionForRunLoss(userID, "combat flee")
endRunOnLoss(userID, sess.RunID, false)
b.WriteString(fmt.Sprintf("🏃 You broke off from **%s** and slipped away. Run ended — you keep your wounds, but you live.", enemy.Name))
default: