aboutsummaryrefslogtreecommitdiff
path: root/skill_plans/combat.md
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-19 02:08:55 -0400
committerhistoria <[not public]>2026-06-19 02:08:55 -0400
commit458916fb78dcef3ff77cb29b7ba98d9f646819fc (patch)
tree19cd8b8eef41a1aa00d155bf7953ad2770f91a4c /skill_plans/combat.md
parent97a1a908b9e6e6d3516628c139c9b64262b4fe08 (diff)
downloadthehouseoficarus-458916fb78dcef3ff77cb29b7ba98d9f646819fc.tar.gz
feat: rough pharmacy skill added
Diffstat (limited to 'skill_plans/combat.md')
-rw-r--r--skill_plans/combat.md3402
1 files changed, 3402 insertions, 0 deletions
diff --git a/skill_plans/combat.md b/skill_plans/combat.md
new file mode 100644
index 0000000..67037d6
--- /dev/null
+++ b/skill_plans/combat.md
@@ -0,0 +1,3402 @@
+# Combat System Overhaul — OSRS-Style Attack Types & Per-Type Defense
+
+## 1. Overview
+
+The current combat system uses a single `attack_bonus` / `defense_bonus` per item, ignoring attack types entirely. OSRS uses per-type accuracy bonuses (stab/slash/crush/ranged/magic) and per-type defense bonuses. This overhaul brings the game's combat inline with OSRS mechanics while adapting the naming to the sci-fi theme (Science replaces Magic, Technology replaces Prayer).
+
+**What changes:**
+- `ItemStats` struct: 5 attack bonuses, 5 defense bonuses, 4 damage bonuses (14 fields total)
+- Weapons get an `attack_type` field (stab/slash/crush/ranged/science)
+- Mobs get per-type defense bonuses and attack bonuses
+- Combat formulas use the true OSRS accuracy formula (not the current binary hit/miss)
+- `formulas.go` rewritten with proper OSRS accuracy curve
+- `cmd_attack.go` updated to use per-type bonuses for both player and mob attacks
+- Mobs get an `attack_type` field (default "crush")
+- New `stats` command shows total equipment bonuses
+- `look <item>` shows per-type combat stats
+- All ~66 combat item YAMLs updated with per-type stats
+- All 5 mob YAMLs updated with per-type defense/attack bonuses
+- 8 new example mob YAMLs for varied combat encounters
+
+**What stays the same:**
+- 4 attack styles (accurate/aggressive/defensive/balanced) — style determines level bonus
+- XP distribution formula (dmg * 4, split by style)
+- Death mechanics, flee mechanics, combat timing/tick system
+- WeaponType enum (melee/ranged/science) — still used for equip text + XP routing
+- Player combat level formula
+
+---
+
+## 2. ItemStats Struct
+
+**File:** `internal/object/item.go:67-73`
+
+**Current:**
+```go
+type ItemStats struct {
+ AttackBonus int `yaml:"attack_bonus"`
+ StrengthBonus int `yaml:"strength_bonus"`
+ DefenseBonus int `yaml:"defense_bonus"`
+ ScienceBonus int `yaml:"science_bonus"`
+ TechnologyBonus int `yaml:"technology_bonus"`
+}
+```
+
+**New:**
+```go
+type ItemStats struct {
+ // Offensive accuracy bonuses
+ StabAttack int `yaml:"stab_attack"`
+ SlashAttack int `yaml:"slash_attack"`
+ CrushAttack int `yaml:"crush_attack"`
+ ScienceAttack int `yaml:"science_attack"`
+ RangedAttack int `yaml:"ranged_attack"`
+
+ // Defensive bonuses
+ StabDefense int `yaml:"stab_defense"`
+ SlashDefense int `yaml:"slash_defense"`
+ CrushDefense int `yaml:"crush_defense"`
+ ScienceDefense int `yaml:"science_defense"`
+ RangedDefense int `yaml:"ranged_defense"`
+
+ // Damage bonuses
+ StrengthBonus int `yaml:"strength_bonus"`
+ RangedStrength int `yaml:"ranged_strength"`
+ ScienceDamage int `yaml:"science_damage"`
+ TechnologyBonus int `yaml:"technology_bonus"`
+}
+```
+
+**Notes:**
+- `AttackBonus` and `DefenseBonus` YAML tags are removed entirely. All item YAMLs will be updated to use the new per-type fields.
+- `ScienceBonus` is removed. It was never used in combat. The new `ScienceDamage` field replaces it for future science combat.
+- `TechnologyBonus` stays — it will be used in the Technology skill (Prayer equivalent) in the future.
+- The old `attack` YAML tag (used by bows/arrows/bolts, silently ignored because the Go struct had `attack_bonus`) is also removed. Those items move to `ranged_attack` and `ranged_strength`.
+
+**Backward Compatibility:** None needed. All YAML files will be rewritten. The old YAML tags (`attack_bonus`, `defense_bonus`, `attack`) will no longer be recognized and will be silently ignored by `gopkg.in/yaml.v3` (which is the current behavior for unknown fields). No `UnmarshalYAML` shim needed.
+
+---
+
+## 3. Weapon Attack Types
+
+**File:** `internal/object/item.go:33-59` — add field to `ItemDef`
+
+**Add this field to `ItemDef`:**
+```go
+AttackType string `yaml:"attack_type"` // "stab", "slash", "crush", "ranged", "science"
+```
+
+Insert it after `WeaponType` (line 41), before `Stats`:
+```go
+type ItemDef struct {
+ ID string `yaml:"id"`
+ Name string `yaml:"name"`
+ Color string `yaml:"color"`
+ Description string `yaml:"description"`
+ Value int `yaml:"value"`
+ Stackable bool `yaml:"stackable"`
+ EquipSlot EquipSlot `yaml:"equip_slot"`
+ WeaponType WeaponType `yaml:"weapon_type"`
+ AttackType string `yaml:"attack_type"` // NEW
+ Stats ItemStats `yaml:"stats"`
+ Speed float64 `yaml:"speed"`
+ // ... rest unchanged
+}
+```
+
+**Default attack type mapping** (used when `attack_type` is empty):
+- If `WeaponType == "ranged"` → `"ranged"`
+- If `WeaponType == "science"` → `"science"`
+- Unarmed (no weapon) → `"crush"`
+- Otherwise → `"crush"` (safest default for melee)
+
+Weapons should always have `attack_type` set explicitly in YAML. The default is only a fallback.
+
+**Attack type assignments for existing weapons:**
+
+| Weapon Category | attack_type | Reasoning |
+|-----------------|-------------|-----------|
+| Swords (all tiers) | slash | Swords are slashing weapons |
+| Daggers (all tiers) | stab | Daggers are stabbing weapons |
+| Axes (all tiers) | slash | Axes are slashing weapons (also tools) |
+| Pickaxes (all tiers) | stab | Pickaxes stab/pierce (also tools) |
+| Bows (all types) | ranged | Ranged weapons |
+| Unarmed | crush | Fists = crush |
+
+---
+
+## 4. Combat Style Mapping
+
+The 4 existing styles remain. The style determines:
+1. **Level bonus** (which combat level gets +3 or +1)
+2. **XP distribution** (unchanged from current)
+
+The **weapon's `attack_type`** determines:
+1. Which of the player's 5 attack bonus totals is used for the accuracy roll
+2. Which of the mob's 5 defense bonus values is used for the defense roll
+
+**Style → Level Bonus (unchanged from current):**
+
+| Style | Attack Level | Strength Level | Defense Level |
+|-------|-------------|----------------|---------------|
+| Accurate | +3 | +0 | +0 |
+| Aggressive | +0 | +3 | +0 |
+| Defensive | +0 | +0 | +3 |
+| Balanced | +1 | +1 | +1 |
+
+**Style → XP Distribution (unchanged from current):**
+
+| Style | Distribution |
+|-------|-------------|
+| Accurate | 75% Attack, 25% Hitpoints |
+| Aggressive | 75% Strength, 25% Hitpoints |
+| Defensive | 75% Defense, 25% Hitpoints |
+| Balanced | 25% each Attack/Strength/Defense/Hitpoints |
+
+**Ranged style bonuses** (when weapon_type is "ranged"):
+
+| Style | Ranged Level | Defense Level | XP Distribution |
+|-------|-------------|---------------|-----------------|
+| Accurate | +3 | +0 | 75% Ranged, 25% Hitpoints |
+| Aggressive | +3 | +0 | 75% Ranged, 25% Hitpoints |
+| Defensive | +0 | +3 | 75% Ranged, 25% Hitpoints |
+| Balanced | +1 | +1 | 75% Ranged, 25% Hitpoints |
+
+Note: For ranged, accurate and aggressive both give +3 Ranged (matching OSRS "accurate" and "rapid" which both boost ranged). The attack/strength bonuses from `AttackStyleBonus()` are NOT used — Ranged level is used instead.
+
+---
+
+## 5. Updated Combat Formulas
+
+**File:** `internal/combat/formulas.go` — complete rewrite
+
+```go
+package combat
+
+import "math/rand"
+
+// AttackRoll computes the maximum attack roll for accuracy.
+// effectiveLevel = level + styleBonus + 8
+// equipBonus = total equipment bonus for the relevant attack type
+func AttackRoll(level int, styleBonus int, equipBonus int) int {
+ effective := level + styleBonus + 8
+ return effective * (equipBonus + 64)
+}
+
+// DefenseRoll computes the maximum defense roll.
+// effectiveLevel = level + styleBonus + 8
+// equipBonus = total equipment bonus for the relevant defense type
+func DefenseRoll(level int, styleBonus int, equipBonus int) int {
+ effective := level + styleBonus + 8
+ return effective * (equipBonus + 64)
+}
+
+// HitChance returns the probability of hitting (0.0 to 1.0) using the
+// true OSRS accuracy formula.
+func HitChance(attackRoll, defenseRoll int) float64 {
+ a := float64(attackRoll)
+ d := float64(defenseRoll)
+ if a > d {
+ return 1.0 - (d+2.0)/(2.0*(a+1.0))
+ }
+ return a / (2.0*(d+1.0))
+}
+
+// HitCheck uses the OSRS accuracy formula to determine if an attack hits.
+// Returns true if the attack lands.
+func HitCheck(attackRoll, defenseRoll int) bool {
+ chance := HitChance(attackRoll, defenseRoll)
+ return rand.Float64() < chance
+}
+
+// MaxHit computes the maximum melee hit.
+// level = Strength level (or Ranged level for ranged)
+// styleBonus = strength style bonus (+3 aggressive, +1 balanced, 0 otherwise)
+// equipBonus = total equipment strength bonus (or ranged_strength for ranged)
+func MaxHit(level int, styleBonus int, equipBonus int) int {
+ effective := level + styleBonus + 8
+ hit := (effective * (equipBonus + 64)) / 512
+ if hit < 1 {
+ hit = 1
+ }
+ return hit
+}
+
+// RollDamage returns a random damage value from 0 to maxHit inclusive.
+// In OSRS, 0 is a valid damage (a "hit 0" or splash). We keep the
+// current behavior of 1..maxHit to match existing game feel, but this
+// can be changed to 0..maxHit for true OSRS.
+func RollDamage(maxHit int) int {
+ if maxHit <= 0 {
+ return 0
+ }
+ return 1 + rand.Intn(maxHit)
+}
+
+// AttackStyleBonus returns the level bonuses for melee combat styles.
+// For ranged weapons, the caller should use RangedStyleBonus instead.
+func AttackStyleBonus(style string) (attack, strength, defense int) {
+ switch style {
+ case "accurate":
+ return 3, 0, 0
+ case "aggressive":
+ return 0, 3, 0
+ case "defensive":
+ return 0, 0, 3
+ case "balanced":
+ return 1, 1, 1
+ default:
+ return 0, 0, 0
+ }
+}
+
+// RangedStyleBonus returns the level bonuses for ranged combat styles.
+func RangedStyleBonus(style string) (ranged, defense int) {
+ switch style {
+ case "accurate":
+ return 3, 0
+ case "aggressive":
+ return 3, 0
+ case "defensive":
+ return 0, 3
+ case "balanced":
+ return 1, 1
+ default:
+ return 0, 0
+ }
+}
+
+// MobCombatLevel computes a mob's combat level.
+// Uses the standard OSRS NPC formula:
+// floor((attack + strength + defense + hp) / 4)
+func MobCombatLevel(attack, strength, defense, hp int) int {
+ return (attack + strength + defense + hp) / 4
+}
+
+// EquipBonusForAttackType selects the correct attack bonus from a
+// pre-summed equipment stats struct based on the weapon's attack type.
+// attackType is one of: "stab", "slash", "crush", "ranged", "science"
+//
+// This function lives here to avoid circular imports. The caller passes
+// the individual bonus values.
+func SelectAttackBonus(attackType string, stab, slash, crush, science, ranged int) int {
+ switch attackType {
+ case "stab":
+ return stab
+ case "slash":
+ return slash
+ case "crush":
+ return crush
+ case "science":
+ return science
+ case "ranged":
+ return ranged
+ default:
+ return crush
+ }
+}
+
+// SelectDefenseBonus picks the correct defense bonus to oppose a given
+// attack type.
+func SelectDefenseBonus(attackType string, stab, slash, crush, science, ranged int) int {
+ switch attackType {
+ case "stab":
+ return stab
+ case "slash":
+ return slash
+ case "crush":
+ return crush
+ case "science":
+ return science
+ case "ranged":
+ return ranged
+ default:
+ return crush
+ }
+}
+```
+
+**Key differences from current formulas.go:**
+1. `HitCheck` now uses the continuous OSRS accuracy formula instead of a binary comparison. Previously, if `attackRoll > defenseRoll` it was always a hit; if less, always a miss. Now it uses the proper probability curve.
+2. `HitChance` is exposed as a separate function (useful for debugging/display).
+3. `RangedStyleBonus` added for ranged weapons.
+4. `MobCombatLevel` moved here from `utils.go` (optional, can stay in utils.go).
+5. `SelectAttackBonus` and `SelectDefenseBonus` helper functions added.
+6. `AttackRoll`, `DefenseRoll`, `MaxHit` formulas are actually unchanged in structure — they were already OSRS-correct. The change is in what values are passed to them.
+
+---
+
+## 6. Code Changes to cmd_attack.go
+
+**File:** `internal/game/cmd_attack.go`
+
+### 6.1 playerAttack (lines 174-236)
+
+**Current flow:**
+1. Gets style bonuses from `AttackStyleBonus`
+2. Reads main hand weapon's `AttackBonus` and `StrengthBonus`
+3. `attRoll = AttackRoll(Attack level, attBonus, equipAtt)`
+4. `defRoll = DefenseRoll(mob.Defense, 0, 0)` — mob has NO equipment defense
+5. Binary `HitCheck(attRoll, defRoll)`
+6. `MaxHit(Strength level, strBonus, equipStr)`
+
+**New flow:**
+```go
+func (g *Game) playerAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ if g.processConsumeQueue(p, sess) {
+ p.ActionState = &ActionState{Type: ActionEating, TargetName: "food"}
+ return
+ }
+
+ // Determine weapon and attack type
+ attackType := "crush" // unarmed default
+ var weaponType object.WeaponType
+ if itemID, ok := p.Equipment[object.SlotMainHand]; ok {
+ if def, err := g.ItemStore.Load(itemID); err == nil {
+ if def.AttackType != "" {
+ attackType = def.AttackType
+ } else if def.WeaponType == object.WeaponRanged {
+ attackType = "ranged"
+ } else if def.WeaponType == object.WeaponScience {
+ attackType = "science"
+ }
+ weaponType = def.WeaponType
+ }
+ }
+
+ // Sum all equipment bonuses
+ totals := g.playerEquipBonuses(p)
+
+ if weaponType == object.WeaponRanged {
+ // --- RANGED ATTACK ---
+ rangedBonus, defBonus := combat.RangedStyleBonus(string(p.AttackStyle))
+
+ equipAttack := totals.RangedAttack
+ attRoll := combat.AttackRoll(p.Level(player.Ranged), rangedBonus, equipAttack)
+
+ mobDefBonus := combat.SelectDefenseBonus(attackType,
+ mob.StabDefense, mob.SlashDefense, mob.CrushDefense,
+ mob.ScienceDefense, mob.RangedDefense)
+ defRoll := combat.DefenseRoll(mob.Defense, 0, mobDefBonus)
+
+ if combat.HitCheck(attRoll, defRoll) {
+ maxHit := combat.MaxHit(p.Level(player.Ranged), rangedBonus, totals.RangedStrength)
+ dmg := combat.RollDamage(maxHit)
+ // ... apply damage, award XP (ranged style), consume ammo ...
+ _ = defBonus // used if needed for defense-related ranged style
+ }
+ } else {
+ // --- MELEE ATTACK ---
+ attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle))
+
+ equipAttack := combat.SelectAttackBonus(attackType,
+ totals.StabAttack, totals.SlashAttack, totals.CrushAttack,
+ totals.ScienceAttack, totals.RangedAttack)
+ attRoll := combat.AttackRoll(p.Level(player.Attack), attBonus, equipAttack)
+
+ mobDefBonus := combat.SelectDefenseBonus(attackType,
+ mob.StabDefense, mob.SlashDefense, mob.CrushDefense,
+ mob.ScienceDefense, mob.RangedDefense)
+ defRoll := combat.DefenseRoll(mob.Defense, 0, mobDefBonus)
+
+ if combat.HitCheck(attRoll, defRoll) {
+ maxHit := combat.MaxHit(p.Level(player.Strength), strBonus, totals.StrengthBonus)
+ dmg := combat.RollDamage(maxHit)
+ // ... apply damage, award XP (current style) ...
+ }
+ }
+}
+```
+
+**Detailed changes (line-by-line for melee path):**
+
+Replace lines 180-193:
+```go
+// OLD:
+attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle))
+equipAtt := 0
+equipStr := 0
+if itemID, ok := p.Equipment[object.SlotMainHand]; ok {
+ def, err := g.ItemStore.Load(itemID)
+ if err == nil {
+ equipAtt = def.Stats.AttackBonus
+ equipStr = def.Stats.StrengthBonus
+ }
+}
+attRoll := combat.AttackRoll(p.Level(player.Attack), attBonus, equipAtt)
+defRoll := combat.DefenseRoll(mob.Defense, 0, 0)
+
+// NEW:
+attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle))
+
+attackType := "crush"
+if itemID, ok := p.Equipment[object.SlotMainHand]; ok {
+ if def, err := g.ItemStore.Load(itemID); err == nil {
+ if def.AttackType != "" {
+ attackType = def.AttackType
+ }
+ }
+}
+
+totals := g.playerEquipBonuses(p)
+equipAttack := combat.SelectAttackBonus(attackType,
+ totals.StabAttack, totals.SlashAttack, totals.CrushAttack,
+ totals.ScienceAttack, totals.RangedAttack)
+attRoll := combat.AttackRoll(p.Level(player.Attack), attBonus, equipAttack)
+
+mobDefBonus := combat.SelectDefenseBonus(attackType,
+ mob.StabDefense, mob.SlashDefense, mob.CrushDefense,
+ mob.ScienceDefense, mob.RangedDefense)
+defRoll := combat.DefenseRoll(mob.Defense, 0, mobDefBonus)
+```
+
+Replace line 196 (`MaxHit` call):
+```go
+// OLD:
+maxHit := combat.MaxHit(p.Level(player.Strength), strBonus, equipStr)
+
+// NEW:
+maxHit := combat.MaxHit(p.Level(player.Strength), strBonus, totals.StrengthBonus)
+```
+
+### 6.2 mobAttack (lines 238-289)
+
+**Current flow:**
+1. Gets defense style bonus
+2. Sums `DefenseBonus` from ALL equipped items
+3. `attRoll = AttackRoll(mob.Attack, 0, 0)` — mob has NO attack bonus
+4. `defRoll = DefenseRoll(Defense level, defBonus, equipDef)`
+
+**New flow:**
+```go
+func (g *Game) mobAttack(sess *net.Session, p *player.Player, mob *world.MobInstance) {
+ _, _, defStyleBonus := combat.AttackStyleBonus(string(p.AttackStyle))
+
+ // Determine mob's attack type
+ mobAttackType := mob.AttackType
+ if mobAttackType == "" {
+ mobAttackType = "crush"
+ }
+
+ // Mob attack roll: uses mob's attack level + mob's attack bonus
+ attRoll := combat.AttackRoll(mob.Attack, 0, mob.AttackBonus)
+
+ // Player defense roll: uses player's defense level + style bonus +
+ // equipment defense matching the mob's attack type
+ totals := g.playerEquipBonuses(p)
+ equipDef := combat.SelectDefenseBonus(mobAttackType,
+ totals.StabDefense, totals.SlashDefense, totals.CrushDefense,
+ totals.ScienceDefense, totals.RangedDefense)
+ defRoll := combat.DefenseRoll(p.Level(player.Defense), defStyleBonus, equipDef)
+
+ if combat.HitCheck(attRoll, defRoll) {
+ maxHit := combat.MaxHit(mob.Strength, 0, mob.StrengthBonus)
+ dmg := combat.RollDamage(maxHit)
+ // ... rest unchanged (apply damage, save, flee check, etc.)
+ }
+}
+```
+
+**Detailed changes (line-by-line):**
+
+Replace lines 239-250:
+```go
+// OLD:
+_, _, defBonus := combat.AttackStyleBonus(string(p.AttackStyle))
+equipDef := 0
+for _, itemID := range p.Equipment {
+ def, err := g.ItemStore.Load(itemID)
+ if err == nil {
+ equipDef += def.Stats.DefenseBonus
+ }
+}
+attRoll := combat.AttackRoll(mob.Attack, 0, 0)
+defRoll := combat.DefenseRoll(p.Level(player.Defense), defBonus, equipDef)
+
+// NEW:
+_, _, defStyleBonus := combat.AttackStyleBonus(string(p.AttackStyle))
+
+mobAttackType := mob.AttackType
+if mobAttackType == "" {
+ mobAttackType = "crush"
+}
+
+attRoll := combat.AttackRoll(mob.Attack, 0, mob.AttackBonus)
+
+totals := g.playerEquipBonuses(p)
+equipDef := combat.SelectDefenseBonus(mobAttackType,
+ totals.StabDefense, totals.SlashDefense, totals.CrushDefense,
+ totals.ScienceDefense, totals.RangedDefense)
+defRoll := combat.DefenseRoll(p.Level(player.Defense), defStyleBonus, equipDef)
+```
+
+Replace line 253 (`MaxHit` call):
+```go
+// OLD:
+maxHit := combat.MaxHit(mob.Strength, 0, 0)
+
+// NEW:
+maxHit := combat.MaxHit(mob.Strength, 0, mob.StrengthBonus)
+```
+
+### 6.3 awardCombatXP changes for Ranged
+
+**Current** (lines 378-406): Only awards to Attack/Strength/Defense/Hitpoints based on style.
+
+**New:** If the player's weapon_type is "ranged", XP goes to Ranged instead of melee skills.
+
+Add a new parameter to `awardCombatXP`:
+```go
+func (g *Game) awardCombatXP(p *player.Player, dmg int, isRanged bool) ([]xpGain, []player.SkillName) {
+ baseXP := dmg * 4
+ var gains []xpGain
+ var leveledUp []player.SkillName
+
+ if isRanged {
+ // All ranged styles: 75% ranged, 25% hitpoints
+ gains = []xpGain{
+ {string(player.Ranged), baseXP * 3 / 4},
+ {string(player.Hitpoints), baseXP / 4},
+ }
+ } else {
+ switch p.AttackStyle {
+ case player.Accurate:
+ gains = []xpGain{{string(player.Attack), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}}
+ case player.Aggressive:
+ gains = []xpGain{{string(player.Strength), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}}
+ case player.Defensive:
+ gains = []xpGain{{string(player.Defense), baseXP * 3 / 4}, {string(player.Hitpoints), baseXP / 4}}
+ case player.Balanced:
+ quarter := baseXP / 4
+ gains = []xpGain{
+ {string(player.Attack), quarter},
+ {string(player.Strength), quarter},
+ {string(player.Defense), quarter},
+ {string(player.Hitpoints), quarter},
+ }
+ }
+ }
+
+ for _, gain := range gains {
+ if newLevel := p.AddSkillXP(player.SkillName(gain.Skill), gain.XP); newLevel > 0 {
+ leveledUp = append(leveledUp, player.SkillName(gain.Skill))
+ }
+ }
+ g.AccountStore.SaveCharacter(p)
+ return gains, leveledUp
+}
+```
+
+Update call sites:
+- `playerAttack` melee path: `g.awardCombatXP(p, dmg, false)`
+- `playerAttack` ranged path: `g.awardCombatXP(p, dmg, true)`
+
+### 6.4 Ammo Consumption (Ranged)
+
+In `playerAttack`, after a successful ranged attack:
+```go
+if weaponType == object.WeaponRanged {
+ p.AmmoQty--
+ if p.AmmoQty <= 0 {
+ delete(p.Equipment, object.SlotAmmo)
+ p.AmmoQty = 0
+ sess.WriteLine("You've run out of ammo!")
+ combat.LeaveCombat(p.Name)
+ // End combat
+ }
+ g.AccountStore.SaveCharacter(p)
+}
+```
+
+Before combat starts (in `startCombat` or `doAttack`), check for ammo:
+```go
+if weaponDef.WeaponType == object.WeaponRanged {
+ if _, hasAmmo := p.Equipment[object.SlotAmmo]; !hasAmmo || p.AmmoQty <= 0 {
+ sess.WriteLine("You don't have any ammo equipped.")
+ return
+ }
+}
+```
+
+### 6.5 startCombat Style Display
+
+In `startCombat` (line 120), the style display should also show the attack type:
+```go
+attackType := "crush"
+if itemID, ok := p.Equipment[object.SlotMainHand]; ok {
+ if def, err := g.ItemStore.Load(itemID); err == nil && def.AttackType != "" {
+ attackType = def.AttackType
+ }
+}
+styleStr := fmt.Sprintf(" Style: %s (%s, %s)", p.AttackStyle, strings.Join(styleParts, ", "), attackType)
+```
+
+---
+
+## 7. Code Changes to MobDef and MobInstance
+
+**File:** `internal/world/mob.go`
+
+### 7.1 MobDef (lines 20-37)
+
+**Add these fields:**
+```go
+type MobDef struct {
+ ID string `yaml:"id"`
+ Name string `yaml:"name"`
+ Description string `yaml:"description"`
+ BehaviorID string `yaml:"behavior"`
+ IdleDescriptions []string `yaml:"idle_descriptions"`
+ CombatDescriptions []string `yaml:"combat_descriptions"`
+ Attack int `yaml:"attack"`
+ Strength int `yaml:"strength"`
+ Defense int `yaml:"defense"`
+ HP int `yaml:"hp"`
+ Ranged int `yaml:"ranged"` // NEW
+ Science int `yaml:"science"` // NEW
+ Speed float64 `yaml:"speed"`
+ Aggressive bool `yaml:"aggressive"`
+ Protected bool `yaml:"protected"`
+ Unique bool `yaml:"unique"`
+ RespawnTicks float64 `yaml:"respawn_ticks"`
+ Drops DropTable `yaml:"drops"`
+
+ // NEW: Attack bonuses
+ AttackBonus int `yaml:"attack_bonus"` // equipment-equivalent attack bonus
+ StrengthBonus int `yaml:"strength_bonus"` // equipment-equivalent strength bonus
+ RangedBonus int `yaml:"ranged_bonus"` // ranged attack bonus
+ ScienceBonus int `yaml:"science_bonus"` // science attack bonus
+ AttackType string `yaml:"attack_type"` // "stab"/"slash"/"crush"/"ranged"/"science" (default "crush")
+
+ // NEW: Defense bonuses
+ StabDefense int `yaml:"stab_defense"`
+ SlashDefense int `yaml:"slash_defense"`
+ CrushDefense int `yaml:"crush_defense"`
+ ScienceDefense int `yaml:"science_defense"`
+ RangedDefense int `yaml:"ranged_defense"`
+
+ // NEW: Elemental weakness
+ Weakness string `yaml:"weakness"` // "solar"/"hydro"/"eco"/"bio"/""
+}
+```
+
+### 7.2 MobInstance (lines 39-62)
+
+**Add matching fields:**
+```go
+type MobInstance struct {
+ InstanceID string
+ DefID string
+ Name string
+ BehaviorID string
+ HP int
+ MaxHP int
+ Attack int
+ Strength int
+ Defense int
+ Ranged int // NEW
+ Science int // NEW
+ Speed float64
+ Aggressive bool
+ Protected bool
+ Unique bool
+ RespawnTicks float64
+ RoomID int
+ HomeRoomID int
+ Drops DropTable
+ IdleDescription string
+ WanderRooms []int
+ WanderInterval float64
+ WanderTickCounter int
+ regenerateTick int
+
+ // NEW: Attack bonuses
+ AttackBonus int
+ StrengthBonus int
+ RangedBonus int
+ ScienceBonus int
+ AttackType string
+
+ // NEW: Defense bonuses
+ StabDefense int
+ SlashDefense int
+ CrushDefense int
+ ScienceDefense int
+ RangedDefense int
+
+ // NEW: Elemental weakness
+ Weakness string
+}
+```
+
+### 7.3 SeedMobs (lines 213-259)
+
+**Add field propagation** in the `inst := &MobInstance{...}` block (lines 236-258):
+
+After line 245 (`Defense: dw.def.Defense,`), add:
+```go
+Ranged: dw.def.Ranged,
+Science: dw.def.Science,
+```
+
+After line 255 (`Drops: dw.def.Drops,`), add:
+```go
+AttackBonus: dw.def.AttackBonus,
+StrengthBonus: dw.def.StrengthBonus,
+RangedBonus: dw.def.RangedBonus,
+ScienceBonus: dw.def.ScienceBonus,
+AttackType: dw.def.AttackType,
+StabDefense: dw.def.StabDefense,
+SlashDefense: dw.def.SlashDefense,
+CrushDefense: dw.def.CrushDefense,
+ScienceDefense: dw.def.ScienceDefense,
+RangedDefense: dw.def.RangedDefense,
+Weakness: dw.def.Weakness,
+```
+
+### 7.4 mobCombatLevel Update
+
+**File:** `internal/game/utils.go:47-49`
+
+**Current:**
+```go
+func mobCombatLevel(m *world.MobInstance) int {
+ return int(0.25*float64(m.Attack+m.Strength+m.Defense+m.MaxHP) + 0.5)
+}
+```
+
+**New** (unchanged — this formula is already correct for melee-only mobs):
+```go
+func mobCombatLevel(m *world.MobInstance) int {
+ base := float64(m.Defense+m.MaxHP) / 4.0
+ melee := float64(m.Attack+m.Strength) / 4.0
+ ranged := float64(m.Ranged) * 3.0 / 8.0
+ science := float64(m.Science) * 3.0 / 8.0
+ best := melee
+ if ranged > best {
+ best = ranged
+ }
+ if science > best {
+ best = science
+ }
+ return int(base + best + 0.5)
+}
+```
+
+This uses the OSRS NPC combat level formula:
+- `base = (def + hp) / 4`
+- `offensive = max(melee=(atk+str)/4, ranged=rng*3/8, magic=sci*3/8)`
+- `level = floor(base + offensive)`
+
+For existing mobs with no Ranged/Science, this produces the same results as the old formula.
+
+---
+
+## 8. Equipment Bonus Aggregation
+
+**File:** `internal/game/cmd_attack.go` (or a new `internal/game/equip_stats.go`)
+
+Add a helper function that sums all equipped items' stats:
+
+```go
+func (g *Game) playerEquipBonuses(p *player.Player) object.ItemStats {
+ var totals object.ItemStats
+ for _, itemID := range p.Equipment {
+ def, err := g.ItemStore.Load(itemID)
+ if err != nil {
+ continue
+ }
+ totals.StabAttack += def.Stats.StabAttack
+ totals.SlashAttack += def.Stats.SlashAttack
+ totals.CrushAttack += def.Stats.CrushAttack
+ totals.ScienceAttack += def.Stats.ScienceAttack
+ totals.RangedAttack += def.Stats.RangedAttack
+ totals.StabDefense += def.Stats.StabDefense
+ totals.SlashDefense += def.Stats.SlashDefense
+ totals.CrushDefense += def.Stats.CrushDefense
+ totals.ScienceDefense += def.Stats.ScienceDefense
+ totals.RangedDefense += def.Stats.RangedDefense
+ totals.StrengthBonus += def.Stats.StrengthBonus
+ totals.RangedStrength += def.Stats.RangedStrength
+ totals.ScienceDamage += def.Stats.ScienceDamage
+ totals.TechnologyBonus += def.Stats.TechnologyBonus
+ }
+ return totals
+}
+```
+
+This function is called by `playerAttack`, `mobAttack`, the new `stats` command, and `doLookTarget` for comparing equipment.
+
+---
+
+## 9. Equipment Stats Display
+
+**File:** `internal/game/cmd_look.go`
+
+### 9.1 Item Examination (doLookTarget)
+
+When looking at an item (inventory or ground), if the item has any non-zero combat stats, display them in a formatted block.
+
+**Location:** After displaying item name/description/value (lines 508-514 for ground items, lines 526-536 for inventory items).
+
+**Add this helper function:**
+```go
+func (g *Game) showItemStats(sess *net.Session, def *object.ItemDef) {
+ s := def.Stats
+ hasAttack := s.StabAttack != 0 || s.SlashAttack != 0 || s.CrushAttack != 0 ||
+ s.ScienceAttack != 0 || s.RangedAttack != 0
+ hasDefense := s.StabDefense != 0 || s.SlashDefense != 0 || s.CrushDefense != 0 ||
+ s.ScienceDefense != 0 || s.RangedDefense != 0
+ hasOther := s.StrengthBonus != 0 || s.RangedStrength != 0 ||
+ s.ScienceDamage != 0 || s.TechnologyBonus != 0
+
+ if !hasAttack && !hasDefense && !hasOther {
+ return
+ }
+
+ sess.WriteLine("")
+ if hasAttack || hasDefense {
+ sess.WriteLine(" Attack bonuses: Defense bonuses:")
+ sess.WriteLine(fmt.Sprintf(" Stab: %+4d Stab: %+4d", s.StabAttack, s.StabDefense))
+ sess.WriteLine(fmt.Sprintf(" Slash: %+4d Slash: %+4d", s.SlashAttack, s.SlashDefense))
+ sess.WriteLine(fmt.Sprintf(" Crush: %+4d Crush: %+4d", s.CrushAttack, s.CrushDefense))
+ sess.WriteLine(fmt.Sprintf(" Science:%+4d Science:%+4d", s.ScienceAttack, s.ScienceDefense))
+ sess.WriteLine(fmt.Sprintf(" Ranged: %+4d Ranged: %+4d", s.RangedAttack, s.RangedDefense))
+ }
+ if hasOther {
+ sess.WriteLine("")
+ sess.WriteLine(" Other bonuses:")
+ if s.StrengthBonus != 0 {
+ sess.WriteLine(fmt.Sprintf(" Melee strength: %+d", s.StrengthBonus))
+ }
+ if s.RangedStrength != 0 {
+ sess.WriteLine(fmt.Sprintf(" Ranged strength: %+d", s.RangedStrength))
+ }
+ if s.ScienceDamage != 0 {
+ sess.WriteLine(fmt.Sprintf(" Science damage: %+d", s.ScienceDamage))
+ }
+ if s.TechnologyBonus != 0 {
+ sess.WriteLine(fmt.Sprintf(" Technology: %+d", s.TechnologyBonus))
+ }
+ }
+ if def.AttackType != "" {
+ sess.WriteLine(fmt.Sprintf(" Attack type: %s", def.AttackType))
+ }
+ if def.Speed > 0 {
+ sess.WriteLine(fmt.Sprintf(" Speed: %.0f", def.Speed))
+ }
+}
+```
+
+**Call it** in the ground item branch (after line 513):
+```go
+g.showItemStats(sess, def)
+```
+
+**Call it** in the inventory item branch (after line 531):
+```go
+g.showItemStats(sess, def)
+```
+
+### 9.2 Mob Examination
+
+Update the mob examination section in `doLookTarget` (lines 451-457) to show per-type bonuses:
+
+```go
+// Replace lines 451-457:
+sess.WriteLines(
+ "",
+ fmt.Sprintf(" Attack: %d Strength: %d Defense: %d", best.Attack, best.Strength, best.Defense),
+ fmt.Sprintf(" HP: %d/%d", best.HP, best.MaxHP),
+)
+if best.StabDefense != 0 || best.SlashDefense != 0 || best.CrushDefense != 0 ||
+ best.ScienceDefense != 0 || best.RangedDefense != 0 {
+ sess.WriteLines(
+ "",
+ " Defense bonuses:",
+ fmt.Sprintf(" Stab: %+d Slash: %+d Crush: %+d", best.StabDefense, best.SlashDefense, best.CrushDefense),
+ fmt.Sprintf(" Science: %+d Ranged: %+d", best.ScienceDefense, best.RangedDefense),
+ )
+}
+if best.Weakness != "" {
+ sess.WriteLine(fmt.Sprintf(" Weakness: %s", best.Weakness))
+}
+```
+
+---
+
+## 10. Equipment Totals Command — `stats`
+
+**New file:** `internal/game/cmd_stats.go`
+
+```go
+package game
+
+import (
+ "fmt"
+ "strings"
+
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
+)
+
+func (g *Game) doStats(sess *net.Session) {
+ p := sess.Player.(*player.Player)
+ totals := g.playerEquipBonuses(p)
+
+ attackType := "crush"
+ weaponName := "unarmed"
+ if itemID, ok := p.Equipment[object.SlotMainHand]; ok {
+ if def, err := g.ItemStore.Load(itemID); err == nil {
+ weaponName = def.Name
+ if def.AttackType != "" {
+ attackType = def.AttackType
+ }
+ }
+ }
+
+ sess.WriteLines(
+ "",
+ fmt.Sprintf("Weapon: %s (attack type: %s)", weaponName, attackType),
+ "",
+ "Attack bonuses: Defense bonuses:",
+ fmt.Sprintf(" Stab: %+4d Stab: %+4d", totals.StabAttack, totals.StabDefense),
+ fmt.Sprintf(" Slash: %+4d Slash: %+4d", totals.SlashAttack, totals.SlashDefense),
+ fmt.Sprintf(" Crush: %+4d Crush: %+4d", totals.CrushAttack, totals.CrushDefense),
+ fmt.Sprintf(" Science:%+4d Science:%+4d", totals.ScienceAttack, totals.ScienceDefense),
+ fmt.Sprintf(" Ranged: %+4d Ranged: %+4d", totals.RangedAttack, totals.RangedDefense),
+ "",
+ "Other bonuses:",
+ fmt.Sprintf(" Melee strength: %+d", totals.StrengthBonus),
+ fmt.Sprintf(" Ranged strength: %+d", totals.RangedStrength),
+ fmt.Sprintf(" Science damage: %+d", totals.ScienceDamage),
+ fmt.Sprintf(" Technology: %+d", totals.TechnologyBonus),
+ )
+
+ // Show effective accuracy and max hit for current setup
+ attBonus, strBonus, _ := combat.AttackStyleBonus(string(p.AttackStyle))
+ equipAtt := combat.SelectAttackBonus(attackType,
+ totals.StabAttack, totals.SlashAttack, totals.CrushAttack,
+ totals.ScienceAttack, totals.RangedAttack)
+ attRoll := combat.AttackRoll(p.Level(player.Attack), attBonus, equipAtt)
+ maxHit := combat.MaxHit(p.Level(player.Strength), strBonus, totals.StrengthBonus)
+
+ sess.WriteLines(
+ "",
+ fmt.Sprintf("Style: %s Attack roll: %d Max hit: %d",
+ p.AttackStyle, attRoll, maxHit),
+ )
+
+ _ = strings.Join // avoid unused import (the real code won't need this)
+}
+```
+
+**Register the command:**
+
+**File:** `internal/game/game.go`
+
+In `classifyCommand` (line 138), add `"stats"` to the `ClassInstant` case:
+```go
+case "say", "score", "sc", "inventory", "i", "inv",
+ "look", "l", "exits", "help",
+ "map", "option", "options", "alias", "unalias",
+ "description", "desc", "queued", "color", "colors",
+ "colortable", "prompt", "style", "stats": // ADD "stats"
+ return ClassInstant
+```
+
+In `executeCommand` (find the switch statement for instant commands), add:
+```go
+case "stats":
+ g.doStats(sess)
+```
+
+---
+
+## 11. Item YAML Migration
+
+Every item with combat stats needs updated YAML. Below is the complete set.
+
+### 11.1 Melee Weapons — Swords (attack_type: slash)
+
+Stats are OSRS-proportional. Swords have high slash, moderate stab, no crush.
+
+**Bronze Sword** (`data/items/bronze_sword.yaml`):
+```yaml
+id: bronze_sword
+name: bronze sword
+color: "178"
+description: "A basic bronze sword."
+value: 12
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: slash
+stats:
+ stab_attack: 4
+ slash_attack: 10
+ crush_attack: -2
+ strength_bonus: 7
+speed: 4
+```
+
+**Iron Sword** (`data/items/iron_sword.yaml`):
+```yaml
+id: iron_sword
+name: iron sword
+color: "250"
+description: "An iron sword."
+value: 30
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: slash
+stats:
+ stab_attack: 7
+ slash_attack: 15
+ crush_attack: -2
+ strength_bonus: 10
+speed: 4
+```
+
+**Steel Sword** (`data/items/steel_sword.yaml`):
+```yaml
+id: steel_sword
+name: steel sword
+color: "253"
+description: "A steel sword."
+value: 100
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: slash
+stats:
+ stab_attack: 10
+ slash_attack: 22
+ crush_attack: -2
+ strength_bonus: 14
+speed: 4
+```
+
+**Mithril Sword** (`data/items/mithril_sword.yaml`):
+```yaml
+id: mithril_sword
+name: mithril sword
+color: "75"
+description: "A mithril sword."
+value: 250
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: slash
+stats:
+ stab_attack: 14
+ slash_attack: 30
+ crush_attack: -2
+ strength_bonus: 20
+speed: 4
+```
+
+**Adamant Sword** (`data/items/adamant_sword.yaml`):
+```yaml
+id: adamant_sword
+name: adamant sword
+color: "120"
+description: "An adamant sword."
+value: 700
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: slash
+stats:
+ stab_attack: 20
+ slash_attack: 40
+ crush_attack: -2
+ strength_bonus: 27
+speed: 4
+```
+
+**Rune Sword** (`data/items/rune_sword.yaml`):
+```yaml
+id: rune_sword
+name: rune sword
+color: "87"
+description: "A rune sword."
+value: 10000
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: slash
+stats:
+ stab_attack: 27
+ slash_attack: 52
+ crush_attack: -2
+ strength_bonus: 36
+speed: 4
+```
+
+### 11.2 Melee Weapons — Daggers (attack_type: stab)
+
+Daggers have high stab, moderate slash, negative crush. Fast speed.
+
+**Bronze Dagger** (`data/items/bronze_dagger.yaml`):
+```yaml
+id: bronze_dagger
+name: bronze dagger
+color: "178"
+description: "A small bronze dagger."
+value: 8
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: stab
+stats:
+ stab_attack: 5
+ slash_attack: 3
+ crush_attack: -1
+ strength_bonus: 4
+speed: 3
+```
+
+**Iron Dagger** (`data/items/iron_dagger.yaml`):
+```yaml
+id: iron_dagger
+name: iron dagger
+color: "250"
+description: "A small iron dagger."
+value: 20
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: stab
+stats:
+ stab_attack: 8
+ slash_attack: 5
+ crush_attack: -1
+ strength_bonus: 7
+speed: 3
+```
+
+**Steel Dagger** (`data/items/steel_dagger.yaml`):
+```yaml
+id: steel_dagger
+name: steel dagger
+color: "253"
+description: "A small steel dagger."
+value: 60
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: stab
+stats:
+ stab_attack: 12
+ slash_attack: 7
+ crush_attack: -1
+ strength_bonus: 10
+speed: 3
+```
+
+**Mithril Dagger** (`data/items/mithril_dagger.yaml`):
+```yaml
+id: mithril_dagger
+name: mithril dagger
+color: "75"
+description: "A small mithril dagger."
+value: 150
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: stab
+stats:
+ stab_attack: 17
+ slash_attack: 10
+ crush_attack: -1
+ strength_bonus: 14
+speed: 3
+```
+
+**Adamant Dagger** (`data/items/adamant_dagger.yaml`):
+```yaml
+id: adamant_dagger
+name: adamant dagger
+color: "120"
+description: "A small adamant dagger."
+value: 400
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: stab
+stats:
+ stab_attack: 23
+ slash_attack: 14
+ crush_attack: -1
+ strength_bonus: 19
+speed: 3
+```
+
+**Rune Dagger** (`data/items/rune_dagger.yaml`):
+```yaml
+id: rune_dagger
+name: rune dagger
+color: "87"
+description: "A small rune dagger."
+value: 5000
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: stab
+stats:
+ stab_attack: 30
+ slash_attack: 18
+ crush_attack: -1
+ strength_bonus: 24
+speed: 3
+```
+
+### 11.3 Melee Weapons — Axes (attack_type: slash)
+
+Axes are also tools. They have moderate slash, some crush, weak stab. Slower than swords.
+
+**Bronze Axe** (`data/items/bronze_axe.yaml`):
+```yaml
+id: bronze_axe
+name: bronze axe
+color: "178"
+description: "A sturdy bronze axe, good for woodcutting."
+value: 10
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: slash
+stats:
+ stab_attack: -2
+ slash_attack: 7
+ crush_attack: 5
+ strength_bonus: 5
+speed: 5
+tool_type: axe
+tool_speed: 4
+```
+
+**Iron Axe** (`data/items/iron_axe.yaml`):
+```yaml
+id: iron_axe
+name: iron axe
+color: "250"
+description: "A sturdy iron axe, good for woodcutting."
+value: 25
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: slash
+stats:
+ stab_attack: -2
+ slash_attack: 10
+ crush_attack: 7
+ strength_bonus: 8
+speed: 5
+tool_type: axe
+tool_speed: 3.5
+```
+
+**Steel Axe** (`data/items/steel_axe.yaml`):
+```yaml
+id: steel_axe
+name: steel axe
+color: "253"
+description: "A sturdy steel axe, good for woodcutting."
+value: 75
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: slash
+stats:
+ stab_attack: -2
+ slash_attack: 15
+ crush_attack: 11
+ strength_bonus: 12
+speed: 5
+tool_type: axe
+tool_speed: 3
+```
+
+**Black Axe** (`data/items/black_axe.yaml`):
+```yaml
+id: black_axe
+name: black axe
+color: "240"
+description: "A black axe, good for woodcutting."
+value: 150
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: slash
+stats:
+ stab_attack: -2
+ slash_attack: 18
+ crush_attack: 14
+ strength_bonus: 14
+speed: 5
+tool_type: axe
+tool_speed: 2.5
+```
+
+**Mithril Axe** (`data/items/mithril_axe.yaml`):
+```yaml
+id: mithril_axe
+name: mithril axe
+color: "75"
+description: "A sturdy mithril axe, good for woodcutting."
+value: 200
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: slash
+stats:
+ stab_attack: -2
+ slash_attack: 21
+ crush_attack: 16
+ strength_bonus: 17
+speed: 5
+tool_type: axe
+tool_speed: 2.5
+```
+
+**Adamant Axe** (`data/items/adamant_axe.yaml`):
+```yaml
+id: adamant_axe
+name: adamant axe
+color: "120"
+description: "A sturdy adamant axe, good for woodcutting."
+value: 500
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: slash
+stats:
+ stab_attack: -2
+ slash_attack: 29
+ crush_attack: 22
+ strength_bonus: 24
+speed: 5
+tool_type: axe
+tool_speed: 2
+```
+
+**Rune Axe** (`data/items/rune_axe.yaml`):
+```yaml
+id: rune_axe
+name: rune axe
+color: "87"
+description: "A sturdy rune axe, good for woodcutting."
+value: 5000
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: slash
+stats:
+ stab_attack: -2
+ slash_attack: 38
+ crush_attack: 29
+ strength_bonus: 32
+speed: 5
+tool_type: axe
+tool_speed: 1.5
+```
+
+**Dragon Axe** (`data/items/dragon_axe.yaml`):
+```yaml
+id: dragon_axe
+name: dragon axe
+color: "196"
+description: "A powerful dragon axe."
+value: 50000
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: slash
+stats:
+ stab_attack: -2
+ slash_attack: 43
+ crush_attack: 32
+ strength_bonus: 36
+speed: 5
+tool_type: axe
+tool_speed: 1
+```
+
+### 11.4 Melee Weapons — Pickaxes (attack_type: stab)
+
+Pickaxes stab/pierce. Tool items with moderate combat stats.
+
+**Bronze Pickaxe** (`data/items/bronze_pickaxe.yaml`):
+```yaml
+id: bronze_pickaxe
+name: bronze pickaxe
+color: "178"
+description: "A sturdy bronze pickaxe, good for mining."
+value: 10
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: stab
+stats:
+ stab_attack: 4
+ slash_attack: -2
+ crush_attack: 2
+ strength_bonus: 3
+speed: 5
+tool_type: pickaxe
+tool_speed: 2
+```
+
+**Iron Pickaxe** (`data/items/iron_pickaxe.yaml`):
+```yaml
+id: iron_pickaxe
+name: iron pickaxe
+color: "250"
+description: "A sturdy iron pickaxe, good for mining."
+value: 25
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: stab
+stats:
+ stab_attack: 7
+ slash_attack: -2
+ crush_attack: 3
+ strength_bonus: 5
+speed: 5
+tool_type: pickaxe
+tool_speed: 1.5
+```
+
+**Steel Pickaxe** (`data/items/steel_pickaxe.yaml`):
+```yaml
+id: steel_pickaxe
+name: steel pickaxe
+color: "253"
+description: "A sturdy steel pickaxe, good for mining."
+value: 75
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: stab
+stats:
+ stab_attack: 10
+ slash_attack: -2
+ crush_attack: 4
+ strength_bonus: 7
+speed: 5
+tool_type: pickaxe
+tool_speed: 1.25
+```
+
+**Black Pickaxe** (`data/items/black_pickaxe.yaml`):
+```yaml
+id: black_pickaxe
+name: black pickaxe
+color: "240"
+description: "A black pickaxe, good for mining."
+value: 150
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: stab
+stats:
+ stab_attack: 14
+ slash_attack: -2
+ crush_attack: 6
+ strength_bonus: 9
+speed: 5
+tool_type: pickaxe
+tool_speed: 1
+```
+
+**Mithril Pickaxe** (`data/items/mithril_pickaxe.yaml`):
+```yaml
+id: mithril_pickaxe
+name: mithril pickaxe
+color: "75"
+description: "A sturdy mithril pickaxe, good for mining."
+value: 200
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: stab
+stats:
+ stab_attack: 17
+ slash_attack: -2
+ crush_attack: 8
+ strength_bonus: 12
+speed: 5
+tool_type: pickaxe
+tool_speed: 1
+```
+
+**Adamant Pickaxe** (`data/items/adamant_pickaxe.yaml`):
+```yaml
+id: adamant_pickaxe
+name: adamant pickaxe
+color: "120"
+description: "A sturdy adamant pickaxe, good for mining."
+value: 500
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: stab
+stats:
+ stab_attack: 23
+ slash_attack: -2
+ crush_attack: 10
+ strength_bonus: 16
+speed: 5
+tool_type: pickaxe
+tool_speed: 0.75
+```
+
+**Rune Pickaxe** (`data/items/rune_pickaxe.yaml`):
+```yaml
+id: rune_pickaxe
+name: rune pickaxe
+color: "87"
+description: "A sturdy rune pickaxe, good for mining."
+value: 5000
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: stab
+stats:
+ stab_attack: 30
+ slash_attack: -2
+ crush_attack: 14
+ strength_bonus: 22
+speed: 5
+tool_type: pickaxe
+tool_speed: 0.5
+```
+
+**Dragon Pickaxe** (`data/items/dragon_pickaxe.yaml`):
+```yaml
+id: dragon_pickaxe
+name: dragon pickaxe
+color: "196"
+description: "A powerful dragon pickaxe."
+value: 50000
+stackable: false
+equip_slot: main_hand
+weapon_type: melee
+attack_type: stab
+stats:
+ stab_attack: 34
+ slash_attack: -2
+ crush_attack: 16
+ strength_bonus: 26
+speed: 5
+tool_type: pickaxe
+tool_speed: 0.25
+```
+
+### 11.5 Armor — Med Helms
+
+Med helms have moderate defense, balanced across melee types. Slightly negative science defense (metal conducts energy), neutral ranged.
+
+**Bronze Med Helm** (`data/items/bronze_med_helm.yaml`):
+```yaml
+id: bronze_med_helm
+name: bronze med helm
+color: "178"
+description: "A bronze medium helmet."
+value: 8
+stackable: false
+equip_slot: head
+stats:
+ stab_defense: 3
+ slash_defense: 4
+ crush_defense: 2
+ science_defense: -1
+ ranged_defense: 3
+```
+
+**Iron Med Helm** (`data/items/iron_med_helm.yaml`):
+```yaml
+id: iron_med_helm
+name: iron med helm
+color: "250"
+description: "An iron medium helmet."
+value: 20
+stackable: false
+equip_slot: head
+stats:
+ stab_defense: 5
+ slash_defense: 6
+ crush_defense: 3
+ science_defense: -1
+ ranged_defense: 5
+```
+
+**Steel Med Helm** (`data/items/steel_med_helm.yaml`):
+```yaml
+id: steel_med_helm
+name: steel med helm
+color: "253"
+description: "A steel medium helmet."
+value: 60
+stackable: false
+equip_slot: head
+stats:
+ stab_defense: 7
+ slash_defense: 9
+ crush_defense: 5
+ science_defense: -2
+ ranged_defense: 7
+```
+
+**Mithril Med Helm** (`data/items/mithril_med_helm.yaml`):
+```yaml
+id: mithril_med_helm
+name: mithril med helm
+color: "75"
+description: "A mithril medium helmet."
+value: 150
+stackable: false
+equip_slot: head
+stats:
+ stab_defense: 10
+ slash_defense: 12
+ crush_defense: 7
+ science_defense: -3
+ ranged_defense: 10
+```
+
+**Adamant Med Helm** (`data/items/adamant_med_helm.yaml`):
+```yaml
+id: adamant_med_helm
+name: adamant med helm
+color: "120"
+description: "An adamant medium helmet."
+value: 400
+stackable: false
+equip_slot: head
+stats:
+ stab_defense: 14
+ slash_defense: 16
+ crush_defense: 10
+ science_defense: -3
+ ranged_defense: 14
+```
+
+**Rune Med Helm** (`data/items/rune_med_helm.yaml`):
+```yaml
+id: rune_med_helm
+name: rune med helm
+color: "87"
+description: "A rune medium helmet."
+value: 5000
+stackable: false
+equip_slot: head
+stats:
+ stab_defense: 19
+ slash_defense: 22
+ crush_defense: 13
+ science_defense: -4
+ ranged_defense: 19
+```
+
+### 11.6 Armor — Full Helms
+
+Full helms have higher defense than med helms. Larger science penalty.
+
+**Bronze Full Helm** (`data/items/bronze_full_helm.yaml`):
+```yaml
+id: bronze_full_helm
+name: bronze full helm
+color: "178"
+description: "A bronze full helmet."
+value: 20
+stackable: false
+equip_slot: head
+stats:
+ stab_defense: 5
+ slash_defense: 6
+ crush_defense: 4
+ science_defense: -3
+ ranged_defense: 5
+```
+
+**Iron Full Helm** (`data/items/iron_full_helm.yaml`):
+```yaml
+id: iron_full_helm
+name: iron full helm
+color: "250"
+description: "An iron full helmet."
+value: 50
+stackable: false
+equip_slot: head
+stats:
+ stab_defense: 8
+ slash_defense: 9
+ crush_defense: 6
+ science_defense: -3
+ ranged_defense: 8
+```
+
+**Steel Full Helm** (`data/items/steel_full_helm.yaml`):
+```yaml
+id: steel_full_helm
+name: steel full helm
+color: "253"
+description: "A steel full helmet."
+value: 150
+stackable: false
+equip_slot: head
+stats:
+ stab_defense: 12
+ slash_defense: 13
+ crush_defense: 9
+ science_defense: -5
+ ranged_defense: 12
+```
+
+**Mithril Full Helm** (`data/items/mithril_full_helm.yaml`):
+```yaml
+id: mithril_full_helm
+name: mithril full helm
+color: "75"
+description: "A mithril full helmet."
+value: 400
+stackable: false
+equip_slot: head
+stats:
+ stab_defense: 16
+ slash_defense: 18
+ crush_defense: 12
+ science_defense: -6
+ ranged_defense: 16
+```
+
+**Adamant Full Helm** (`data/items/adamant_full_helm.yaml`):
+```yaml
+id: adamant_full_helm
+name: adamant full helm
+color: "120"
+description: "An adamant full helmet."
+value: 1000
+stackable: false
+equip_slot: head
+stats:
+ stab_defense: 22
+ slash_defense: 24
+ crush_defense: 16
+ science_defense: -8
+ ranged_defense: 22
+```
+
+**Rune Full Helm** (`data/items/rune_full_helm.yaml`):
+```yaml
+id: rune_full_helm
+name: rune full helm
+color: "87"
+description: "A rune full helmet."
+value: 12000
+stackable: false
+equip_slot: head
+stats:
+ stab_defense: 30
+ slash_defense: 33
+ crush_defense: 22
+ science_defense: -11
+ ranged_defense: 30
+```
+
+### 11.7 Armor — Platebodies
+
+Platebodies are the highest-defense torso armor. Heavy science penalty, strong melee defense.
+
+**Bronze Platebody** (`data/items/bronze_platebody.yaml`):
+```yaml
+id: bronze_platebody
+name: bronze platebody
+color: "178"
+description: "A bronze platebody."
+value: 60
+stackable: false
+equip_slot: torso
+stats:
+ stab_defense: 12
+ slash_defense: 15
+ crush_defense: 10
+ science_defense: -10
+ ranged_defense: 12
+```
+
+**Iron Platebody** (`data/items/iron_platebody.yaml`):
+```yaml
+id: iron_platebody
+name: iron platebody
+color: "250"
+description: "An iron platebody."
+value: 150
+stackable: false
+equip_slot: torso
+stats:
+ stab_defense: 18
+ slash_defense: 22
+ crush_defense: 14
+ science_defense: -10
+ ranged_defense: 18
+```
+
+**Steel Platebody** (`data/items/steel_platebody.yaml`):
+```yaml
+id: steel_platebody
+name: steel platebody
+color: "253"
+description: "A steel platebody."
+value: 500
+stackable: false
+equip_slot: torso
+stats:
+ stab_defense: 27
+ slash_defense: 32
+ crush_defense: 22
+ science_defense: -15
+ ranged_defense: 27
+```
+
+**Mithril Platebody** (`data/items/mithril_platebody.yaml`):
+```yaml
+id: mithril_platebody
+name: mithril platebody
+color: "75"
+description: "A mithril platebody."
+value: 1500
+stackable: false
+equip_slot: torso
+stats:
+ stab_defense: 36
+ slash_defense: 42
+ crush_defense: 30
+ science_defense: -20
+ ranged_defense: 36
+```
+
+**Adamant Platebody** (`data/items/adamant_platebody.yaml`):
+```yaml
+id: adamant_platebody
+name: adamant platebody
+color: "120"
+description: "An adamant platebody."
+value: 5000
+stackable: false
+equip_slot: torso
+stats:
+ stab_defense: 49
+ slash_defense: 55
+ crush_defense: 40
+ science_defense: -25
+ ranged_defense: 49
+```
+
+**Rune Platebody** (`data/items/rune_platebody.yaml`):
+```yaml
+id: rune_platebody
+name: rune platebody
+color: "87"
+description: "A rune platebody."
+value: 40000
+stackable: false
+equip_slot: torso
+stats:
+ stab_defense: 65
+ slash_defense: 72
+ crush_defense: 52
+ science_defense: -30
+ ranged_defense: 65
+```
+
+### 11.8 Ranged Weapons — Bows
+
+Bows use the `ranged_attack` field. The bug where they used `attack:` (which was silently ignored since ItemStats has no `attack` YAML tag) is fixed. Longbows have higher accuracy than shortbows but are slower.
+
+**Shortbow** (`data/items/shortbow.yaml`):
+```yaml
+id: shortbow
+name: shortbow
+color: "137"
+description: "A shortbow."
+value: 10
+equip_slot: main_hand
+weapon_type: ranged
+attack_type: ranged
+speed: 4
+stats:
+ ranged_attack: 8
+```
+
+**Oak Shortbow** (`data/items/oak_shortbow.yaml`):
+```yaml
+id: oak_shortbow
+name: oak shortbow
+color: "136"
+description: "An oak shortbow."
+value: 40
+equip_slot: main_hand
+weapon_type: ranged
+attack_type: ranged
+speed: 4
+stats:
+ ranged_attack: 14
+```
+
+**Willow Shortbow** (`data/items/willow_shortbow.yaml`):
+```yaml
+id: willow_shortbow
+name: willow shortbow
+color: "107"
+description: "A willow shortbow."
+value: 160
+equip_slot: main_hand
+weapon_type: ranged
+attack_type: ranged
+speed: 4
+stats:
+ ranged_attack: 20
+```
+
+**Maple Shortbow** (`data/items/maple_shortbow.yaml`):
+```yaml
+id: maple_shortbow
+name: maple shortbow
+color: "172"
+description: "A maple shortbow."
+value: 640
+equip_slot: main_hand
+weapon_type: ranged
+attack_type: ranged
+speed: 4
+stats:
+ ranged_attack: 29
+```
+
+**Yew Shortbow** (`data/items/yew_shortbow.yaml`):
+```yaml
+id: yew_shortbow
+name: yew shortbow
+color: "94"
+description: "A yew shortbow."
+value: 1600
+equip_slot: main_hand
+weapon_type: ranged
+attack_type: ranged
+speed: 4
+stats:
+ ranged_attack: 47
+```
+
+**Magic Shortbow** (`data/items/magic_shortbow.yaml`):
+```yaml
+id: magic_shortbow
+name: magic shortbow
+color: "63"
+description: "A magic shortbow."
+value: 4000
+equip_slot: main_hand
+weapon_type: ranged
+attack_type: ranged
+speed: 4
+stats:
+ ranged_attack: 69
+```
+
+**Longbow** (`data/items/longbow.yaml`):
+```yaml
+id: longbow
+name: longbow
+color: "137"
+description: "A longbow."
+value: 20
+equip_slot: main_hand
+weapon_type: ranged
+attack_type: ranged
+speed: 6
+stats:
+ ranged_attack: 8
+```
+
+**Oak Longbow** (`data/items/oak_longbow.yaml`):
+```yaml
+id: oak_longbow
+name: oak longbow
+color: "136"
+description: "An oak longbow."
+value: 80
+equip_slot: main_hand
+weapon_type: ranged
+attack_type: ranged
+speed: 6
+stats:
+ ranged_attack: 14
+```
+
+**Willow Longbow** (`data/items/willow_longbow.yaml`):
+```yaml
+id: willow_longbow
+name: willow longbow
+color: "107"
+description: "A willow longbow."
+value: 320
+equip_slot: main_hand
+weapon_type: ranged
+attack_type: ranged
+speed: 6
+stats:
+ ranged_attack: 20
+```
+
+**Maple Longbow** (`data/items/maple_longbow.yaml`):
+```yaml
+id: maple_longbow
+name: maple longbow
+color: "172"
+description: "A maple longbow."
+value: 1280
+equip_slot: main_hand
+weapon_type: ranged
+attack_type: ranged
+speed: 6
+stats:
+ ranged_attack: 29
+```
+
+**Yew Longbow** (`data/items/yew_longbow.yaml`):
+```yaml
+id: yew_longbow
+name: yew longbow
+color: "94"
+description: "A yew longbow."
+value: 2000
+equip_slot: main_hand
+weapon_type: ranged
+attack_type: ranged
+speed: 6
+stats:
+ ranged_attack: 47
+```
+
+**Magic Longbow** (`data/items/magic_longbow.yaml`):
+```yaml
+id: magic_longbow
+name: magic longbow
+color: "63"
+description: "A magic longbow."
+value: 6000
+equip_slot: main_hand
+weapon_type: ranged
+attack_type: ranged
+speed: 6
+stats:
+ ranged_attack: 69
+```
+
+### 11.9 Ranged Ammo — Arrows
+
+Arrows now use `ranged_strength` instead of the broken `attack:` field. Arrow accuracy (ranged_attack) is 0 — all accuracy comes from the bow. Damage comes from the arrow's `ranged_strength`.
+
+**Bronze Arrow** (`data/items/bronze_arrow.yaml`):
+```yaml
+id: bronze_arrow
+name: bronze arrow
+color: "178"
+description: "A bronze-tipped arrow."
+value: 1
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_strength: 7
+```
+
+**Iron Arrow** (`data/items/iron_arrow.yaml`):
+```yaml
+id: iron_arrow
+name: iron arrow
+color: "250"
+description: "An iron-tipped arrow."
+value: 2
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_strength: 10
+```
+
+**Steel Arrow** (`data/items/steel_arrow.yaml`):
+```yaml
+id: steel_arrow
+name: steel arrow
+color: "253"
+description: "A steel-tipped arrow."
+value: 5
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_strength: 16
+```
+
+**Mithril Arrow** (`data/items/mithril_arrow.yaml`):
+```yaml
+id: mithril_arrow
+name: mithril arrow
+color: "75"
+description: "A mithril-tipped arrow."
+value: 10
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_strength: 22
+```
+
+**Adamant Arrow** (`data/items/adamant_arrow.yaml`):
+```yaml
+id: adamant_arrow
+name: adamant arrow
+color: "120"
+description: "An adamant-tipped arrow."
+value: 20
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_strength: 31
+```
+
+**Rune Arrow** (`data/items/rune_arrow.yaml`):
+```yaml
+id: rune_arrow
+name: rune arrow
+color: "87"
+description: "A rune-tipped arrow."
+value: 40
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_strength: 49
+```
+
+### 11.10 Ranged Ammo — Bolts
+
+Same pattern as arrows. `ranged_strength` for damage.
+
+**Bronze Bolts** (`data/items/bronze_bolts.yaml`):
+```yaml
+id: bronze_bolts
+name: bronze bolts
+color: "178"
+description: "Bronze crossbow bolts."
+value: 1
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_strength: 10
+```
+
+**Iron Bolts** (`data/items/iron_bolts.yaml`):
+```yaml
+id: iron_bolts
+name: iron bolts
+color: "250"
+description: "Iron crossbow bolts."
+value: 3
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_strength: 17
+```
+
+**Steel Bolts** (`data/items/steel_bolts.yaml`):
+```yaml
+id: steel_bolts
+name: steel bolts
+color: "253"
+description: "Steel crossbow bolts."
+value: 6
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_strength: 25
+```
+
+**Mithril Bolts** (`data/items/mithril_bolts.yaml`):
+```yaml
+id: mithril_bolts
+name: mithril bolts
+color: "75"
+description: "Mithril crossbow bolts."
+value: 12
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_strength: 32
+```
+
+**Adamant Bolts** (`data/items/adamant_bolts.yaml`):
+```yaml
+id: adamant_bolts
+name: adamant bolts
+color: "120"
+description: "Adamant crossbow bolts."
+value: 24
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_strength: 43
+```
+
+**Rune Bolts** (`data/items/rune_bolts.yaml`):
+```yaml
+id: rune_bolts
+name: rune bolts
+color: "87"
+description: "Rune crossbow bolts."
+value: 48
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_strength: 55
+```
+
+**Sapphire Bolts** (`data/items/sapphire_bolts.yaml`):
+```yaml
+id: sapphire_bolts
+name: sapphire bolts
+color: "69"
+description: "Mithril bolts tipped with sapphire."
+value: 40
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_strength: 36
+```
+
+**Emerald Bolts** (`data/items/emerald_bolts.yaml`):
+```yaml
+id: emerald_bolts
+name: emerald bolts
+color: "83"
+description: "Mithril bolts tipped with emerald."
+value: 65
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_strength: 39
+```
+
+**Ruby Bolts** (`data/items/ruby_bolts.yaml`):
+```yaml
+id: ruby_bolts
+name: ruby bolts
+color: "196"
+description: "Adamant bolts tipped with ruby."
+value: 130
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_strength: 49
+```
+
+**Diamond Bolts** (`data/items/diamond_bolts.yaml`):
+```yaml
+id: diamond_bolts
+name: diamond bolts
+color: "255"
+description: "Adamant bolts tipped with diamond."
+value: 230
+stackable: true
+equip_slot: ammo
+stats:
+ ranged_strength: 52
+```
+
+### 11.11 Ranged Armor — Leather
+
+Leather armor provides ranged defense and slight melee defense. Positive ranged defense, negative science defense.
+
+**Leather Cowl** (`data/items/leather_cowl.yaml`):
+```yaml
+id: leather_cowl
+name: leather cowl
+color: "130"
+description: "A leather cowl."
+value: 15
+equip_slot: head
+stats:
+ stab_defense: 1
+ slash_defense: 2
+ crush_defense: 1
+ science_defense: 2
+ ranged_defense: 4
+```
+
+**Leather Body** (`data/items/leather_body.yaml`):
+```yaml
+id: leather_body
+name: leather body
+color: "130"
+description: "A leather body armour."
+value: 25
+equip_slot: torso
+stats:
+ stab_defense: 4
+ slash_defense: 7
+ crush_defense: 5
+ science_defense: 4
+ ranged_defense: 10
+```
+
+**Leather Chaps** (`data/items/leather_chaps.yaml`):
+```yaml
+id: leather_chaps
+name: leather chaps
+color: "130"
+description: "A pair of leather chaps."
+value: 20
+equip_slot: legs
+stats:
+ stab_defense: 2
+ slash_defense: 4
+ crush_defense: 3
+ science_defense: 3
+ ranged_defense: 6
+```
+
+**Leather Gloves** (`data/items/leather_gloves.yaml`):
+```yaml
+id: leather_gloves
+name: leather gloves
+color: "130"
+description: "A pair of leather gloves."
+value: 10
+equip_slot: hands
+stats:
+ stab_defense: 1
+ slash_defense: 1
+ crush_defense: 1
+ science_defense: 1
+ ranged_defense: 2
+```
+
+**Leather Vambraces** (`data/items/leather_vambraces.yaml`):
+```yaml
+id: leather_vambraces
+name: leather vambraces
+color: "130"
+description: "A pair of leather vambraces."
+value: 18
+equip_slot: hands
+stats:
+ stab_defense: 2
+ slash_defense: 2
+ crush_defense: 2
+ science_defense: 2
+ ranged_defense: 4
+ ranged_attack: 4
+```
+
+**Leather Boots** (`data/items/leather_boots.yaml`):
+```yaml
+id: leather_boots
+name: leather boots
+color: "130"
+description: "A pair of leather boots."
+value: 12
+equip_slot: feet
+stats:
+ stab_defense: 1
+ slash_defense: 1
+ crush_defense: 1
+ science_defense: 1
+ ranged_defense: 2
+```
+
+**Coif** (`data/items/coif.yaml`):
+```yaml
+id: coif
+name: coif
+color: "94"
+description: "A hard leather coif."
+value: 45
+equip_slot: head
+stats:
+ stab_defense: 2
+ slash_defense: 4
+ crush_defense: 2
+ science_defense: 4
+ ranged_defense: 6
+ ranged_attack: 2
+```
+
+**Hard Leather Body** (`data/items/hard_leather_body.yaml`):
+```yaml
+id: hard_leather_body
+name: hard leather body
+color: "94"
+description: "A hardened leather body armour."
+value: 60
+equip_slot: torso
+stats:
+ stab_defense: 8
+ slash_defense: 12
+ crush_defense: 8
+ science_defense: 6
+ ranged_defense: 14
+```
+
+**Hard Leather Shield** (`data/items/hard_leather_shield.yaml`):
+```yaml
+id: hard_leather_shield
+name: hard leather shield
+color: "94"
+description: "A shield made of hardened leather."
+value: 55
+equip_slot: off_hand
+stats:
+ stab_defense: 5
+ slash_defense: 7
+ crush_defense: 5
+ science_defense: 4
+ ranged_defense: 8
+```
+
+### 11.12 Non-Combat Equipment (no changes needed)
+
+These items have no combat stats and need no changes:
+- Graceful set (hat, torso, legs, gloves, boots, cape) — movement only
+- Cape of Agility — movement only
+- Jewelry (rings, necklaces, amulets, bracelets) — currently no stats
+- Tools without equip (needle, chisel, etc.)
+
+### 11.13 Unfinished Bow Items (no combat stats)
+
+These are fletching intermediates with no combat purpose:
+- `shortbow_u`, `longbow_u`, `oak_shortbow_u`, `oak_longbow_u`, `willow_shortbow_u`, `willow_longbow_u`, `maple_shortbow_u`, `maple_longbow_u`, `yew_shortbow_u`, `yew_longbow_u`, `magic_shortbow_u`, `magic_longbow_u`
+- `arrow_shaft`, `headless_arrow`, all `*_arrowtips`, all `*_bolts_unf`, all `*_bolt_tips`
+
+These have no equip slot or stats and need no changes.
+
+---
+
+## 12. Mob YAML Updates
+
+### 12.1 Cow (`data/mobs/cow.yaml`)
+
+```yaml
+id: cow
+name: cow
+description: "A placid dairy cow, chewing cud."
+attack: 1
+strength: 1
+defense: 1
+hp: 8
+speed: 5
+aggressive: false
+respawn_ticks: 30
+attack_type: crush
+stab_defense: 1
+slash_defense: 1
+crush_defense: 1
+science_defense: 0
+ranged_defense: 0
+idle_descriptions:
+ - "moos softly"
+ - "chews on some grass"
+ - "swishes its tail lazily"
+ - "stares at you with big brown eyes"
+drops:
+ remains: bones
+ loot:
+ - item_id: cowhide
+ weight: 100
+ - item_id: raw_beef
+ weight: 100
+```
+
+### 12.2 Man (`data/mobs/man.yaml`)
+
+```yaml
+id: man
+name: man
+description: "A shabby-looking man loitering in the town square."
+combat_descriptions:
+ - "is engaged in a fight to the death with %s"
+ - "is getting pummelled by %s"
+ - "is locked in combat with %s"
+ - "trades blows with %s"
+ - "circles warily around %s"
+idle_descriptions:
+ - "scribbles something in a small notebook"
+ - "gazes skyward at the clouds"
+ - "leans against a wall, looking bored"
+ - "scratches his head thoughtfully"
+ - "stares off into the distance"
+ - "adjusts his tunic and stretches"
+attack: 1
+strength: 1
+defense: 1
+hp: 7
+speed: 5
+aggressive: false
+respawn_ticks: 30
+attack_type: crush
+stab_defense: 0
+slash_defense: 0
+crush_defense: 0
+science_defense: 0
+ranged_defense: 0
+drops:
+ remains: "bones"
+ loot:
+ - item_id: "credits"
+ weight: 98
+ quantity: 10
+ - item_id: "credits"
+ weight: 2
+ quantity: 150
+```
+
+### 12.3 Guard (`data/mobs/guard.yaml`)
+
+```yaml
+id: guard
+name: Guard
+description: "A stern-looking guard in weathered armor."
+behavior: guard_talk
+unique: true
+protected: true
+attack: 5
+strength: 5
+defense: 5
+hp: 30
+speed: 5
+aggressive: false
+respawn_ticks: 60
+attack_type: slash
+attack_bonus: 8
+strength_bonus: 6
+stab_defense: 12
+slash_defense: 15
+crush_defense: 10
+science_defense: -5
+ranged_defense: 12
+idle_descriptions:
+ - "scans the area with a watchful eye"
+ - "adjusts the grip on his weapon"
+ - "nods curtly at passersby"
+ - "leans against the gate, arms folded"
+```
+
+### 12.4 Newbie Trainer (`data/mobs/newbie_trainer.yaml`)
+
+```yaml
+id: newbie_trainer
+name: Newbie Trainer
+description: "A friendly-looking instructor ready to help new adventurers."
+unique: true
+protected: true
+attack: 1
+strength: 1
+defense: 1
+hp: 50
+speed: 5
+aggressive: false
+respawn_ticks: 30
+attack_type: crush
+stab_defense: 0
+slash_defense: 0
+crush_defense: 0
+science_defense: 0
+ranged_defense: 0
+idle_descriptions:
+ - "reviews a training manual"
+ - "adjusts a practice dummy"
+ - "demonstrates a sword stance to no one in particular"
+ - "offers a friendly nod to passersby"
+ - "polishes a wooden shield"
+ - "checks a stopwatch and jots something down"
+```
+
+### 12.5 Tanner (`data/mobs/tanner.yaml`)
+
+```yaml
+id: tanner
+name: Tanner
+description: "A weathered craftsman with stained hands and a leather apron."
+behavior: tanner_talk
+unique: true
+protected: true
+hp: 50
+attack: 1
+strength: 1
+defense: 1
+speed: 5
+aggressive: false
+respawn_ticks: 30
+attack_type: crush
+stab_defense: 0
+slash_defense: 0
+crush_defense: 0
+science_defense: 0
+ranged_defense: 0
+idle_descriptions:
+ - "scrapes at a piece of hide"
+ - "examines a stack of leathers"
+ - "sharpens a tanning knife"
+ - "wipes sweat from his brow"
+```
+
+---
+
+## 13. Example New Mobs
+
+These new mob YAMLs demonstrate the full stat spectrum. Place in `data/mobs/`.
+
+### 13.1 Rat (Level 1)
+
+```yaml
+id: rat
+name: rat
+description: "A large, mangy rat with beady red eyes."
+attack: 1
+strength: 1
+defense: 1
+hp: 2
+speed: 4
+aggressive: false
+respawn_ticks: 20
+attack_type: crush
+stab_defense: 0
+slash_defense: 0
+crush_defense: 0
+science_defense: 0
+ranged_defense: 0
+idle_descriptions:
+ - "sniffs around the ground"
+ - "nibbles on something"
+ - "scurries in circles"
+drops:
+ remains: bones
+```
+
+### 13.2 Goblin (Level 5)
+
+Weak to crush — goblins wear thin leather that resists slashing poorly and does nothing against blunt force.
+
+```yaml
+id: goblin
+name: goblin
+description: "A small green-skinned goblin with a rusty blade."
+attack: 1
+strength: 3
+defense: 3
+hp: 12
+speed: 5
+aggressive: true
+respawn_ticks: 25
+attack_type: slash
+attack_bonus: 3
+strength_bonus: 2
+stab_defense: 4
+slash_defense: 3
+crush_defense: -2
+science_defense: 0
+ranged_defense: 3
+idle_descriptions:
+ - "mutters something unintelligible"
+ - "waves a rusty blade around"
+ - "picks its nose"
+ - "kicks a pebble"
+drops:
+ remains: bones
+ loot:
+ - item_id: credits
+ weight: 80
+ quantity: 5
+ - item_id: credits
+ weight: 20
+ quantity: 25
+```
+
+### 13.3 Skeleton (Level 22)
+
+Skeletons have high slash defense (blade passes through gaps in bones), weak to crush (bones shatter).
+
+```yaml
+id: skeleton
+name: skeleton
+description: "An animated pile of bones, held together by dark energy."
+attack: 8
+strength: 6
+defense: 8
+hp: 22
+speed: 5
+aggressive: true
+respawn_ticks: 30
+attack_type: stab
+attack_bonus: 6
+strength_bonus: 4
+stab_defense: 10
+slash_defense: 18
+crush_defense: -5
+science_defense: -8
+ranged_defense: 10
+idle_descriptions:
+ - "rattles ominously"
+ - "clicks its jaw open and shut"
+ - "stares with empty eye sockets"
+drops:
+ remains: bones
+ loot:
+ - item_id: credits
+ weight: 70
+ quantity: 30
+ - item_id: credits
+ weight: 30
+ quantity: 100
+```
+
+### 13.4 Zombie (Level 28)
+
+High slash defense (rotting flesh gives way), weak to crush and solar (sci-fi fire equivalent).
+
+```yaml
+id: zombie
+name: zombie
+description: "A shambling corpse reanimated by residual asteroid radiation."
+attack: 10
+strength: 8
+defense: 8
+hp: 30
+speed: 6
+aggressive: true
+respawn_ticks: 35
+attack_type: crush
+attack_bonus: 4
+strength_bonus: 6
+stab_defense: 8
+slash_defense: 20
+crush_defense: -8
+science_defense: -15
+ranged_defense: 8
+weakness: solar
+idle_descriptions:
+ - "groans and shuffles forward"
+ - "reaches out with decaying arms"
+ - "stumbles and catches itself"
+ - "stares blankly ahead"
+drops:
+ remains: bones
+ loot:
+ - item_id: credits
+ weight: 60
+ quantity: 50
+ - item_id: credits
+ weight: 40
+ quantity: 200
+```
+
+### 13.5 Moss Giant (Level 42)
+
+High HP, moderate defense across the board. Big and slow.
+
+```yaml
+id: moss_giant
+name: moss giant
+description: "A towering creature covered in moss and vines, its rocky skin dripping with moisture."
+attack: 12
+strength: 14
+defense: 10
+hp: 60
+speed: 7
+aggressive: true
+respawn_ticks: 45
+attack_type: crush
+attack_bonus: 10
+strength_bonus: 12
+stab_defense: 15
+slash_defense: 12
+crush_defense: 18
+science_defense: -5
+ranged_defense: 15
+idle_descriptions:
+ - "surveys its territory with dull eyes"
+ - "pulls a vine from its shoulder"
+ - "stamps the ground, sending tremors"
+ - "scratches its mossy hide"
+drops:
+ remains: bones
+ loot:
+ - item_id: credits
+ weight: 50
+ quantity: 120
+ - item_id: credits
+ weight: 50
+ quantity: 500
+```
+
+### 13.6 Lesser Drone (Level 53)
+
+A science-based mob. High melee defense, weak to ranged. Uses science attacks.
+
+```yaml
+id: lesser_drone
+name: lesser drone
+description: "A floating metallic drone with a pulsing energy core. Its surface crackles with electric discharge."
+attack: 1
+strength: 1
+defense: 18
+hp: 55
+ranged: 0
+science: 20
+speed: 4
+aggressive: true
+respawn_ticks: 50
+attack_type: science
+attack_bonus: 0
+science_bonus: 22
+strength_bonus: 0
+stab_defense: 35
+slash_defense: 35
+crush_defense: 30
+science_defense: 20
+ranged_defense: -10
+idle_descriptions:
+ - "hovers silently, scanning the area"
+ - "emits a low electronic hum"
+ - "projects a thin beam of light across the floor"
+ - "rotates slowly in place"
+drops:
+ loot:
+ - item_id: scrap_metal
+ weight: 60
+ - item_id: credits
+ weight: 40
+ quantity: 300
+```
+
+### 13.7 Greater Drone (Level 83)
+
+Stronger version of the lesser drone.
+
+```yaml
+id: greater_drone
+name: greater drone
+description: "A large, heavily armored drone with multiple energy emitters. Red warning lights pulse along its hull."
+attack: 1
+strength: 1
+defense: 30
+hp: 120
+ranged: 0
+science: 40
+speed: 4
+aggressive: true
+respawn_ticks: 80
+attack_type: science
+attack_bonus: 0
+science_bonus: 45
+strength_bonus: 0
+stab_defense: 55
+slash_defense: 55
+crush_defense: 50
+science_defense: 40
+ranged_defense: -20
+idle_descriptions:
+ - "projects a defensive energy shield"
+ - "recalibrates its targeting array"
+ - "emits a warning klaxon"
+ - "deploys sensor probes"
+drops:
+ loot:
+ - item_id: scrap_metal
+ weight: 40
+ quantity: 3
+ - item_id: credits
+ weight: 60
+ quantity: 1500
+```
+
+### 13.8 Iron Sentinel (Level 92)
+
+A heavily armored mechanical guardian. Very high melee defense, weak to science attacks.
+
+```yaml
+id: iron_sentinel
+name: iron sentinel
+description: "A massive bipedal automaton forged from pre-regression alloys. Its joints grind with each deliberate step."
+attack: 25
+strength: 30
+defense: 45
+hp: 180
+speed: 6
+aggressive: true
+respawn_ticks: 100
+attack_type: crush
+attack_bonus: 40
+strength_bonus: 35
+stab_defense: 80
+slash_defense: 80
+crush_defense: 70
+science_defense: -30
+ranged_defense: 50
+idle_descriptions:
+ - "stands motionless, servos whining softly"
+ - "turns its armored head with a mechanical grind"
+ - "vents steam from exhaust ports"
+ - "scans the area with a red optical sensor"
+drops:
+ loot:
+ - item_id: scrap_metal
+ weight: 30
+ quantity: 5
+ - item_id: runite_bar
+ weight: 10
+ - item_id: credits
+ weight: 60
+ quantity: 5000
+```
+
+---
+
+## 14. Ranged Combat Integration
+
+### 14.1 Attack Flow
+
+When `weapon_type == "ranged"`:
+
+1. **Ammo check** (in `doAttack`, before `startCombat`):
+ ```go
+ if weaponDef.WeaponType == object.WeaponRanged {
+ if _, hasAmmo := p.Equipment[object.SlotAmmo]; !hasAmmo || p.AmmoQty <= 0 {
+ sess.WriteLine("You don't have any ammo equipped.")
+ return
+ }
+ }
+ ```
+
+2. **Attack roll**: Uses `Ranged` level + `RangedStyleBonus` + total `ranged_attack` from all equipment (bow + ammo + any ranged-boosting armor).
+
+3. **Max hit**: Uses `Ranged` level + `RangedStyleBonus` (strength portion) + total `ranged_strength` from equipment (primarily the ammo).
+
+4. **Mob defense**: Uses mob's `ranged_defense` value.
+
+5. **Ammo consumption**: After each attack (hit or miss), decrement `p.AmmoQty` by 1. If ammo reaches 0, unequip it and end combat.
+
+6. **XP**: 75% Ranged, 25% Hitpoints (all ranged styles).
+
+### 14.2 RangedStyleBonus
+
+| Style | Ranged Level Bonus | Defense Level Bonus | XP |
+|-------|-------------------|--------------------|----|
+| Accurate | +3 | +0 | 75% rng, 25% hp |
+| Aggressive | +3 | +0 | 75% rng, 25% hp |
+| Defensive | +0 | +3 | 75% rng, 25% hp |
+| Balanced | +1 | +1 | 75% rng, 25% hp |
+
+### 14.3 Ranged Max Hit Formula
+
+```
+effective_ranged = Ranged_level + style_bonus + 8
+max_hit = (effective_ranged * (total_ranged_strength + 64)) / 512
+```
+
+The `ranged_strength` comes primarily from ammo (arrows/bolts). Some equipment might also add ranged_strength in the future.
+
+---
+
+## 15. Science Combat Integration
+
+Science (magic) combat is covered in a separate plan (`science.md`). Key integration points:
+
+- `attack_type: "science"` — uses Science level + `science_attack` bonus for accuracy
+- Max hit is determined by the equipped "mod" (spell/ability), NOT by equipment
+- Mob defense against science uses `science_defense`
+- XP: 75% Science, 25% Hitpoints
+- Science-based mobs (like drones) use `science` level + `science_bonus` for their attacks
+
+For this combat overhaul, the struct fields (`ScienceAttack`, `ScienceDefense`, `ScienceDamage`) are added but the science combat flow implementation is deferred.
+
+---
+
+## 16. Backward Compatibility
+
+### Approach: Full YAML rewrite (recommended)
+
+All item YAML files are updated to use the new field names. The old `attack_bonus`, `defense_bonus`, and `attack` YAML tags are simply removed from the Go struct. Since `gopkg.in/yaml.v3` silently ignores unknown fields during unmarshalling, any YAML files accidentally left with old tags will simply have those values ignored (zeroed).
+
+**No custom `UnmarshalYAML`** is needed because:
+1. We control all the YAML files — they're in the repo under `data/`
+2. Player data (accounts/characters) doesn't store `ItemStats` — equipment is stored as item IDs
+3. There's no external API consuming these fields
+
+**Migration checklist:**
+- [x] All `attack_bonus: N` → replaced by `stab_attack`/`slash_attack`/`crush_attack` per weapon type
+- [x] All `defense_bonus: N` → replaced by per-type defense fields
+- [x] All `attack: N` (bows/arrows/bolts) → replaced by `ranged_attack` or `ranged_strength`
+- [x] All `strength_bonus: N` → kept as-is (same YAML tag)
+
+### What if old-format items are loaded?
+
+If someone has a custom item YAML with the old format, all combat stats will be 0. The item will have no combat effectiveness. This is acceptable — the items were already broken (bows used `attack:` which was silently ignored).
+
+---
+
+## 17. Help Files
+
+### 17.1 Combat Help (`data/help/combat.yaml`)
+
+```yaml
+id: combat
+name: Combat
+aliases:
+ - fighting
+ - fight
+content: |
+ COMBAT
+
+ Attack a mob with: attack <mob>
+
+ Your accuracy depends on your attack type (stab, slash, crush, ranged,
+ or science) and your total equipment bonus for that type. Your weapon
+ determines the attack type.
+
+ Mobs have per-type defense bonuses. A skeleton with high slash defense
+ but weak crush defense takes more hits from a mace than a sword.
+
+ Use the 'style' command to change your combat style:
+ accurate - +3 attack level, XP to attack
+ aggressive - +3 strength level, XP to strength
+ defensive - +3 defense level, XP to defense
+ balanced - +1 to all, XP split evenly
+
+ Use 'stats' to see your total equipment bonuses and current attack roll.
+ Use 'look <item>' to see an item's combat stats.
+```
+
+### 17.2 Stats Help (`data/help/stats.yaml`)
+
+```yaml
+id: stats
+name: Stats
+aliases:
+ - bonuses
+ - equipment stats
+content: |
+ STATS
+
+ The 'stats' command shows your total equipment bonuses from all worn
+ items, your current weapon and attack type, and your effective attack
+ roll and max hit.
+
+ Attack bonuses affect accuracy. The relevant bonus is determined by
+ your weapon's attack type (stab, slash, crush, ranged, or science).
+
+ Defense bonuses reduce incoming damage. When a mob attacks you, the
+ game uses your defense bonus matching the mob's attack type.
+
+ Strength bonus increases your max melee hit.
+ Ranged strength increases your max ranged hit.
+ Science damage increases your max science hit.
+ Technology bonus will affect technology abilities (future).
+```
+
+### 17.3 Style Help (`data/help/style.yaml`)
+
+If this file already exists, update it. If not, create it:
+
+```yaml
+id: style
+name: Style
+aliases:
+ - combat style
+ - attack style
+content: |
+ STYLE
+
+ Usage: style <name>
+
+ Changes your combat style. Prefix matching is supported.
+
+ Styles:
+ accurate - +3 to attack level, XP goes to Attack
+ aggressive - +3 to strength level, XP goes to Strength
+ defensive - +3 to defense level, XP goes to Defense
+ balanced - +1 to all three, XP split evenly
+
+ For ranged weapons:
+ accurate - +3 to ranged level
+ aggressive - +3 to ranged level (same as accurate for ranged)
+ defensive - +3 to defense level
+ balanced - +1 to ranged and defense
+
+ Your style does NOT change your attack type. The attack type is
+ determined by your weapon (e.g., swords use slash, daggers use stab).
+
+ Use 'stats' to see how your current style affects your attack roll.
+```
+
+---
+
+## 18. Implementation Order
+
+Step-by-step checklist. Each step should compile and pass `make vet` before moving to the next.
+
+### Phase 1: Struct Changes (no behavior change yet)
+
+- [ ] **Step 1:** Update `ItemStats` struct in `internal/object/item.go:67-73`
+ - Replace the 5 fields with the 14 new fields
+ - Add `AttackType string` field to `ItemDef` after line 41
+
+- [ ] **Step 2:** Update `MobDef` struct in `internal/world/mob.go:20-37`
+ - Add: `Ranged`, `Science`, `AttackBonus`, `StrengthBonus`, `RangedBonus`, `ScienceBonus`, `AttackType`
+ - Add: `StabDefense`, `SlashDefense`, `CrushDefense`, `ScienceDefense`, `RangedDefense`
+ - Add: `Weakness`
+
+- [ ] **Step 3:** Update `MobInstance` struct in `internal/world/mob.go:39-62`
+ - Mirror all new fields from MobDef
+
+- [ ] **Step 4:** Update `SeedMobs` in `internal/world/mob.go:213-259`
+ - Copy all new fields from def to instance
+
+- [ ] **Step 5:** Fix all compilation errors from removed `AttackBonus`/`DefenseBonus`/`ScienceBonus` fields
+ - `cmd_attack.go:187` — references `def.Stats.AttackBonus` → temporarily use `def.Stats.SlashAttack`
+ - `cmd_attack.go:188` — references `def.Stats.StrengthBonus` → OK, field still exists
+ - `cmd_attack.go:245` — references `def.Stats.DefenseBonus` → temporarily use sum of all defense types
+ - Any other references to removed fields
+
+- [ ] **Step 6:** Run `make vet` — should pass with no errors
+
+### Phase 2: Item YAML Updates
+
+- [ ] **Step 7:** Update all 6 sword YAMLs (Section 11.1)
+- [ ] **Step 8:** Update all 6 dagger YAMLs (Section 11.2)
+- [ ] **Step 9:** Update all 8 axe YAMLs (Section 11.3)
+- [ ] **Step 10:** Update all 8 pickaxe YAMLs (Section 11.4)
+- [ ] **Step 11:** Update all 6 med helm YAMLs (Section 11.5)
+- [ ] **Step 12:** Update all 6 full helm YAMLs (Section 11.6)
+- [ ] **Step 13:** Update all 6 platebody YAMLs (Section 11.7)
+- [ ] **Step 14:** Update all 12 bow YAMLs (Section 11.8)
+- [ ] **Step 15:** Update all 6 arrow YAMLs (Section 11.9)
+- [ ] **Step 16:** Update all 10 bolt YAMLs (Section 11.10)
+- [ ] **Step 17:** Update all 9 leather/ranged armor YAMLs (Section 11.11)
+
+### Phase 3: Mob YAML Updates
+
+- [ ] **Step 18:** Update all 5 existing mob YAMLs (Section 12)
+- [ ] **Step 19:** Create 8 new mob YAMLs (Section 13)
+
+### Phase 4: Formula & Combat Logic
+
+- [ ] **Step 20:** Rewrite `internal/combat/formulas.go` (Section 5)
+ - New `HitChance` function
+ - Updated `HitCheck` to use probability curve
+ - Add `RangedStyleBonus`
+ - Add `SelectAttackBonus` and `SelectDefenseBonus`
+
+- [ ] **Step 21:** Add `playerEquipBonuses` helper (Section 8)
+ - New file `internal/game/equip_stats.go` or add to `cmd_attack.go`
+
+- [ ] **Step 22:** Update `playerAttack` in `cmd_attack.go` (Section 6.1)
+ - Determine attack type from weapon
+ - Use `playerEquipBonuses` for totals
+ - Use `SelectAttackBonus` for the right attack bonus
+ - Use `SelectDefenseBonus` for mob's matching defense
+ - Melee path: use Attack level + melee style bonus
+ - Ranged path: use Ranged level + ranged style bonus + ammo consumption
+
+- [ ] **Step 23:** Update `mobAttack` in `cmd_attack.go` (Section 6.2)
+ - Use mob's `AttackType` (default "crush")
+ - Use mob's `AttackBonus` in attack roll
+ - Use mob's `StrengthBonus` in max hit
+ - Use player's matching defense bonus for the mob's attack type
+
+- [ ] **Step 24:** Update `awardCombatXP` (Section 6.3)
+ - Add `isRanged` parameter
+ - Ranged path: 75% Ranged, 25% Hitpoints
+
+- [ ] **Step 25:** Update `mobCombatLevel` in `utils.go` (Section 7.4)
+ - New formula accounting for Ranged/Science levels
+
+- [ ] **Step 26:** Add ammo check in `doAttack` (Section 6.4)
+
+- [ ] **Step 27:** Run `make vet` and `make test`
+
+### Phase 5: Display Changes
+
+- [ ] **Step 28:** Add `showItemStats` helper to `cmd_look.go` (Section 9.1)
+- [ ] **Step 29:** Call `showItemStats` in item examination paths in `doLookTarget`
+- [ ] **Step 30:** Update mob examination in `doLookTarget` (Section 9.2)
+- [ ] **Step 31:** Create `cmd_stats.go` with `doStats` command (Section 10)
+- [ ] **Step 32:** Register `stats` command in `classifyCommand` and `executeCommand` in `game.go`
+- [ ] **Step 33:** Create help YAML files (Section 17)
+
+### Phase 6: Final Verification
+
+- [ ] **Step 34:** Run `make build`
+- [ ] **Step 35:** Run `make test`
+- [ ] **Step 36:** Run `make vet`
+- [ ] **Step 37:** Manual testing: equip weapons, check `stats`, `look <item>`, attack mobs, verify damage/accuracy feel reasonable
+- [ ] **Step 38:** Verify ranged combat works with ammo consumption
+- [ ] **Step 39:** Verify `look <mob>` shows defense bonuses
+
+---
+
+## Appendix A: Complete Item Stats Reference Table
+
+### Melee Weapons
+
+| Item | Type | Stab | Slash | Crush | Str | Speed |
+|------|------|------|-------|-------|-----|-------|
+| bronze_dagger | stab | +5 | +3 | -1 | +4 | 3 |
+| iron_dagger | stab | +8 | +5 | -1 | +7 | 3 |
+| steel_dagger | stab | +12 | +7 | -1 | +10 | 3 |
+| mithril_dagger | stab | +17 | +10 | -1 | +14 | 3 |
+| adamant_dagger | stab | +23 | +14 | -1 | +19 | 3 |
+| rune_dagger | stab | +30 | +18 | -1 | +24 | 3 |
+| bronze_sword | slash | +4 | +10 | -2 | +7 | 4 |
+| iron_sword | slash | +7 | +15 | -2 | +10 | 4 |
+| steel_sword | slash | +10 | +22 | -2 | +14 | 4 |
+| mithril_sword | slash | +14 | +30 | -2 | +20 | 4 |
+| adamant_sword | slash | +20 | +40 | -2 | +27 | 4 |
+| rune_sword | slash | +27 | +52 | -2 | +36 | 4 |
+| bronze_axe | slash | -2 | +7 | +5 | +5 | 5 |
+| iron_axe | slash | -2 | +10 | +7 | +8 | 5 |
+| steel_axe | slash | -2 | +15 | +11 | +12 | 5 |
+| black_axe | slash | -2 | +18 | +14 | +14 | 5 |
+| mithril_axe | slash | -2 | +21 | +16 | +17 | 5 |
+| adamant_axe | slash | -2 | +29 | +22 | +24 | 5 |
+| rune_axe | slash | -2 | +38 | +29 | +32 | 5 |
+| dragon_axe | slash | -2 | +43 | +32 | +36 | 5 |
+| bronze_pickaxe | stab | +4 | -2 | +2 | +3 | 5 |
+| iron_pickaxe | stab | +7 | -2 | +3 | +5 | 5 |
+| steel_pickaxe | stab | +10 | -2 | +4 | +7 | 5 |
+| black_pickaxe | stab | +14 | -2 | +6 | +9 | 5 |
+| mithril_pickaxe | stab | +17 | -2 | +8 | +12 | 5 |
+| adamant_pickaxe | stab | +23 | -2 | +10 | +16 | 5 |
+| rune_pickaxe | stab | +30 | -2 | +14 | +22 | 5 |
+| dragon_pickaxe | stab | +34 | -2 | +16 | +26 | 5 |
+
+### Metal Armor (Defense Bonuses)
+
+| Item | Slot | Stab | Slash | Crush | Science | Ranged |
+|------|------|------|-------|-------|---------|--------|
+| bronze_med_helm | head | +3 | +4 | +2 | -1 | +3 |
+| iron_med_helm | head | +5 | +6 | +3 | -1 | +5 |
+| steel_med_helm | head | +7 | +9 | +5 | -2 | +7 |
+| mithril_med_helm | head | +10 | +12 | +7 | -3 | +10 |
+| adamant_med_helm | head | +14 | +16 | +10 | -3 | +14 |
+| rune_med_helm | head | +19 | +22 | +13 | -4 | +19 |
+| bronze_full_helm | head | +5 | +6 | +4 | -3 | +5 |
+| iron_full_helm | head | +8 | +9 | +6 | -3 | +8 |
+| steel_full_helm | head | +12 | +13 | +9 | -5 | +12 |
+| mithril_full_helm | head | +16 | +18 | +12 | -6 | +16 |
+| adamant_full_helm | head | +22 | +24 | +16 | -8 | +22 |
+| rune_full_helm | head | +30 | +33 | +22 | -11 | +30 |
+| bronze_platebody | torso | +12 | +15 | +10 | -10 | +12 |
+| iron_platebody | torso | +18 | +22 | +14 | -10 | +18 |
+| steel_platebody | torso | +27 | +32 | +22 | -15 | +27 |
+| mithril_platebody | torso | +36 | +42 | +30 | -20 | +36 |
+| adamant_platebody | torso | +49 | +55 | +40 | -25 | +49 |
+| rune_platebody | torso | +65 | +72 | +52 | -30 | +65 |
+
+### Leather/Ranged Armor (Defense Bonuses)
+
+| Item | Slot | Stab | Slash | Crush | Science | Ranged | Other |
+|------|------|------|-------|-------|---------|--------|-------|
+| leather_cowl | head | +1 | +2 | +1 | +2 | +4 | |
+| leather_body | torso | +4 | +7 | +5 | +4 | +10 | |
+| leather_chaps | legs | +2 | +4 | +3 | +3 | +6 | |
+| leather_gloves | hands | +1 | +1 | +1 | +1 | +2 | |
+| leather_vambraces | hands | +2 | +2 | +2 | +2 | +4 | ranged_attack: +4 |
+| leather_boots | feet | +1 | +1 | +1 | +1 | +2 | |
+| coif | head | +2 | +4 | +2 | +4 | +6 | ranged_attack: +2 |
+| hard_leather_body | torso | +8 | +12 | +8 | +6 | +14 | |
+| hard_leather_shield | off_hand | +5 | +7 | +5 | +4 | +8 | |
+
+### Ranged Weapons (Attack Bonuses)
+
+| Item | Ranged Attack | Ranged Str | Speed |
+|------|--------------|------------|-------|
+| shortbow | +8 | 0 | 4 |
+| oak_shortbow | +14 | 0 | 4 |
+| willow_shortbow | +20 | 0 | 4 |
+| maple_shortbow | +29 | 0 | 4 |
+| yew_shortbow | +47 | 0 | 4 |
+| magic_shortbow | +69 | 0 | 4 |
+| longbow | +8 | 0 | 6 |
+| oak_longbow | +14 | 0 | 6 |
+| willow_longbow | +20 | 0 | 6 |
+| maple_longbow | +29 | 0 | 6 |
+| yew_longbow | +47 | 0 | 6 |
+| magic_longbow | +69 | 0 | 6 |
+
+### Ranged Ammo (Ranged Strength)
+
+| Item | Ranged Str |
+|------|-----------|
+| bronze_arrow | +7 |
+| iron_arrow | +10 |
+| steel_arrow | +16 |
+| mithril_arrow | +22 |
+| adamant_arrow | +31 |
+| rune_arrow | +49 |
+| bronze_bolts | +10 |
+| iron_bolts | +17 |
+| steel_bolts | +25 |
+| mithril_bolts | +32 |
+| adamant_bolts | +43 |
+| rune_bolts | +55 |
+| sapphire_bolts | +36 |
+| emerald_bolts | +39 |
+| ruby_bolts | +49 |
+| diamond_bolts | +52 |
+
+---
+
+## Appendix B: OSRS Accuracy Formula Explanation
+
+The true OSRS accuracy formula is:
+
+```
+if attack_roll > defense_roll:
+ hit_chance = 1 - (defense_roll + 2) / (2 * (attack_roll + 1))
+else:
+ hit_chance = attack_roll / (2 * (defense_roll + 1))
+```
+
+This produces a smooth curve rather than the current binary hit/miss:
+- Equal rolls → ~50% hit chance
+- 2x attack vs defense → ~75% hit chance
+- 0.5x attack vs defense → ~25% hit chance
+- Very high attack → approaches ~100% but never reaches it
+- Very low attack → approaches ~0% but never reaches it
+
+This is more balanced than the current system where `attackRoll > defenseRoll` is always a hit and `<` is always a miss.
+
+---
+
+## Appendix C: Files Modified (Summary)
+
+| File | Changes |
+|------|---------|
+| `internal/object/item.go` | `ItemStats` struct rewrite (14 fields), `AttackType` field on `ItemDef` |
+| `internal/world/mob.go` | New fields on `MobDef`, `MobInstance`, `SeedMobs` propagation |
+| `internal/combat/formulas.go` | Full rewrite: `HitChance`, `HitCheck`, `RangedStyleBonus`, `SelectAttackBonus`, `SelectDefenseBonus` |
+| `internal/game/cmd_attack.go` | `playerAttack` rewrite, `mobAttack` rewrite, `awardCombatXP` ranged support, ammo consumption |
+| `internal/game/equip_stats.go` | New file: `playerEquipBonuses` helper |
+| `internal/game/cmd_stats.go` | New file: `doStats` command |
+| `internal/game/cmd_look.go` | `showItemStats` helper, item/mob examination updates |
+| `internal/game/game.go` | Register `stats` in `classifyCommand` and `executeCommand` |
+| `internal/game/utils.go` | Updated `mobCombatLevel` formula |
+| `data/items/*.yaml` | ~66 item files updated with per-type stats |
+| `data/mobs/*.yaml` | 5 existing mobs updated, 8 new mobs added |
+| `data/help/combat.yaml` | New help file |
+| `data/help/stats.yaml` | New help file |
+| `data/help/style.yaml` | New/updated help file |