mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-18 01:42:42 +00:00
adventure: honor death-cheats on a cantrip kill and narrate the cantrip
A caster's at-will cantrip that lands the killing blow returned via a raw enemyHP<=0 read, skipping enemyDown -- so a survive_at_1 boss or T6 Valdris's phylactery rebirth died instead of cheating death. Route it through enemyDown, matching resolvePlayerAttack and the concentration-tick path. Also add the missing renderEvent "cantrip" case: the CantripDesc narration hook was set but never rendered, so cantrip damage dropped enemy HP with no log line.
This commit is contained in:
@@ -181,6 +181,20 @@ type CombatModifiers struct {
|
|||||||
SpellPreDamage int
|
SpellPreDamage int
|
||||||
SpellPreDamageDesc string
|
SpellPreDamageDesc string
|
||||||
SpellEnemySkipFirst bool
|
SpellEnemySkipFirst bool
|
||||||
|
|
||||||
|
// At-will cantrip channel. Arcane blasters (Mage/Sorcerer/Warlock) throw a
|
||||||
|
// scaling damage cantrip EVERY round — 5e cantrips are the caster's at-will
|
||||||
|
// floor and scale to 4 dice at L17 (Fire Bolt 4d10, Eldritch Blast 4 beams).
|
||||||
|
// The pre-combat one-shot SpellPreDamage modelled a single leveled cast and
|
||||||
|
// left the caster swinging a stick for the rest of the fight; that is the
|
||||||
|
// whole of the caster T5-room wall (one burst can't finish a 65-HP monster
|
||||||
|
// and the quarterstaff floor does nothing). CantripPerRound is dealt as flat
|
||||||
|
// magic damage at the top of resolvePlayerSwings each round — no dice roll,
|
||||||
|
// so the RNG stream is stable and variance stays low. 0 for non-casters, so
|
||||||
|
// martial combat is byte-identical. Halved by enemy spell_resist like any
|
||||||
|
// spell. CantripDesc is the narration hook (spell name).
|
||||||
|
CantripPerRound int
|
||||||
|
CantripDesc string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Combatant struct {
|
type Combatant struct {
|
||||||
@@ -568,6 +582,31 @@ func maybeTriggerOrcRage(st *combatState, player *Combatant, phaseName string) {
|
|||||||
// consumes once-per-fight openers (AutoCritFirst, FirstAttackBonus,
|
// consumes once-per-fight openers (AutoCritFirst, FirstAttackBonus,
|
||||||
// AssassinateAdvantage) via st flags — extras roll vanilla.
|
// AssassinateAdvantage) via st flags — extras roll vanilla.
|
||||||
func resolvePlayerSwings(st *combatState, player, enemy *Combatant, phase *CombatPhase, result *CombatResult) bool {
|
func resolvePlayerSwings(st *combatState, player, enemy *Combatant, phase *CombatPhase, result *CombatResult) bool {
|
||||||
|
// At-will cantrip: fires once per round before the weapon swing, independent
|
||||||
|
// of whether the swing connects (a caster who whiffs the stick still throws
|
||||||
|
// its Fire Bolt). Flat magic damage, halved by spell_resist. 0 for
|
||||||
|
// non-casters → skipped entirely, so martial combat draws no extra events
|
||||||
|
// and no RNG. See CantripPerRound in CombatModifiers.
|
||||||
|
if player.Mods.CantripPerRound > 0 && st.enemyHP > 0 {
|
||||||
|
dmg := player.Mods.CantripPerRound
|
||||||
|
if enemyResistsSpells(enemy, st) {
|
||||||
|
dmg = max(1, dmg/2)
|
||||||
|
}
|
||||||
|
st.enemyHP = max(0, st.enemyHP-dmg)
|
||||||
|
st.events = append(st.events, CombatEvent{
|
||||||
|
Round: st.round, Phase: phase.Name, Actor: "player", Action: "cantrip",
|
||||||
|
Damage: dmg, PlayerHP: st.playerHP, EnemyHP: st.enemyHP,
|
||||||
|
Desc: player.Mods.CantripDesc,
|
||||||
|
})
|
||||||
|
// Route the kill through enemyDown, not a raw HP read: a boss that cheats
|
||||||
|
// death (survive_at_1) or holds a phylactery rebirth (T6 Valdris) must get
|
||||||
|
// that chance even when the lethal blow is the at-will cantrip. enemyDown
|
||||||
|
// restores its HP and returns false, so the weapon swing below resolves
|
||||||
|
// against the revived pool. Mirrors resolvePlayerAttack's own kill routing.
|
||||||
|
if enemyDown(st, phase.Name) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
if resolvePlayerAttack(st, player, enemy, phase, result) {
|
if resolvePlayerAttack(st, player, enemy, phase, result) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,6 +242,10 @@ func renderEvent(e CombatEvent, playerName, enemyName string, result CombatResul
|
|||||||
case "concentration_tick":
|
case "concentration_tick":
|
||||||
return fmt.Sprintf(pickRand(narrativeConcentrationTick), e.Damage)
|
return fmt.Sprintf(pickRand(narrativeConcentrationTick), e.Damage)
|
||||||
|
|
||||||
|
case "cantrip":
|
||||||
|
// e.Desc is the spell name (Fire Bolt / Eldritch Blast); e.Damage the hit.
|
||||||
|
return fmt.Sprintf(pickRand(narrativeCantrip), e.Desc, e.Damage)
|
||||||
|
|
||||||
case "pet_deflect":
|
case "pet_deflect":
|
||||||
return pickRand(narrativePetDeflect)
|
return pickRand(narrativePetDeflect)
|
||||||
|
|
||||||
@@ -558,6 +562,15 @@ var narrativeConcentrationTick = []string{
|
|||||||
"🌀 The enemy steps wrong and the standing magic answers, %d damage. It does not move on.",
|
"🌀 The enemy steps wrong and the standing magic answers, %d damage. It does not move on.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// narrativeCantrip fires each round an arcane blaster throws its at-will cantrip
|
||||||
|
// (Fire Bolt / Eldritch Blast) before the weapon swing. %s is the spell name,
|
||||||
|
// %d the damage — a caster's sustained floor, so it lands every round.
|
||||||
|
var narrativeCantrip = []string{
|
||||||
|
"✨ %s streaks out and burns home — %d damage. The at-will floor never stops.",
|
||||||
|
"✨ A bolt of %s answers before the staff even moves, scorching for %d.",
|
||||||
|
"✨ %s lances the enemy for %d. No incantation, no wind-up — just the steady arcane drum.",
|
||||||
|
}
|
||||||
|
|
||||||
var narrativePetDeflect = []string{
|
var narrativePetDeflect = []string{
|
||||||
"🐾 Your pet intercepts the blow. Damage halved. Your pet is now your best piece of equipment.",
|
"🐾 Your pet intercepts the blow. Damage halved. Your pet is now your best piece of equipment.",
|
||||||
"🐾 Your pet pushes you aside at the last second. Impact reduced. You did not ask to be pushed. Results speak for themselves.",
|
"🐾 Your pet pushes you aside at the last second. Impact reduced. You did not ask to be pushed. Results speak for themselves.",
|
||||||
|
|||||||
Reference in New Issue
Block a user