Skip to main content

Combat Helpers

The machinery the built-in combat modules share — target selection, humanized clicking, and rotation strategies — is available to scripts, both as schema nodes (they render the same options the built-in modules show) and as importable values.

import { defineModule, targets, ModuleCategory } from "@protohax/userscript";

defineModule(
{ name: "MyAura", category: ModuleCategory.Combat },
{
click: { type: "click-scheduler" },
target: { type: "target", range: 3, maxRange: 10, wallRange: 2, hurtTime: true },
rotation: { type: "rotation" },
},
(ctx) => { /* see the sections below */ },
);

The Targets module

targets() returns the live Targets singleton — the user's shared target-selection settings (entity kinds, anti-bot, team) that every combat module consults. Use it so your module targets the same things the built-ins do.

MemberDescription
isTarget(entity, attackIntent?)Whether entity is a valid target under the user's settings. Pass attackIntent: false to skip the team check (e.g. for highlighting).
notifyTarget(entity)Report that you attacked entity — it becomes the active target (drives TargetHud and the target events below).
isActiveTarget(entity)Whether entity is currently tracked as active in its session.

It is a regular module, so getOption("Player"), options() and subscribeOptions(...) work on it too.

Three game events follow the active target lifecycle: target_active, target_inactive, and target_killed, each carrying the EntityNetworkPlayer:

ctx.on("target_active", (entity) => console.log("fighting", entity.username));

Target scanning

The target schema node embeds the host's TargetConfigurable — the range / wall range / scan range / hurt time / priority drawer every combat module has. The node's fields decide which sub-options exist at all:

target: { type: "target", range: 3, maxRange: 10, wallRange: 2, scanExtraRange: 3, hurtTime: true },
FieldMeaning
rangeInitial attack range.
maxRangeUpper bound of the range sliders.
wallRange?Show the "Wall Range" option (line-of-sight check beyond it), starting at this value.
scanExtraRange?Show the "Scan Extra Range" option with this upper bound — extra band of seen but not attacked targets.
hurtTime?Show the "Hurt Time" filter.

The handle is the TargetConfigurable itself. Its main method scans the session, filtered through the Targets module and sorted by the configured priority:

const found = ctx.options.target.getTargets(targets(), ctx.session);
// TargetInfo[]: { entity, isScanExtra } — isScanExtra = in the extra band, don't attack
const best = found.find((t) => !t.isScanExtra);

The individual options are exposed as handles too — range, wallRange, scanExtraRange, hurtTime (each Option<number>, the optional ones undefined when not enabled) and the priority value (a TargetPriority: Distance, Health, Direction, Age, Hurt Time).

Clicker

The click-scheduler node embeds the host's CPS clicker — a drawer with a CPS range and a technique selector (Stabilized, Randomized, Double Tap, Gaussian Distribution — the same humanized timing engines the built-in modules use).

The handle is a ClickSchedulerConfigurable. Start a scheduler on enable, dispose it on disable:

{
click: { type: "click-scheduler" },
},
(ctx) => {
let clicker: AbstractClickScheduler | undefined;

ctx.onEnable(() => {
clicker = ctx.options.click.getClickScheduler(() => attackTick(ctx));
});
ctx.onDisable(() => {
clicker?.dispose();
clicker = undefined;
});
}

The callback fires once per (humanized) click for as long as the scheduler lives. The configured values are readable as ctx.options.click.cps (a NumberRange) and ctx.options.click.technique (a ClickTechnique) — note a running scheduler samples its settings at creation, so recreate it if you want changes applied mid-session.

Rotation

The rotation node embeds the host's rotation strategy selector — None / Simple / Smart (default Smart, which respects FOV settings and picks server-legal rotations).

The handle asks the selected strategy for a decision toward a RotationFace — a pitch/yaw region, usually derived from the target's bounding box:

import { face3DToRotationFace, utils } from "@protohax/userscript";

(ctx) => {
const OWNER = {}; // stable identity for rotationScheduler requests
const player = () => ctx.session.entityState.localPlayer;

ctx.on("tick", () => {
const [best] = ctx.options.target.getTargets(targets(), ctx.session);
if (!best || best.isScanExtra) {
player().rotationScheduler.cancel(OWNER);
return;
}

const eye = player().eyePosition;
const face = face3DToRotationFace(best.entity.aabb.getFaceFacing(eye), eye);
const decision = ctx.options.rotation.getRotation(ctx.session, face);

if (decision.type === "rotate") {
// a new rotation is needed — decision.rotation is the (pitch, yaw) Vector2
player().rotationScheduler.request(decision.rotation, utils.SchedulerPriority.High, OWNER, 200);
} else if (decision.type === "cancel") {
// the face is already in view — release the override
player().rotationScheduler.cancel(OWNER);
}
// "keep": the current rotation still satisfies the face — leave it standing
});

ctx.onDisable(() => player().rotationScheduler.cancel(OWNER));
}

The decision is a RotationType union: { type: "cancel" } (already in the client's FOV), { type: "keep" } (serverside FOV covers it), or { type: "rotate", rotation, face }. Apply rotate decisions through rotationScheduler; ctx.options.rotation.value reads or sets the selected strategy by name.

Raw nodes — the escape hatch

For host configurables without a curated node, the raw node embeds a pre-constructed object as-is, and the handle preserves its exact type:

{
// a Configurable, shown as a drawer named after the key (or `name`)
extraTarget: { type: "raw", configurable: new TargetConfigurable({ range: 6, maxRange: 12 }) },

// an Option instance, added verbatim (its own display name is used)
something: { type: "raw", option: someFreshlyConstructedOption },
},
(ctx) => {
ctx.options.extraTarget.getTargets(targets(), ctx.session); // typed as TargetConfigurable
}

The constructible classes exported for this today: TargetConfigurable, ClickSchedulerConfigurable, and the rotation strategies (RotationNoneConfigurable / RotationSimpleConfigurable / RotationSmartConfigurable). The object must be freshly constructed — do not embed options that already belong to another module.