mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 17:02:42 +00:00
Combat: inject per-session RNG into the engine
Adds an optional *rand.Rand to combatState so a fight can be driven by a deterministic source. Auto-resolve leaves it nil and falls through to the package global in the same call order — behaviorally identical. Threads the source through calcDamage and rollWeaponDamage. Groundwork for the turn-based elite/boss engine and its timeout reaper, which seed the rng per combat_session to make fights resumable and replayable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -277,8 +277,23 @@ type combatState struct {
|
|||||||
|
|
||||||
round int
|
round int
|
||||||
events []CombatEvent
|
events []CombatEvent
|
||||||
|
|
||||||
|
// rng, when non-nil, is the deterministic source the turn-based engine
|
||||||
|
// and the timeout reaper seed per session so a fight can be resumed and
|
||||||
|
// replayed reproducibly. Auto-resolve (SimulateCombat called without a
|
||||||
|
// session) leaves this nil and the engine falls back to the package
|
||||||
|
// global — behaviorally identical to the pre-injection code.
|
||||||
|
rng *rand.Rand
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// roll / randFloat draw from the session's injected rng when present,
|
||||||
|
// else the package global (see rngIntN / rngFloat in dnd_zone_loot.go).
|
||||||
|
// Auto-resolve leaves st.rng nil — behaviorally identical to the
|
||||||
|
// pre-injection code; the turn-based engine and the timeout reaper seed
|
||||||
|
// it per session so a fight can be resumed and replayed reproducibly.
|
||||||
|
func (st *combatState) roll(n int) int { return rngIntN(st.rng, n) }
|
||||||
|
func (st *combatState) randFloat() float64 { return rngFloat(st.rng) }
|
||||||
|
|
||||||
func SimulateCombat(player, enemy Combatant, phases []CombatPhase) CombatResult {
|
func SimulateCombat(player, enemy Combatant, phases []CombatPhase) CombatResult {
|
||||||
playerStart := player.Stats.MaxHP
|
playerStart := player.Stats.MaxHP
|
||||||
if player.Stats.StartHP > 0 && player.Stats.StartHP < player.Stats.MaxHP {
|
if player.Stats.StartHP > 0 && player.Stats.StartHP < player.Stats.MaxHP {
|
||||||
@@ -313,7 +328,7 @@ func SimulateCombat(player, enemy Combatant, phases []CombatPhase) CombatResult
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pre-combat: Arina sniper check
|
// Pre-combat: Arina sniper check
|
||||||
if player.Mods.SniperKillProc > 0 && rand.Float64() < player.Mods.SniperKillProc {
|
if player.Mods.SniperKillProc > 0 && st.randFloat() < player.Mods.SniperKillProc {
|
||||||
st.enemyHP = 0
|
st.enemyHP = 0
|
||||||
st.events = append(st.events, CombatEvent{
|
st.events = append(st.events, CombatEvent{
|
||||||
Round: 0, Phase: "pre_combat", Actor: "npc", Action: "sniper_kill",
|
Round: 0, Phase: "pre_combat", Actor: "npc", Action: "sniper_kill",
|
||||||
@@ -361,7 +376,7 @@ func SimulateCombat(player, enemy Combatant, phases []CombatPhase) CombatResult
|
|||||||
roundsThisPhase := phase.Rounds
|
roundsThisPhase := phase.Rounds
|
||||||
// Add slight variance: ±1 round for non-Decisive phases
|
// Add slight variance: ±1 round for non-Decisive phases
|
||||||
if phase.Name != "Decisive" && roundsThisPhase > 1 {
|
if phase.Name != "Decisive" && roundsThisPhase > 1 {
|
||||||
roundsThisPhase += rand.IntN(2) // 0 or +1
|
roundsThisPhase += st.roll(2) // 0 or +1
|
||||||
}
|
}
|
||||||
for r := 0; r < roundsThisPhase; r++ {
|
for r := 0; r < roundsThisPhase; r++ {
|
||||||
st.round++
|
st.round++
|
||||||
@@ -444,23 +459,23 @@ func simulateRound(st *combatState, player, enemy *Combatant, phase *CombatPhase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pet whiff: if proc'd, enemy's attack this round is a guaranteed miss
|
// Pet whiff: if proc'd, enemy's attack this round is a guaranteed miss
|
||||||
petWhiff := player.Mods.PetWhiffProc > 0 && rand.Float64() < player.Mods.PetWhiffProc
|
petWhiff := player.Mods.PetWhiffProc > 0 && st.randFloat() < player.Mods.PetWhiffProc
|
||||||
// Pet deflect: halves incoming damage to player this round
|
// Pet deflect: halves incoming damage to player this round
|
||||||
petDeflect := player.Mods.PetDeflectProc > 0 && rand.Float64() < player.Mods.PetDeflectProc
|
petDeflect := player.Mods.PetDeflectProc > 0 && st.randFloat() < player.Mods.PetDeflectProc
|
||||||
if petDeflect {
|
if petDeflect {
|
||||||
result.PetDeflected = true
|
result.PetDeflected = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spore cloud: enemy has 15% miss chance (decremented when enemy actually attacks)
|
// Spore cloud: enemy has 15% miss chance (decremented when enemy actually attacks)
|
||||||
sporeMiss := st.sporeRounds > 0 && rand.Float64() < 0.15
|
sporeMiss := st.sporeRounds > 0 && st.randFloat() < 0.15
|
||||||
|
|
||||||
// Determine initiative. DM mood (Effusive/Hostile) biases the player's
|
// Determine initiative. DM mood (Effusive/Hostile) biases the player's
|
||||||
// roll via player.Mods.InitiativeBias — +X means player goes first
|
// roll via player.Mods.InitiativeBias — +X means player goes first
|
||||||
// more often, -X means the enemy does.
|
// more often, -X means the enemy does.
|
||||||
playerSpeed := float64(player.Stats.Speed) * phase.SpeedWeight
|
playerSpeed := float64(player.Stats.Speed) * phase.SpeedWeight
|
||||||
enemySpeed := float64(enemy.Stats.Speed) * phase.SpeedWeight
|
enemySpeed := float64(enemy.Stats.Speed) * phase.SpeedWeight
|
||||||
playerInit := playerSpeed + rand.Float64()*10 + player.Mods.InitiativeBias
|
playerInit := playerSpeed + st.randFloat()*10 + player.Mods.InitiativeBias
|
||||||
enemyInit := enemySpeed + rand.Float64()*10
|
enemyInit := enemySpeed + st.randFloat()*10
|
||||||
playerFirst := playerInit >= enemyInit
|
playerFirst := playerInit >= enemyInit
|
||||||
|
|
||||||
if playerFirst {
|
if playerFirst {
|
||||||
@@ -484,8 +499,8 @@ func simulateRound(st *combatState, player, enemy *Combatant, phase *CombatPhase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Environmental hazard
|
// Environmental hazard
|
||||||
if phase.EnvironmentProc > 0 && rand.Float64() < phase.EnvironmentProc {
|
if phase.EnvironmentProc > 0 && st.randFloat() < phase.EnvironmentProc {
|
||||||
envDmg := 2 + rand.IntN(5)
|
envDmg := 2 + st.roll(5)
|
||||||
st.playerHP = max(0, st.playerHP-envDmg)
|
st.playerHP = max(0, st.playerHP-envDmg)
|
||||||
st.events = append(st.events, CombatEvent{
|
st.events = append(st.events, CombatEvent{
|
||||||
Round: st.round, Phase: phaseName, Actor: "environment", Action: "environmental",
|
Round: st.round, Phase: phaseName, Actor: "environment", Action: "environmental",
|
||||||
@@ -499,7 +514,7 @@ func simulateRound(st *combatState, player, enemy *Combatant, phase *CombatPhase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Misty crowd revenge (debuff for declining Misty)
|
// Misty crowd revenge (debuff for declining Misty)
|
||||||
if player.Mods.CrowdRevengeProc > 0 && rand.Float64() < player.Mods.CrowdRevengeProc {
|
if player.Mods.CrowdRevengeProc > 0 && st.randFloat() < player.Mods.CrowdRevengeProc {
|
||||||
dmg := player.Mods.CrowdRevengeDmg
|
dmg := player.Mods.CrowdRevengeDmg
|
||||||
st.playerHP = max(0, st.playerHP-dmg)
|
st.playerHP = max(0, st.playerHP-dmg)
|
||||||
st.events = append(st.events, CombatEvent{
|
st.events = append(st.events, CombatEvent{
|
||||||
@@ -515,8 +530,8 @@ func simulateRound(st *combatState, player, enemy *Combatant, phase *CombatPhase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pet attack
|
// Pet attack
|
||||||
if player.Mods.PetAttackProc > 0 && rand.Float64() < player.Mods.PetAttackProc {
|
if player.Mods.PetAttackProc > 0 && st.randFloat() < player.Mods.PetAttackProc {
|
||||||
petDmg := player.Mods.PetAttackDmg + rand.IntN(5)
|
petDmg := player.Mods.PetAttackDmg + st.roll(5)
|
||||||
st.enemyHP = max(0, st.enemyHP-petDmg)
|
st.enemyHP = max(0, st.enemyHP-petDmg)
|
||||||
result.PetAttacked = true
|
result.PetAttacked = true
|
||||||
st.events = append(st.events, CombatEvent{
|
st.events = append(st.events, CombatEvent{
|
||||||
@@ -529,7 +544,7 @@ func simulateRound(st *combatState, player, enemy *Combatant, phase *CombatPhase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Misty heal
|
// Misty heal
|
||||||
if player.Mods.MistyHealProc > 0 && rand.Float64() < player.Mods.MistyHealProc {
|
if player.Mods.MistyHealProc > 0 && st.randFloat() < player.Mods.MistyHealProc {
|
||||||
healAmt := player.Mods.MistyHealAmt
|
healAmt := player.Mods.MistyHealAmt
|
||||||
st.playerHP = min(player.Stats.MaxHP, st.playerHP+healAmt)
|
st.playerHP = min(player.Stats.MaxHP, st.playerHP+healAmt)
|
||||||
result.MistyHealed = true
|
result.MistyHealed = true
|
||||||
@@ -592,11 +607,11 @@ func resolvePlayerAttack(st *combatState, player, enemy *Combatant, phase *Comba
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
roll := 1 + rand.IntN(20)
|
roll := 1 + st.roll(20)
|
||||||
// Halfling Lucky: reroll the first nat 1 of the fight.
|
// Halfling Lucky: reroll the first nat 1 of the fight.
|
||||||
if roll == 1 && player.Mods.LuckyReroll && !st.luckyUsed {
|
if roll == 1 && player.Mods.LuckyReroll && !st.luckyUsed {
|
||||||
st.luckyUsed = true
|
st.luckyUsed = true
|
||||||
newRoll := 1 + rand.IntN(20)
|
newRoll := 1 + st.roll(20)
|
||||||
st.events = append(st.events, CombatEvent{
|
st.events = append(st.events, CombatEvent{
|
||||||
Round: st.round, Phase: phaseName, Actor: "player", Action: "lucky_reroll",
|
Round: st.round, Phase: phaseName, Actor: "player", Action: "lucky_reroll",
|
||||||
PlayerHP: st.playerHP, EnemyHP: st.enemyHP,
|
PlayerHP: st.playerHP, EnemyHP: st.enemyHP,
|
||||||
@@ -609,7 +624,7 @@ func resolvePlayerAttack(st *combatState, player, enemy *Combatant, phase *Comba
|
|||||||
// creatures that haven't acted yet" applied to the opening strike only.
|
// creatures that haven't acted yet" applied to the opening strike only.
|
||||||
if player.Mods.AssassinateAdvantage && !st.assassinateRerollUsed {
|
if player.Mods.AssassinateAdvantage && !st.assassinateRerollUsed {
|
||||||
st.assassinateRerollUsed = true
|
st.assassinateRerollUsed = true
|
||||||
alt := 1 + rand.IntN(20)
|
alt := 1 + st.roll(20)
|
||||||
if alt > roll {
|
if alt > roll {
|
||||||
st.events = append(st.events, CombatEvent{
|
st.events = append(st.events, CombatEvent{
|
||||||
Round: st.round, Phase: phaseName, Actor: "player", Action: "assassinate_advantage",
|
Round: st.round, Phase: phaseName, Actor: "player", Action: "assassinate_advantage",
|
||||||
@@ -661,7 +676,7 @@ func resolvePlayerAttack(st *combatState, player, enemy *Combatant, phase *Comba
|
|||||||
if !player.Stats.WeaponProficient {
|
if !player.Stats.WeaponProficient {
|
||||||
mod = 0
|
mod = 0
|
||||||
}
|
}
|
||||||
total, _ := rollWeaponDamage(player.Stats.Weapon, mod, player.Stats.TwoHandedMode)
|
total, _ := rollWeaponDamage(st.rng, player.Stats.Weapon, mod, player.Stats.TwoHandedMode)
|
||||||
dmg = total
|
dmg = total
|
||||||
// Class damage bonus / streak bonus / etc. layered on top via DamageBonus.
|
// Class damage bonus / streak bonus / etc. layered on top via DamageBonus.
|
||||||
if player.Mods.DamageBonus > 0 {
|
if player.Mods.DamageBonus > 0 {
|
||||||
@@ -672,11 +687,11 @@ func resolvePlayerAttack(st *combatState, player, enemy *Combatant, phase *Comba
|
|||||||
dmg = int(float64(dmg) * enemy.Mods.DamageReduct)
|
dmg = int(float64(dmg) * enemy.Mods.DamageReduct)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dmg = calcDamage(player.Stats.Attack, phase.AttackWeight, player.Mods.DamageBonus,
|
dmg = calcDamage(st.rng, player.Stats.Attack, phase.AttackWeight, player.Mods.DamageBonus,
|
||||||
enemy.Stats.Defense, phase.DefenseWeight, enemy.Mods.DamageReduct)
|
enemy.Stats.Defense, phase.DefenseWeight, enemy.Mods.DamageReduct)
|
||||||
}
|
}
|
||||||
|
|
||||||
blocked := enemy.Stats.BlockRate > 0 && rand.Float64() < enemy.Stats.BlockRate
|
blocked := enemy.Stats.BlockRate > 0 && st.randFloat() < enemy.Stats.BlockRate
|
||||||
if blocked {
|
if blocked {
|
||||||
dmg = max(1, dmg/2)
|
dmg = max(1, dmg/2)
|
||||||
}
|
}
|
||||||
@@ -777,7 +792,7 @@ func resolveEnemyAttack(st *combatState, player, enemy *Combatant, phase *Combat
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
roll := 1 + rand.IntN(20)
|
roll := 1 + st.roll(20)
|
||||||
isFumble := roll == 1
|
isFumble := roll == 1
|
||||||
isNat20 := roll == 20
|
isNat20 := roll == 20
|
||||||
total := roll + enemy.Stats.AttackBonus
|
total := roll + enemy.Stats.AttackBonus
|
||||||
@@ -808,10 +823,10 @@ func resolveEnemyAttack(st *combatState, player, enemy *Combatant, phase *Combat
|
|||||||
if st.enraged {
|
if st.enraged {
|
||||||
atkMult = 1.5
|
atkMult = 1.5
|
||||||
}
|
}
|
||||||
dmg := calcDamage(int(float64(enemy.Stats.Attack)*atkMult), phase.AttackWeight, enemy.Mods.DamageBonus,
|
dmg := calcDamage(st.rng, int(float64(enemy.Stats.Attack)*atkMult), phase.AttackWeight, enemy.Mods.DamageBonus,
|
||||||
playerDefense(player, st), phase.DefenseWeight, player.Mods.DamageReduct)
|
playerDefense(player, st), phase.DefenseWeight, player.Mods.DamageReduct)
|
||||||
|
|
||||||
blocked := player.Stats.BlockRate > 0 && rand.Float64() < player.Stats.BlockRate
|
blocked := player.Stats.BlockRate > 0 && st.randFloat() < player.Stats.BlockRate
|
||||||
if blocked {
|
if blocked {
|
||||||
dmg = max(1, dmg/2)
|
dmg = max(1, dmg/2)
|
||||||
}
|
}
|
||||||
@@ -897,7 +912,7 @@ func abilityFires(ability *MonsterAbility, phaseName string, st *combatState) bo
|
|||||||
if !phaseMatch {
|
if !phaseMatch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return rand.Float64() < ability.ProcChance
|
return st.randFloat() < ability.ProcChance
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyAbility(st *combatState, player, enemy *Combatant, phase *CombatPhase, result *CombatResult) bool {
|
func applyAbility(st *combatState, player, enemy *Combatant, phase *CombatPhase, result *CombatResult) bool {
|
||||||
@@ -907,7 +922,7 @@ func applyAbility(st *combatState, player, enemy *Combatant, phase *CombatPhase,
|
|||||||
switch ab.Effect {
|
switch ab.Effect {
|
||||||
case "poison":
|
case "poison":
|
||||||
st.poisonTicks = 2
|
st.poisonTicks = 2
|
||||||
st.poisonDmg = 3 + rand.IntN(3)
|
st.poisonDmg = 3 + st.roll(3)
|
||||||
if player.Mods.PoisonResist {
|
if player.Mods.PoisonResist {
|
||||||
st.poisonDmg = max(1, st.poisonDmg/2)
|
st.poisonDmg = max(1, st.poisonDmg/2)
|
||||||
}
|
}
|
||||||
@@ -947,7 +962,7 @@ func applyAbility(st *combatState, player, enemy *Combatant, phase *CombatPhase,
|
|||||||
if st.enraged {
|
if st.enraged {
|
||||||
atkMult = 1.5
|
atkMult = 1.5
|
||||||
}
|
}
|
||||||
dmg := calcDamage(int(float64(enemy.Stats.Attack)*atkMult), phase.AttackWeight, enemy.Mods.DamageBonus,
|
dmg := calcDamage(st.rng, int(float64(enemy.Stats.Attack)*atkMult), phase.AttackWeight, enemy.Mods.DamageBonus,
|
||||||
playerDefense(player, st), phase.DefenseWeight, player.Mods.DamageReduct)
|
playerDefense(player, st), phase.DefenseWeight, player.Mods.DamageReduct)
|
||||||
dmg = max(1, dmg)
|
dmg = max(1, dmg)
|
||||||
st.playerHP = max(0, st.playerHP-dmg)
|
st.playerHP = max(0, st.playerHP-dmg)
|
||||||
@@ -966,7 +981,7 @@ func applyAbility(st *combatState, player, enemy *Combatant, phase *CombatPhase,
|
|||||||
if st.enraged {
|
if st.enraged {
|
||||||
atkMult = 1.5
|
atkMult = 1.5
|
||||||
}
|
}
|
||||||
dmg1 := calcDamage(int(float64(enemy.Stats.Attack)*atkMult), phase.AttackWeight, enemy.Mods.DamageBonus,
|
dmg1 := calcDamage(st.rng, int(float64(enemy.Stats.Attack)*atkMult), phase.AttackWeight, enemy.Mods.DamageBonus,
|
||||||
playerDefense(player, st), phase.DefenseWeight, player.Mods.DamageReduct)
|
playerDefense(player, st), phase.DefenseWeight, player.Mods.DamageReduct)
|
||||||
dmg1 = max(1, dmg1)
|
dmg1 = max(1, dmg1)
|
||||||
st.playerHP = max(0, st.playerHP-dmg1)
|
st.playerHP = max(0, st.playerHP-dmg1)
|
||||||
@@ -1004,13 +1019,13 @@ func applyAbility(st *combatState, player, enemy *Combatant, phase *CombatPhase,
|
|||||||
//
|
//
|
||||||
// A ±15% per-hit jitter is applied so successive hits in the same phase don't
|
// A ±15% per-hit jitter is applied so successive hits in the same phase don't
|
||||||
// produce identical numbers — the previous flat-damage output read as scripted.
|
// produce identical numbers — the previous flat-damage output read as scripted.
|
||||||
func calcDamage(attack int, atkWeight, dmgBonus float64, defense int, defWeight, dmgReduct float64) int {
|
func calcDamage(rng *rand.Rand, attack int, atkWeight, dmgBonus float64, defense int, defWeight, dmgReduct float64) int {
|
||||||
const K = 40.0
|
const K = 40.0
|
||||||
rawAtk := float64(attack) * atkWeight * (1 + dmgBonus)
|
rawAtk := float64(attack) * atkWeight * (1 + dmgBonus)
|
||||||
effectiveDef := float64(defense) * defWeight * dmgReduct
|
effectiveDef := float64(defense) * defWeight * dmgReduct
|
||||||
reduction := K / (K + effectiveDef)
|
reduction := K / (K + effectiveDef)
|
||||||
dmg := rawAtk * reduction
|
dmg := rawAtk * reduction
|
||||||
jitter := 0.85 + rand.Float64()*0.30 // 0.85 .. 1.15
|
jitter := 0.85 + rngFloat(rng)*0.30 // 0.85 .. 1.15
|
||||||
dmg *= jitter
|
dmg *= jitter
|
||||||
if dmg < 1 {
|
if dmg < 1 {
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -239,13 +239,13 @@ func dndClassArmorProficiency(class DnDClass, a *ArmorProfile) bool {
|
|||||||
// + magic bonus. If twoHanded is true and the weapon is versatile, rolls
|
// + magic bonus. If twoHanded is true and the weapon is versatile, rolls
|
||||||
// the larger versatile die instead. Returns the unmodified dice total too
|
// the larger versatile die instead. Returns the unmodified dice total too
|
||||||
// for the crit doubling math.
|
// for the crit doubling math.
|
||||||
func rollWeaponDamage(w *WeaponProfile, abilityMod int, twoHanded bool) (total, dice int) {
|
func rollWeaponDamage(rng *rand.Rand, w *WeaponProfile, abilityMod int, twoHanded bool) (total, dice int) {
|
||||||
count, sides := w.DamageCount, w.DamageSides
|
count, sides := w.DamageCount, w.DamageSides
|
||||||
if twoHanded && w.HasProperty(PropVersatile) && w.VersaCount > 0 {
|
if twoHanded && w.HasProperty(PropVersatile) && w.VersaCount > 0 {
|
||||||
count, sides = w.VersaCount, w.VersaSides
|
count, sides = w.VersaCount, w.VersaSides
|
||||||
}
|
}
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
dice += 1 + rand.IntN(sides)
|
dice += 1 + rngIntN(rng, sides)
|
||||||
}
|
}
|
||||||
total = dice + abilityMod + w.MagicBonus
|
total = dice + abilityMod + w.MagicBonus
|
||||||
if total < 1 {
|
if total < 1 {
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ func TestComputeArmorAC_MagicBonus(t *testing.T) {
|
|||||||
func TestRollWeaponDamage_Bounds(t *testing.T) {
|
func TestRollWeaponDamage_Bounds(t *testing.T) {
|
||||||
greatsword := weaponByID("wpn_greatsword") // 2d6
|
greatsword := weaponByID("wpn_greatsword") // 2d6
|
||||||
for i := 0; i < 1000; i++ {
|
for i := 0; i < 1000; i++ {
|
||||||
total, dice := rollWeaponDamage(greatsword, 3, false)
|
total, dice := rollWeaponDamage(nil, greatsword, 3, false)
|
||||||
if dice < 2 || dice > 12 {
|
if dice < 2 || dice > 12 {
|
||||||
t.Fatalf("greatsword dice out of [2,12]: %d", dice)
|
t.Fatalf("greatsword dice out of [2,12]: %d", dice)
|
||||||
}
|
}
|
||||||
@@ -193,14 +193,14 @@ func TestRollWeaponDamage_VersatileTwoHanded(t *testing.T) {
|
|||||||
longsword := weaponByID("wpn_longsword") // 1d8 / 1d10
|
longsword := weaponByID("wpn_longsword") // 1d8 / 1d10
|
||||||
// One-handed
|
// One-handed
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
_, dice := rollWeaponDamage(longsword, 0, false)
|
_, dice := rollWeaponDamage(nil, longsword, 0, false)
|
||||||
if dice < 1 || dice > 8 {
|
if dice < 1 || dice > 8 {
|
||||||
t.Errorf("longsword 1H dice = %d, want [1,8]", dice)
|
t.Errorf("longsword 1H dice = %d, want [1,8]", dice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Two-handed (versatile)
|
// Two-handed (versatile)
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
_, dice := rollWeaponDamage(longsword, 0, true)
|
_, dice := rollWeaponDamage(nil, longsword, 0, true)
|
||||||
if dice < 1 || dice > 10 {
|
if dice < 1 || dice > 10 {
|
||||||
t.Errorf("longsword 2H dice = %d, want [1,10]", dice)
|
t.Errorf("longsword 2H dice = %d, want [1,10]", dice)
|
||||||
}
|
}
|
||||||
@@ -211,7 +211,7 @@ func TestRollWeaponDamage_FloorAt1(t *testing.T) {
|
|||||||
// Pathological: 1d4 with -10 mod always rolls 1+(-10) = negative, floored to 1.
|
// Pathological: 1d4 with -10 mod always rolls 1+(-10) = negative, floored to 1.
|
||||||
dagger := weaponByID("wpn_dagger")
|
dagger := weaponByID("wpn_dagger")
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
total, _ := rollWeaponDamage(dagger, -10, false)
|
total, _ := rollWeaponDamage(nil, dagger, -10, false)
|
||||||
if total < 1 {
|
if total < 1 {
|
||||||
t.Errorf("damage floor violated: %d", total)
|
t.Errorf("damage floor violated: %d", total)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user