Combat notes — NPC attackability & the "Attack" option (A3 findings)
How player-initiated PvM is offered to the client, and why the "Attack" option sometimes appeared
How player-initiated PvM is offered to the client, and why the "Attack" option sometimes appeared missing. Resolved: the missing-Attack symptom was a client setting, not a server/cache bug.
TL;DR (the real answer)
Whether "Attack" shows up — and whether it's left-click, right-click, or hidden — is decided entirely client-side from two inputs the server always sends:
- The npc type's op array (Attack lives in op slot 2 /
op[1]), straight from the cache npc def. - Both combat levels — the player's (
Appearance.combatLevel, default 3) and the npc's (vislevelin the npc def) — sent in the appearance / npc-info packets.
The server does no "is this npc too low/high" gating. It always offers Attack and ships both
levels; the client's "NPC 'Attack' options" setting (option_attackpriority_npc) decides the menu.
option_attackpriority_npc — the setting that controls it
NpcPriority enum: CombatLevel(0), RightClickAlways(1), LeftClick(2), Hidden(3).
- 0 — "Dependent on combat level" (OSRS default, what we set for new accounts):
- npc combat level ≤ player → Attack is the left-click (first) option.
- npc combat level > player → first option becomes "Walk here", Attack demoted to right-click (still in the menu).
- Attack is never removed from the right-click menu under this setting.
- 1 — Always right-click: Attack is always present, never the left-click default.
- 2 — Left-click where available: Attack is always the left-click default when the npc is attackable.
- 3 — Hidden until combat: Attack is hidden from the menu until you're already in combat with it. This is the only setting that removes Attack from the right-click menu.
So a brand-new level-3 account left-clicks level-2 goblins/cows out of the box (2 ≤ 3) — no client change needed. You only ever need the right-click for mobs above your level, or if you've chosen the "Hidden" setting. This matches real RuneScape exactly.
Verified live (rev-233 cache, native client, authentic default =0)
- Don combat level 11 vs Goblin (level 2) → menu first option = "Attack Goblin (level-2)" (left-click), Attack also in right-click. ✓
- Don combat level 11 vs Giant Mole (level 230) → menu = "Walk here" first, then "Attack Giant Mole (level-230)" demoted to right-click — present, not hidden. ✓
Both directions of the combat-level rule reproduce correctly on our server. The data + transmission were always correct.
What the earlier "Attack completely gone" was (corrected)
An earlier note here theorized a server/client npc-def (op2) mismatch. That was wrong. The
real cause: during testing the client's NPC-Attack-options setting had been left at a non-default
value (and/or the player's combat level made Attack a right-click-only entry that was overlooked).
Setting option_attackpriority_npc = 0 (the authentic default) restores correct behavior. The user
independently confirmed by switching the in-client setting to "Always right-click" and successfully
attacking — proving the server combat path was wired the whole time.
Lesson: the op2/attackability data model was never broken — don't chase a cache ghost. When "Attack" looks missing, check the client setting and the combat-level comparison first.
Server defaulting (content)
LeagueRealmScript.enterLeagueMode sets new accounts to option_attackpriority_npc = 0
(CombatLevel) — the authentic OSRS default — explicitly, so it's guaranteed regardless of the
engine's unset-varp default, and only for new accounts so it never overrides a returning player's
chosen setting.
Player→npc combat path (unchanged, confirmed working)
PvNCombatScript registers onDefaultOpNpc2 / onDefaultApNpc2 (op2 = Attack) → attemptCombatOp/Ap
→ canAttack() → PvNCombat.attack(...). PvNCombat gates on npc.visType.hasOp(InteractionOp.Op2).
No per-npc hookup needed; any op2 click routes to combat.
Dev tooling added for this
::npcops— prints the nearest npc's op array +vislevel+hasOp(Op2)(attackability).::npcadd <duration> <name>— spawn an npc (e.g.::npcadd 1500 hill_giant).::killnpc— kills the nearest npc (credits the player, runs the real death→drop-table path).::killme— queues the player's own death (tests death item-keeping).
Broken npc animations — root cause + bulk fix (done)
The attack_anim/defend_anim/death_anim params default to human seqs
(human_unarmedpunch/human_unarmedblock/human_death). Any npc whose type doesn't set them plays
a human animation on its own skeleton — "completely broken" for non-humans. Only ~53 of 12,638 npcs
in npcs.toml had combat anims. (Human-type npcs — men, women, guards — are fine on the defaults.)
Fix: a non-destructive bulk pass added species-correct attack/block/death seqs (from seq.sym named
seqs) to common non-human combat families, matched by npc-name token within the monster set
(goblin/cow/chicken/rat/spider/skeleton/zombie/imp/giant/bear/scorpion/dwarf/werewolf/hobgoblin),
with sub-variant guards. ~283 npcs gained anims (336 total now). Long-tail non-human npcs still fall
back to human anims — documented gap, extend the curated map as needed.
Bulk combat-data import (done) — osrsreboxed-db
npcs.toml combat data was imported for 1192 monsters (id-keyed, from osrsreboxed-db, the
maintained osrsbox-db fork). The cache enricher (DefaultNpcCacheEnricher + ExternalNpcConfig) was
extended to carry the full param set, and a generator merged data non-destructively (only adds missing
keys; preserves hand-curated entries):
attack_type(melee stab/slash/crush) + newnpc_combat_styleparam (0 melee / 1 ranged / 2 magic)attack_rate(attack speed), offensive bonuses (attack_melee/attack_ranged/attack_magic), strengths (melee_strength/ranged_strength/magic_damage)- defensive bonuses (
defence_stab/slash/crush/magic+defence_light/standard/heavy)
Combat levels (attack/str/def/hp/ranged/magic) already come from the cache npc def, so the import
focuses on the params the formulas read (see param map above). Generator scripts live in the session
scratchpad (gen_npc_combat.py, gen_npc_anims.py); a .bak of the original toml is kept beside it.
Coverage gap: osrsreboxed is ~2021 data, so post-2021 monsters/bosses (some Leagues V content) aren't included and need a later pass. Per-npc projectile data (for the ranged/magic visual) also isn't in the source — see the NvPCombat TODOs.
⚠️ Debugging note: hitsplat colour is NOT a combat-style tell
In OSRS, all damage hitsplats are red regardless of style (melee/ranged/magic). Only 0/blocked hits are blue. So a magic npc dealing damage shows a red splat exactly like melee. Do not use splat colour to judge whether an npc is attacking with magic/ranged — use the server-side dispatch (verified below) or the attacker's animation/projectile. This burned a debugging cycle: a magic-style npc looked like it was "meleeing" purely because of red splats + a punch animation + adjacent range.
NPC ranged/magic AI (done) — A3
NvPCombatScript now dispatches by npc_combat_style: melee → NpcMelee, ranged → NpcRanged,
magic → NpcMagic. NvPCombat gained attackRanged/attackMagic (anim + sound + accuracy roll via
AccuracyFormulae.rollRangedAccuracy/rollMagicAccuracy + max hit via
getRangedMaxHit/getMagicMaxHit + HitType.Ranged/Magic with a fixed projectile-travel delay).
Verified live via a temp [NvP] log: a young_dark_wizard (combat_style=2) dispatches
-> NpcMagic and applies magic-typed damage every hit.
When an npc defines no attack_anim, attackRanged/attackMagic now fall back to seqs.human_bow /
seqs.human_caststrike (instead of the melee-punch default) so casters/rangers animate correctly.
Attacking from range (done): ranged/magic npcs now attack from a distance instead of closing to melee. Three parts:
DefaultNpcCacheEnrichersetsattackRangefor ranged/magic npcs (defaultDEFAULT_RANGED_MAGIC_ATTACK_RANGE = 7; the base cache leaves casters atattackRange=1). An explicitattack_rangein the toml overrides it.AiPlayerInteractionsusesvisType.attackRangeas the ap range, so this is what the npc attacks from.NpcRetaliateScriptretaliates viacombatDefaultRetaliateAp(act-at-range) for ranged/magic,combatDefaultRetaliateOp(adjacent) for melee.NvPCombatScriptregisters BOTHonDefaultAiOpPlayer2andonDefaultAiApPlayer2→ same style-based dispatch.
Default attack animations also fall back to seqs.human_bow / seqs.human_caststrike (via
paramMap.getOrNull, which — unlike paramOrNull/param — does NOT return the param default) so
casters/rangers don't play the melee punch.
Projectiles (done, generic): attackRanged/attackMagic now spawn a projectile via
worldRepo.projAnimSourced(npc, target, spotanim, projanim) and time the hit to the projectile's
durations.serverDelay (instead of a fixed delay). Magic also plays a cast glow on the npc
(spotanims.windstrike_casting). Defaults: magic = windstrike_travel + projanims.magic_spell,
ranged = bronze_arrow_travel + projanims.arrow. These are generic — every caster currently
fires a wind-strike missile and every ranger a bronze arrow; per-npc spell/ammo graphics (and a target
impact spotanim) are the remaining polish, ideally driven by per-npc proj_* params.
Remaining (realism follow-ups):
- Per-npc projectile graphics + target impact spotanim (generic projectile flies today).
- Mixed-style npcs use a single primary style (no style rotation).
attackRange=7is a blanket default for ranged/magic; per-npc tuning (some casters reach further, some bosses differ) can override viaattack_rangein the toml.
Verification status
Data + code compile; gradlew packCache re-enriches the cache from the new toml. Changes take effect
after a server restart (gradlew run) which loads the freshly packed cache/enriched.