Death source tracking + combat threat/jitter fixes

- Adventure characters now record DeathSource ("adventure"|"arena") and
  DeathLocation when killed. Threaded through Kill() and transitionDeath.
  Daily report uses the recorded death location instead of misattributing
  arena deaths to the day's adventure log: when DeathSource != "adventure",
  the adventure block shows the alive icon and a separate "Later fell in
  {location}." line. Standout-loss flavor uses DeathLocation. Empty
  DeathSource (legacy rows) falls back to current behavior.
- calcDamage applies a ±15% per-hit jitter, so successive hits in a phase
  no longer produce identical numbers. Previously every hit in a phase was
  flat (e.g. 17, 17, 17, 17) and mirror-symmetric between player/enemy,
  reading as scripted.
- assessThreat rewritten to use the engine's actual penetration formula:
  damage-per-round each direction, then rounds-to-kill ratio. The old
  additive HP+Attack*3 model ignored Defense, so even fights could be
  rated trivial — players died after consumables were skipped with the
  "threat assessed as manageable" message.
- SelectConsumables never skips on trivial when arenaRound > 0. Arena
  losses cost real money and equipment, so the cost of a wrong assessment
  is too high to gamble on; adventure-side fights still skip trivial
  threats to avoid wasting items on chump enemies.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-03 11:42:00 -07:00
parent a00987e75c
commit 8e0fe0230c
9 changed files with 93 additions and 19 deletions

View File

@@ -540,6 +540,8 @@ type AdvPlayerDaySummary struct {
IsResting bool
SummaryLine string
HolidayActions int // 0 = not holiday or no action; 1 = took one; 2 = took both
DeathSource string
DeathLocation string
}
func renderAdvDailySummary(date string, tb *TwinBeeResult, tbRewards TwinBeeRewardSummary, players []AdvPlayerDaySummary, holidayName string) string {
@@ -611,12 +613,29 @@ func renderAdvDailySummary(date string, tb *TwinBeeResult, tbRewards TwinBeeRewa
p := &players[i]
if p.IsDead {
dead = append(dead, *p)
// Dead players who acted today still show in the main section
// Dead players who acted today still show in the main section.
// If they died OUTSIDE the adventure (e.g. Arena), the adventure
// itself was successful — show the alive icon for that block and
// append a "later fell" note. Empty DeathSource is legacy and
// treated as adventure-death (current behavior).
if p.Location != "" {
sb.WriteString(fmt.Sprintf("💀 **%s** — Combat Lv.%d | Mining Lv.%d | Forage Lv.%d | Fishing Lv.%d\n",
p.DisplayName, p.CombatLevel, p.MiningSkill, p.ForagingSkill, p.FishingSkill))
diedOnAdventure := p.DeathSource == "" || p.DeathSource == "adventure"
icon := "💀"
if !diedOnAdventure {
icon = "⚔️"
}
sb.WriteString(fmt.Sprintf("%s **%s** — Combat Lv.%d | Mining Lv.%d | Forage Lv.%d | Fishing Lv.%d\n",
icon, p.DisplayName, p.CombatLevel, p.MiningSkill, p.ForagingSkill, p.FishingSkill))
sb.WriteString(fmt.Sprintf(" Went to: %s\n", p.Location))
sb.WriteString(fmt.Sprintf(" Outcome: %s\n\n", p.SummaryLine))
sb.WriteString(fmt.Sprintf(" Outcome: %s\n", p.SummaryLine))
if !diedOnAdventure {
where := p.DeathLocation
if where == "" {
where = "elsewhere"
}
sb.WriteString(fmt.Sprintf(" 💀 Later fell in %s.\n", where))
}
sb.WriteString("\n")
if worstPlayer == nil {
worstPlayer = p
}
@@ -725,9 +744,13 @@ func renderAdvDailySummary(date string, tb *TwinBeeResult, tbRewards TwinBeeRewa
pool := SummaryStandoutDeath
if len(pool) > 0 {
line := pool[rand.IntN(len(pool))]
lossLoc := worstPlayer.DeathLocation
if lossLoc == "" {
lossLoc = worstPlayer.Location
}
line = advSubstituteFlavor(line, map[string]string{
"{name}": worstPlayer.DisplayName,
"{location}": worstPlayer.Location,
"{location}": lossLoc,
})
sb.WriteString(fmt.Sprintf("🏆 **Today's standout:** %s\n", line))
}