ClockworkRS Docs

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:

  1. The npc type's op array (Attack lives in op slot 2 / op[1]), straight from the cache npc def.
  2. Both combat levels — the player's (Appearance.combatLevel, default 3) and the npc's (vislevel in 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 ≤ playerAttack 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/ApcanAttack()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) + new npc_combat_style param (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:

  • DefaultNpcCacheEnricher sets attackRange for ranged/magic npcs (default DEFAULT_RANGED_MAGIC_ATTACK_RANGE = 7; the base cache leaves casters at attackRange=1). An explicit attack_range in the toml overrides it. AiPlayerInteractions uses visType.attackRange as the ap range, so this is what the npc attacks from.
  • NpcRetaliateScript retaliates via combatDefaultRetaliateAp (act-at-range) for ranged/magic, combatDefaultRetaliateOp (adjacent) for melee.
  • NvPCombatScript registers BOTH onDefaultAiOpPlayer2 and onDefaultAiApPlayer2 → 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=7 is a blanket default for ranged/magic; per-npc tuning (some casters reach further, some bosses differ) can override via attack_range in 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.

On this page