From c14c2e403c75a913e1a6d962fb6fe64f013adae5 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Wed, 1 Jul 2026 22:30:31 -0400 Subject: fix: combat formulas and mob attack types (mobs switch attack styles if safespotting) --- internal/combat/formulas.go | 44 ++++++++++++++++++++++++++++++------------- internal/combat/types.go | 36 +++++++++++++++++++++++++++++++++++ internal/combat/types_test.go | 30 +++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 internal/combat/types.go create mode 100644 internal/combat/types_test.go (limited to 'internal/combat') diff --git a/internal/combat/formulas.go b/internal/combat/formulas.go index f4e1ba2..f165923 100644 --- a/internal/combat/formulas.go +++ b/internal/combat/formulas.go @@ -2,8 +2,24 @@ package combat import "math/rand" -func EffectiveRoll(level int, styleBonus int, equipBonus int) int { - effective := level + styleBonus + 8 +func PlayerEffective(level, styleBonus int) int { + return level + styleBonus + 8 +} + +func NPCEffective(level int) int { + return level + 9 +} + +// ScienceEffective is the effective-level formula for science attacks. It is +// intentionally the same as NPCEffective (level + 9) and is applied to BOTH the +// attacker and defender of a science-mod exchange (see scienceAttack), giving a +// symmetric science-vs-science model with no attack styles. It is kept as a +// distinct function to document that intent independently of NPCEffective. +func ScienceEffective(level int) int { + return level + 9 +} + +func AttackRoll(effective, equipBonus int) int { return effective * (equipBonus + 64) } @@ -17,13 +33,11 @@ func HitChance(attackRoll, defenseRoll int) float64 { } func HitCheck(attackRoll, defenseRoll int) bool { - chance := HitChance(attackRoll, defenseRoll) - return rand.Float64() < chance + return rand.Float64() < HitChance(attackRoll, defenseRoll) } -func MaxHit(level int, styleBonus int, equipBonus int) int { - effective := level + styleBonus + 8 - hit := (effective * (equipBonus + 64)) / 512 +func MaxHit(effective, equipStrBonus int) int { + hit := (effective*(equipStrBonus+64) + 320) / 640 if hit < 1 { hit = 1 } @@ -34,7 +48,11 @@ func RollDamage(maxHit int) int { if maxHit <= 0 { return 0 } - return 1 + rand.Intn(maxHit) + d := rand.Intn(maxHit + 1) + if d < 1 { + d = 1 + } + return d } func AttackStyleBonus(style string) (attack, strength, defense int) { @@ -69,15 +87,15 @@ func RangedStyleBonus(style string) (ranged, defense int) { func SelectBonus(attackType string, stab, slash, crush, science, ranged int) int { switch attackType { - case "stab": + case AttackStab: return stab - case "slash": + case AttackSlash: return slash - case "crush": + case AttackCrush: return crush - case "science": + case AttackScience: return science - case "ranged": + case AttackRanged: return ranged default: return crush diff --git a/internal/combat/types.go b/internal/combat/types.go new file mode 100644 index 0000000..bb2a897 --- /dev/null +++ b/internal/combat/types.go @@ -0,0 +1,36 @@ +package combat + +// Attack type identifiers shared across combat, mobs, weapons, hazards and +// validation. These are the canonical string values stored in YAML. +const ( + AttackStab = "stab" + AttackSlash = "slash" + AttackCrush = "crush" + AttackRanged = "ranged" + AttackScience = "science" + + // DefaultAttackType is the fallback when no attack type is specified. + DefaultAttackType = AttackCrush +) + +// MeleeAttackTypes lists the melee attack types (exactly one of which a mob +// must have). +var MeleeAttackTypes = []string{AttackStab, AttackSlash, AttackCrush} + +// AllAttackTypes lists every valid attack type. +var AllAttackTypes = []string{AttackStab, AttackSlash, AttackCrush, AttackRanged, AttackScience} + +// IsMeleeType reports whether t is a melee attack type. +func IsMeleeType(t string) bool { + return t == AttackStab || t == AttackSlash || t == AttackCrush +} + +// IsValidAttackType reports whether t is any recognized attack type. +func IsValidAttackType(t string) bool { + switch t { + case AttackStab, AttackSlash, AttackCrush, AttackRanged, AttackScience: + return true + default: + return false + } +} diff --git a/internal/combat/types_test.go b/internal/combat/types_test.go new file mode 100644 index 0000000..a999eb6 --- /dev/null +++ b/internal/combat/types_test.go @@ -0,0 +1,30 @@ +package combat + +import "testing" + +func TestIsMeleeType(t *testing.T) { + melee := []string{AttackStab, AttackSlash, AttackCrush} + for _, m := range melee { + if !IsMeleeType(m) { + t.Errorf("IsMeleeType(%q) = false, want true", m) + } + } + for _, nm := range []string{AttackRanged, AttackScience, "", "magic"} { + if IsMeleeType(nm) { + t.Errorf("IsMeleeType(%q) = true, want false", nm) + } + } +} + +func TestIsValidAttackType(t *testing.T) { + for _, v := range AllAttackTypes { + if !IsValidAttackType(v) { + t.Errorf("IsValidAttackType(%q) = false, want true", v) + } + } + for _, iv := range []string{"", "magic", "melee"} { + if IsValidAttackType(iv) { + t.Errorf("IsValidAttackType(%q) = true, want false", iv) + } + } +} -- cgit v1.2.3