Skip to content

Scripts

Previous: Costs, Conditions & Variables · Next: Configuration File


Declarative conditions handle the simple gates: is the level high enough, is there a target, is the player in a certain world. But some checks can't be expressed that way: reading data from other plugins, doing combined calculations, handing out rewards after a successful cast... That's where JS scripts come in: script.pre_js (the gate) and script.post_js (after-the-fact side effects).

📌 Use conditions first, reach for scripts only when you can't. Scripts are more powerful but more complex—don't pull in JS for anything a declarative condition can already solve.

🖼️ [Image placeholder] Where the pre_js gate / post_js after-the-fact hooks sit in the pipeline · suggested assets/script-hooks.png


🪝 The two hooks

yaml
script:
  pre_js:  "qinhskills:demo.js:canCast"    # Before the cast: return false to block it
  post_js: "qinhskills:demo.js:onCast"     # After a successful cast: run side effects
HookTimingReturn valueBlocks the cast?
pre_jsGate phase, before the cast goes outReturning false → blocked, no resources spent, result code CONDITION_FAILEDYes
post_jsAfter a successful castReturn value ignoredNo (fire-and-forget)

post_js is "fire-and-forget"—even if it fails, it doesn't undo the fact that the skill already went out.


🔖 Reference format

Scripts use a three-part "namespace : path : function name" reference:

namespace:path.js[:functionName]

QS uses the qinhskills namespace, for example:

yaml
pre_js: "qinhskills:demo.js:canCast"
#        └─ namespace   └─ file   └─ function name

Script files are managed centrally by QCL's script loader and live in QCL's conventional scripts directory. Scripts under the qinhskills namespace belong to this QS set.


⚙️ Where the script engine comes from

The script engine is QinhCoreLib (QCL)'s GraalJS, not something QS ships itself. This means:

  • You need Paper / Purpur to pull in the GraalJS runtime libraries (QCL config javascript.enabled).
  • If the engine isn't ready, nothing crashes—QS degrades gracefully (see below).

📥 Injected objects

Two global objects are available inside scripts:

ctx — skill execution context

MethodPurpose
ctx.player()Returns the Player (the caster)
ctx.get(key)Read a variable
ctx.set(key, value)Write a variable
ctx.vars()Get the entire variable table

Variables readable via ctx.get(key) (the same set described in Costs, Conditions & Variables):

KeySource / meaning
skillSkill id (note: it's skill, not skillId)
levelThe player's level in this skill
modeTrigger mode (default if none)
sourceTrigger source (e.g. PLAYER / EVENT_LISTENER / COMMAND)
playerPlayer name
toggle_stateon / off (only present for toggle skills)
has_target / target_type / target_uuidOnly present when there is a target (crosshair / target acquisition / passive-locked target)
var_<name>Skill variables, from both variables: and levels.params:, with a uniform var_ prefix (e.g. var_element, var_power)

⚠️ Note: scripts do not have the keys skillId / castMode / targetCount / slot / param_. Skill variables and level params both use the var_ prefix (not param_); the key for the skill id is skill.

qcl — QCL global utilities

MethodPurpose
qcl.logInfo(msg)Write a log line
qcl.itemGive(player, item)Give an item
qcl.economy*(...)Economy-related (withdraw / deposit / check balance, etc.)
qcl.runSync(runnable)Switch a piece of logic back to the main thread (use when touching the world / entities)

🧪 Example 1: pre_js gate (only cast at level ≥ 2)

Skill yml:

yaml
script:
  pre_js: "qinhskills:demo.js:canCast"

In demo.js:

javascript
// Block if level < 2; returning false → QS spends no resources, result code CONDITION_FAILED
function canCast(ctx) {
    var level = ctx.get("level");           // The player's level in this skill
    if (level == null || level < 2) {
        var p = ctx.player();
        p.sendMessage("§c该技能需要 2 级才能施放");
        return false;                       // ← block the cast
    }
    return true;                            // ← allow it through
}

This simple example could actually be done with conditions: ["player_level:>=2"] too. The real use for pre_js is cases that declarative conditions can't express: "read another plugin's data," "score multiple conditions together," and the like.


🎁 Example 2: post_js after-the-fact side effects (send a message / give an item)

Skill yml:

yaml
script:
  post_js: "qinhskills:demo.js:onCast"

In demo.js:

javascript
// Runs after the skill is successfully cast: send a message, give a reward item
function onCast(ctx) {
    var p = ctx.player();
    p.sendMessage("§a技能命中!获得战利品 §e×1");

    // Touching the world / giving items is safer on the main thread
    qcl.runSync(function() {
        qcl.itemGive(p, "DIAMOND");
    });

    qcl.logInfo("[demo] " + p.getName() + " 触发了 onCast 副作用");
}

post_js has no bearing on whether the skill itself goes out—it just "does a little something afterward."


🛟 Degradation behavior (when the engine isn't ready)

When GraalJS isn't installed / isn't enabled, QS won't error out or hang—it degrades safely instead:

HookDegraded behavior
pre_jsDoesn't block (treated as returning true, casts as normal)
post_jsNo-op (does nothing)

When degrading, it logs a diagnostic telling you the engine isn't ready, but does not interrupt the cast. So even if the script environment isn't set up, skills still cast—only the script logic is inert—which is safer in production.


📚 Further reading