mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +00:00
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:
@@ -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:
|
||||
|
||||
@@ -65,8 +65,7 @@ func (p *AdventurePlugin) finishPartyCombatSession(ct *combatTurn) []string {
|
||||
case CombatStatusLost:
|
||||
return p.finishPartyLoss(ct, zone, cadence)
|
||||
case CombatStatusFled:
|
||||
_ = abandonZoneRun(owner)
|
||||
forceExtractExpeditionForRunLoss(owner, "combat flee")
|
||||
endRunOnLoss(owner, sess.RunID, false)
|
||||
return p.eachSeat(ct, fmt.Sprintf(
|
||||
"🏃 The party broke off from **%s** and slipped away. Run ended — you keep your wounds, but you live.", enemy.Name))
|
||||
default:
|
||||
@@ -84,16 +83,7 @@ func (p *AdventurePlugin) finishPartyWin(
|
||||
sess := ct.sess
|
||||
owner := id.UserID(sess.UserID)
|
||||
|
||||
recordZoneKillForUser(owner, sess.EnemyID)
|
||||
applyRoomCombatThreatForUser(owner, elite)
|
||||
|
||||
bossOnExpedition := false
|
||||
if !elite {
|
||||
if exp, eerr := getActiveExpedition(owner); eerr == nil && exp != nil {
|
||||
_ = applyBossDefeatThreat(exp.ID)
|
||||
bossOnExpedition = true
|
||||
}
|
||||
}
|
||||
bossOnExpedition := p.applyOwnerWinEffects(owner, sess.EnemyID, elite)
|
||||
|
||||
tier := 1
|
||||
if run != nil {
|
||||
@@ -110,14 +100,8 @@ func (p *AdventurePlugin) finishPartyWin(
|
||||
uid := id.UserID(sess.seatUserID(seat))
|
||||
hp, hpMax := sess.seatHP(seat), sess.seatHPMax(seat)
|
||||
|
||||
// A member who went down before the killing blow still earns the kill —
|
||||
// but at a fifth of their pool or less, the XP path calls it near-death.
|
||||
nearDeath := hpMax > 0 && hp*5 < hpMax
|
||||
if xp := zoneCombatXP(CombatResult{PlayerWon: true, NearDeath: nearDeath}, monster.CR, tier); xp > 0 {
|
||||
if _, err := p.grantDnDXP(uid, xp); err != nil {
|
||||
slog.Error("combat: party grantDnDXP", "user", uid, "err", err)
|
||||
}
|
||||
}
|
||||
// A member who went down before the killing blow still earns the kill.
|
||||
p.grantSeatWinXP(uid, hp, hpMax, monster, tier)
|
||||
|
||||
var b strings.Builder
|
||||
if shared != "" && seat == 0 {
|
||||
@@ -156,11 +140,7 @@ func (p *AdventurePlugin) finishPartyLoss(ct *combatTurn, zone ZoneDefinition, c
|
||||
sess := ct.sess
|
||||
owner := id.UserID(sess.UserID)
|
||||
|
||||
if run, _ := getZoneRun(sess.RunID); run != nil {
|
||||
_, _ = applyMoodEvent(sess.RunID, MoodEventPlayerDeath)
|
||||
}
|
||||
_ = abandonZoneRun(owner)
|
||||
forceExtractExpeditionForRunLoss(owner, "combat death")
|
||||
endRunOnLoss(owner, sess.RunID, true)
|
||||
|
||||
line := twinBeeLine(zone.ID, DMPlayerDeath, sess.RunID, cadence)
|
||||
out := make([]string, sess.RosterSize())
|
||||
@@ -184,3 +164,62 @@ func (p *AdventurePlugin) eachSeat(ct *combatTurn, block string) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ── Shared terminal effects (item D) ─────────────────────────────────────────
|
||||
//
|
||||
// finishCombatSession (solo, combat_cmd.go) and finishPartyWin/finishPartyLoss
|
||||
// (party, above) still write their own player-facing text — the solo line says
|
||||
// "You finished", the party line says "The party stands", and the death-on-win
|
||||
// rule is deliberately roster-size-dependent (see item E). But the *effects*
|
||||
// they fire were hand-copied lists, and item A drifted exactly there. These
|
||||
// helpers are the single copy of each effect; both close-outs route through
|
||||
// them so the effect lists cannot diverge again.
|
||||
|
||||
// applyOwnerWinEffects fires the terminal effects a win owes the run and
|
||||
// expedition — the zone-kill record, room threat, and the boss-defeat threat
|
||||
// drop — once, through the row's owner (a party member owns neither row).
|
||||
// Returns whether the kill was a zone boss on an active expedition; both
|
||||
// close-outs use it to reframe the final line as the expedition's climax.
|
||||
func (p *AdventurePlugin) applyOwnerWinEffects(owner id.UserID, enemyID string, elite bool) (bossOnExpedition bool) {
|
||||
recordZoneKillForUser(owner, enemyID)
|
||||
applyRoomCombatThreatForUser(owner, elite)
|
||||
if !elite {
|
||||
// §8.1 — a zone boss defeat drops expedition threat. Silent no-op for a
|
||||
// standalone zone run with no active expedition.
|
||||
if exp, eerr := getActiveExpedition(owner); eerr == nil && exp != nil {
|
||||
_ = applyBossDefeatThreat(exp.ID)
|
||||
bossOnExpedition = true
|
||||
}
|
||||
}
|
||||
return bossOnExpedition
|
||||
}
|
||||
|
||||
// grantSeatWinXP pays one combatant their kill XP. A seat that finished at a
|
||||
// fifth of its pool or less takes the near-death path. Full XP per person,
|
||||
// solo or party, per the accessibility-over-crunch stance.
|
||||
func (p *AdventurePlugin) grantSeatWinXP(uid id.UserID, hp, hpMax int, monster DnDMonsterTemplate, tier int) {
|
||||
nearDeath := hpMax > 0 && hp*5 < hpMax
|
||||
if xp := zoneCombatXP(CombatResult{PlayerWon: true, NearDeath: nearDeath}, monster.CR, tier); xp > 0 {
|
||||
if _, err := p.grantDnDXP(uid, xp); err != nil {
|
||||
slog.Error("combat: grantDnDXP", "user", uid, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endRunOnLoss tears down the owner's run and wrapping expedition when a fight
|
||||
// ends in death or flight. On a death (not a flee) it also stamps the run's
|
||||
// mood event. Owner-scoped, fires once; the per-seat death mark stays with the
|
||||
// caller, since it is decided per HP and gated on roster size.
|
||||
func endRunOnLoss(owner id.UserID, runID string, death bool) {
|
||||
if death {
|
||||
if run, _ := getZoneRun(runID); run != nil {
|
||||
_, _ = applyMoodEvent(runID, MoodEventPlayerDeath)
|
||||
}
|
||||
}
|
||||
_ = abandonZoneRun(owner)
|
||||
reason := "combat flee"
|
||||
if death {
|
||||
reason = "combat death"
|
||||
}
|
||||
forceExtractExpeditionForRunLoss(owner, reason)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user