Skip to content

๐Ÿ“œ Script API (pre_js / post_js) โ€‹

Previous: Placeholdersใ€€ยทใ€€Next: Diagnostics & Protocol

For simple gating, declarative conditions: are enough; only complex logic needs to drop down to JS. QS provides two script hooks in a skill definition: pre_js (pre-cast, can intercept) and post_js (side effects after a successful cast). The engine reuses QinhCoreLib's GraalJS. This chapter covers the reference format, the injected context, fallback behavior, and gives a complete example. This document targets QS 1.0.22.


1. Attaching scripts in a skill definition โ€‹

yaml
script:
  pre_js: "qinhskills:demo.js:canCast"    # returns false โ†’ intercepts the cast
  post_js: "qinhskills:demo.js:onCast"    # runs after a successful cast (fire-and-forget)

Reference format โ€‹

qinhskills:path.js[:functionName]
SegmentNotes
qinhskills:Namespace prefix (QS registers it to the QCL script loader via registerPluginScripts(plugin, "qinhskills"))
path.jsFile path relative to the QS script directory
:functionNameOptional; specifies a function in the file to call. Omit it to run the whole script per QCL's default convention

Script files are managed uniformly by the QCL script loader; QS only registers the namespace.


2. Semantics of the two hooks โ€‹

HookWhenReturn valueConsequence of failure/interception
pre_jsGating stage (before spending resources / entering CD)booleanReturning false โ†’ CONDITION_FAILED, no cost, no cooldown
post_jsAfter a successful castIgnored (fire-and-forget)A thrown exception only logs; it doesn't affect the already-successful cast

pre_js is the real interception gate: it decides before resources are spent and the cooldown is entered. Returning false means this cast never happened (no side effects).


3. Injected context ctx โ€‹

There's a global ctx in the script:

CallReturnsNotes
ctx.player()PlayerThe caster (Bukkit Player)
ctx.get(key)valueReads a context key
ctx.set(key, value)โ€”Writes a context key
ctx.vars()MapAll context key-value pairs

Readable keys โ€‹

The source assembles scriptVars with buildMap (SkillCastService.executeResolved); the keys actually injected are:

KeyMeaningWhen present
skillSkill id (it's skill, not skillId)Always
levelThe player's level for this skillAlways
modeTrigger mode (default if none)Always
sourceTrigger source (PLAYER / EVENT_LISTENER / COMMAND โ€ฆ)Always
playerPlayer nameAlways
toggle_stateon / offOnly for toggle skills
has_targettrueOnly when a target is locked
target_typeTarget entity type nameOnly when a target is locked
target_uuidTarget UUID stringOnly when a target is locked
var_<name>Skill variables, e.g. var_element, var_powerOne per variables: / levels.params: key

โš ๏ธ Keys that don't exist: skillId, castMode, targetCount, slot, param_<name> are all absent. Skill variables and level params uniformly use the var_ prefix (not param_): YAML's variables.element and levels.N.params.power โ†’ ctx.get("var_element"), ctx.get("var_power") in the script.

๐Ÿ“Œ This ctx key set applies only to scripts. The variables a skill passes through to MythicMobs are a different set (<skill.var.playerName>, params without the var_ prefix, and MM can't get level); see Costs, Conditions & Variables and Integrating MythicMobs.


4. Injected global qcl โ€‹

From QCL, a unified cross-plugin utility facade:

CallEffect
qcl.logInfo(msg)Logs info
qcl.itemGive(player, item)Gives the player an item
qcl.economy*(...)Economy-related (deposit/withdraw/query, etc., depending on the QCL version)
qcl.runSync(runnable)Switches the logic back to the main thread (always use it for world/entity operations)

โš ๏ธ Scripts may be invoked in an async context. Wrap any operation that touches the world/entities in qcl.runSync { ... }, otherwise it may throw a threading exception.


5. Complete example โ€‹

plugins/QinhSkills/scripts/demo.js (path is illustrative; the QCL script directory is authoritative):

javascript
// pre_js: returning false intercepts the cast
function canCast(ctx) {
    var player = ctx.player();
    var element = ctx.get("var_element");      // skill variable variables.element
    // Only allow fire-element skills, and not while in lava
    if (element === "fire" && player.getLocation().getBlock().getType().name().indexOf("LAVA") >= 0) {
        player.sendMessage("ยงcCan't cast fire skills in lava");
        return false;                          // โ†’ CONDITION_FAILED, no cost, no CD
    }
    return true;
}

// post_js: runs after a successful cast, fire-and-forget
function onCast(ctx) {
    var player = ctx.player();
    var power = ctx.get("var_power");          // level param levels.N.params.power (uniform var_ prefix)
    qcl.logInfo(player.getName() + " cast " + ctx.get("skill") + " power=" + power);
    // Always switch to the main thread to touch the world/entities
    qcl.runSync(function() {
        player.setFireTicks(20);
    });
}

6. Fallback (when the engine is unavailable) โ€‹

QS bridges QCL's QinhScriptBridge via reflection and safely falls back on various failures, never wrongly locking a skill just because JS is unavailable:

Situationpre_js behaviorpost_js behavior
GraalJS runtime not readyReturns true (no intercept)No-op
Reflection can't get QinhScriptBridge.INSTANCEReturns trueNo-op + warning log
The deployed CoreLib has no execute methodReturns trueNo-op + warning log

GraalJS requires the Paper / Purpur runtime to pull in the GraalJS libraries with javascript.enabled=true. A typical "runtime not ready" log: [QS-JS] GraalJS runtime not ready: org.graalvm.polyglot.Context not loaded (CoreLib libraries didn't pull GraalJS) or javascript.enabled=false

Design principle: when pre_js falls back, it lets the cast through (returns true) โ€” better to not intercept than to wrongly kill; when post_js falls back, it silently no-ops. Both come with diagnostic logs to help you locate the issue.


7. When to use JS, when to use conditions โ€‹

NeedUse
Fixed checks like level / health / world / permission / target type, distance, etc.conditions: (declarative, no engine dependency)
Cross-plugin queries, complex branching, giving items, mutating entity statepre_js / post_js

Prefer conditions for simple gates โ€” zero engine dependency, never falls back.


Further reading โ€‹

  • Events โ€” event cancellation is another interception gate beyond scripts
  • API โ€” the CONDITION_FAILED / SCRIPT_BLOCKED result codes
  • Diagnostics & Protocol โ€” see post_js at the [POST] stage of the debug trace