diff options
| author | historia <[not public]> | 2026-06-19 02:08:55 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-19 02:08:55 -0400 |
| commit | 458916fb78dcef3ff77cb29b7ba98d9f646819fc (patch) | |
| tree | 19cd8b8eef41a1aa00d155bf7953ad2770f91a4c /skill_plans/combat.md | |
| parent | 97a1a908b9e6e6d3516628c139c9b64262b4fe08 (diff) | |
| download | thehouseoficarus-458916fb78dcef3ff77cb29b7ba98d9f646819fc.tar.gz | |
feat: rough pharmacy skill added
Diffstat (limited to 'skill_plans/combat.md')
| -rw-r--r-- | skill_plans/combat.md | 3402 |
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 | |
