# 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 ` 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 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 ' 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 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 `, attack mobs, verify damage/accuracy feel reasonable - [ ] **Step 38:** Verify ranged combat works with ammo consumption - [ ] **Step 39:** Verify `look ` 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 |