Skip to content

WardenSK

A warden-anger Skript ADDON — custom expressions, conditions, effects and events built on reflection — ported to one importable module of exported functions. 273 lines of Skript, 217 of SwoftLang; exercises modules, custom mobs, lambdas, entity poses, sounds and particles.

WardenSK_1.0.sk → WardenSK.sw — the whole file, both sides
the addon header
skript
# WardenSK
# Adds support for all Warden (entity) related things
# Recommended:
    # Skript 2.14+
    # Skript-Reflect 2.6.3+
    # SkBee 3.20+
    # Paper 1.21.10+
# Apologies for any inconsistencies, WardenSK is a solo project, which means it is bound to have errors.

# Lusk Warden Syntax .sk Port
# Although, few syntaxes may not function identical to Lusk.
swoftlang
// WardenSK_1.0.sk port — a Skript ADDON (custom expressions, conditions,
// effects and events built on reflection) becoming a SwoftLang MODULE:
// plain exported functions over module state. Drop this file in addons/
// and consume it with:  import "WardenSK"
//
//   set w to summon_warden(location(0.5, 64.0, 0.5))
//   increase_warden_anger(w, some_player, 100)
//   if warden_is_angry(w) { ... }
//   on_warden_anger_change(function(warden_name: String, target_name: String,
//                                   old_anger: Integer, new_anger: Integer) {
//       broadcast "<red>${warden_name} now ${new_anger} at ${target_name}"
//   })
Why it maps this way

Four version pins (Skript, skript-reflect, SkBee, Paper) and an apology for solo-project errors become one import line for consumers — and the apology retires: the typechecker reads every line before the server boots.

reflection import → a declared mob
skript
import:
    org.bukkit.entity.Entity

# _       __               __               
#| |     / /___ __________/ /__  ____  _____
#| | /| / / __ `/ ___/ __  / _ \/ __ \/ ___/
#| |/ |/ / /_/ / /  / /_/ /  __/ / / (__  ) 
#|__/|__/\__,_/_/   \__,_/\___/_/ /_/____/  

#    ______                               _                 
#   / ____/  ______  ________  __________(_)___  ____  _____
#  / __/ | |/_/ __ \/ ___/ _ \/ ___/ ___/ / __ \/ __ \/ ___/
# / /____>  </ /_/ / /  /  __(__  |__  ) / /_/ / / / (__  ) 
#/_____/_/|_/ .___/_/   \___/____/____/_/\____/_/ /_/____/  
#          /_/
swoftlang
// The .sk imports org.bukkit.entity.Entity and calls Paper methods through
// skript-reflect. No reflection here: the warden is a declared custom mob
// and every "syntax" below is an exported, typechecked function.
mob "warden" {
    type: "WARDEN"
    name: "<dark_aqua>Warden"
    health: 500
    damage: 30
    ai: none

    on_spawn {
        broadcast "<dark_aqua>The ground trembles..."
    }
}

// anger registry: association chain keyed "anger:<warden>:<target>",
// "highest:<warden>", "prey:<warden>". Mobs carry no uuid property, so a
// warden's NAME is its identity — summon_warden hands out unique ones.
var warden_data = function(lookup: String) return none
var warden_count = 0

function warden_put(key: String, value) {
    set previous to warden_data
    set warden_data to function(lookup: String) {
        if lookup is key return value
        return previous(lookup)
    }
}

export function summon_warden(at: Location) {
    set warden_count to warden_count + 1
    spawn mob "warden" at at as w
    set w.name to "<dark_aqua>Warden #${warden_count}"
    return w
}
Why it maps this way

The .sk imports org.bukkit.entity.Entity and drives it through skript-reflect; here WARDEN is a real entity type, so the mob is declared like any other. Mobs expose no uuid property, so a warden's name is its identity, and summon_warden hands out unique ones.

anger: get / set / add / reset
skript
# Warden - Anger (Don't get this confused with Skript's Warden Anger Level, that expression is towards a entity, this one is general anger.)
# Gets the anger level of this warden. Anger is an integer from 0 to 150. Once a Warden reaches 80 anger at a target it will actively pursue it.

expression:
    patterns:
        [the] anger of %entity% [(specific: (for %entity%))]
        %entity%'[s] anger [(specific: (for %entity%))]
    return type: integer
    get:
        if expr-2 is set:
            return expr-1.getAnger(expr-2)
        else if expr-2 is not set:
            return expr-1.getAngerLevel()
    set:
        expr-2 is set:
            expr-1.setAnger(expr-2, change value)
    add:
        expr-2 is set:
            expr-1.increaseAnger(expr-2, change value)
    reset:
        expr-2 is set:
            expr-1.clearAnger(expr-2)
swoftlang
// ── Warden - Anger ───────────────────────────────────────────────────────
// get / set / add / reset of the .sk's anger expression. Anger is clamped
// to 0..150 like the Paper API it wrapped; 80+ means active pursuit.

export function warden_anger_at(w: Mob, target: Player) {
    set stored to warden_data("anger:${w.name}:${target.name}")
    if stored exists return stored
    return 0
}

export function set_warden_anger(w: Mob, target: Player, value: Integer) {
    set old_anger to warden_anger_at(w, target)
    set new_anger to clamp(value, 0, 150)
    warden_put("anger:${w.name}:${target.name}", new_anger)

    // maintain highest + prey (the .sk gets both free from Paper); a drop
    // on the current prey keeps the prey and lowers the highest
    set highest to warden_anger(w)
    if new_anger >= highest {
        warden_put("highest:${w.name}", new_anger)
        warden_put("prey:${w.name}", target.name)
    } else {
        set prey_name to warden_data("prey:${w.name}")
        if prey_name exists {
            if prey_name is target.name {
                warden_put("highest:${w.name}", new_anger)
            }
        }
    }

    fire_anger_change(w.name, target.name, old_anger, new_anger)
}

export function increase_warden_anger(w: Mob, target: Player, amount: Integer) {
    set_warden_anger(w, target, warden_anger_at(w, target) + amount)
}

export function clear_warden_anger(w: Mob, target: Player) {
    set_warden_anger(w, target, 0)
}
Why it maps this way

A pattern grammar with five changer blocks becomes four functions. clamp(value, 0, 150) matches the Paper range. Skript read highest and prey back from Paper on demand; here both are tracked as the anger is written, so clearing the current prey's anger leaves highest at its old value until the next write updates it.

prey
skript
# Warden - (Prey)
# Returns the entity at which this warden is most angry.

expression:
    patterns:
        [the] prey of %entity%
        %entity%'[s] prey
    return type: livingEntity
    get:
        expr-1 = warden
        return expr-1.getEntityAngryAt()
    set:
        expr-1.setAnger(change value)
    remove: 
        entity.clearAnger(change value)
swoftlang
// ── Warden - Prey ────────────────────────────────────────────────────────
// 'the entity at which this warden is most angry', as optional<Player>.

export function warden_prey(w: Mob) {
    set prey_name to warden_data("prey:${w.name}")
    if prey_name exists return player(prey_name)
    return none
}
Why it maps this way

getEntityAngryAt() → the stored prey name resolved through player(name) — an optional<Player>, so a logged-off prey is provably missing, never null.

past/future anger → listener arguments
skript
# Warden Anger (Event)
# Returns the past/future anger of the warden in the Warden Anger Change event.
# The future anger level can be set.

expression:
    patterns:
        [the] [future: future|past: past] anger [level] [of [the] warden]
    return type: integer
    usable in:
        WardenAngerChangeEvent
    get:
        if first element of parse tags is not set:
            return the anger of (event.getEntity())
        else if the first element of parse tags contains "future":
            return {-futureAnger::%(event.getEntity())%}
            delete {-futureAnger::%(event.getEntity())%}
        else if the first element of parse tags contains "future":
            return {-pastAnger::%(event.getEntity())%}
            delete {-pastAnger::%(event.getEntity())%}
    set:
        event.getEntity().setNewAnger(change value)
swoftlang
// ── Warden - anger change listeners ──────────────────────────────────────
// The .sk defines an 'anger [level]' event expression with past/future
// parse tags, fed by {-pastAnger::}/{-futureAnger::} side-channel list
// vars. Listeners here are first-class lambdas and old/new anger arrive
// as ARGUMENTS — no side channel to stash or clean up.

var anger_listeners = function(warden_name: String, target_name: String,
                               old_anger: Integer, new_anger: Integer) return 0

export function on_warden_anger_change(handler) {
    set previous to anger_listeners
    set anger_listeners to function(warden_name: String, target_name: String,
                                    old_anger: Integer, new_anger: Integer) {
        handler(warden_name, target_name, old_anger, new_anger)
        previous(warden_name, target_name, old_anger, new_anger)
        return 0
    }
}

function fire_anger_change(warden_name: String, target_name: String,
                           old_anger: Integer, new_anger: Integer) {
    anger_listeners(warden_name, target_name, old_anger, new_anger)
}
Why it maps this way

The .sk parks old/new anger in {-pastAnger::} / {-futureAnger::} side-channel list vars so a parse-tagged expression can fish them back out. Listeners here are first-class lambdas and old/new anger arrive as arguments — nothing to stash, nothing to clean up.

highest anger
skript
# Warden - Highest Anger Level || NOT IN LUSK
# Anger is an integer from 0 to 150. Once a Warden reaches 80 anger at a target it will actively pursue it.

expression:
    patterns:
        [the] highest anger [level] of %livingEntity%
        %livingEntity%'s highest anger [level]
    return type: integer
    get:
        return expr-1.getHighestAnger()
swoftlang
// ── Warden - Highest Anger / general anger ───────────────────────────────
// the .sk's '%entity%'s anger' without a target, and its 'highest anger'.

export function warden_anger(w: Mob) {
    set stored to warden_data("highest:${w.name}")
    if stored exists return stored
    return 0
}
Why it maps this way

Same registry read as the general anger expression — the .sk needed a second expression block for the second pattern spelling.

five spellings of “is it angry”
skript
# Warden - Angry State
# Returns the Angry State of an entity.
# (Warden, PigZombie, Wolf, Enderman)
# Can be set for all except wardens.

expression:
    patterns:
        [the] ang(ry|er) state of %entity%
        %entity%'[s] ang(ry|er) state
        whether %entity% is angry [or not]
        whether [or not] %entity% is angry
    return type: boolean
    get:
        if expr-1 is angry:
            return true
        else if expr-1 isn't angry:
            return false

# Warden - Is Angry (Property)

expression:
    patterns:
        [the] [is] angry of %livingentities%
        %livingentities%'[s] [is] angry
    return type: boolean
    get:
        return whether expr-1 is angry

#   ______                ___ __  _                 
#  / ____/___  ____  ____/ (_) /_(_)___  ____  _____
# / /   / __ \/ __ \/ __  / / __/ / __ \/ __ \/ ___/
#/ /___/ /_/ / / / / /_/ / / /_/ / /_/ / / / (__  ) 
#\____/\____/_/ /_/\__,_/_/\__/_/\____/_/ /_/____/  

# Warden - Is Agitated
# Checks if a Warden is agitated.

condition:
    patterns:
        %livingentities% (is|are|=) agitated
        %livingentities% (negative:(isn't|is not|aren't|are not|!=)) agitated
    check:
        if first element of parse tags contains "negative":
            loop exprs-1:
                if expr-1's anger is between 40 and 79:
                    stop
                continue
        else if first element of parse tags is not set:
            loop exprs-1:
                if expr-1's anger isn't between 40 and 79:
                    stop
                continue

# Warden - Is Angry
# Checks if a Warden is agitated.

condition:
    patterns:
        %livingentities% (is|are|=) angry
        %livingentities% (negative:(isn't|is not|aren't|are not|!=)) angry
    check:
        if first element of parse tags contains "negative":
            loop exprs-1:
                if expr-1's anger is between 80 and 150:
                    stop
                continue
        else if first element of parse tags is not set:
            loop exprs-1:
                if expr-1's anger isn't between 80 and 150:
                    stop
                continue

# Warden - Is Calm
# Checks if a Warden is calm.

condition:
    patterns:
        %livingentities% (is|are|=) calm
        %livingentities% (negative:(isn't|is not|aren't|are not|!=)) calm
    check:
        if first element of parse tags contains "negative":
            loop exprs-1:
                if expr-1's anger is between 0 and 39:
                    stop
                continue
        else if first element of parse tags is not set:
            loop exprs-1:
                if expr-1's anger isn't between 0 and 39:
                    stop
                continue
swoftlang
// ── Angry State / Is Angry / conditions ──────────────────────────────────
// the .sk spells "is it angry" five ways: an angry-state expression, an
// is-angry property, and three double-pattern conditions with negation
// handled by parse tags. A Boolean-returning function is all of them;
// 'not' comes from the language.

export function warden_is_calm(w: Mob) {
    return warden_anger(w) <= 39
}

export function warden_is_agitated(w: Mob) {
    set a to warden_anger(w)
    return a >= 40 and a <= 79
}

export function warden_is_angry(w: Mob) {
    return warden_anger(w) >= 80
}
Why it maps this way

An angry-state expression, an is-angry property, and three double-pattern conditions (negation via parse tags, multi-entity via exprs-1 loops) — five Skript registrations — are three Boolean functions. Negation is not at the call site; many-mobs is a loop. The language already had the grammar.

disturb
skript
#    ______________          __      
#   / ____/ __/ __/__  _____/ /______
#  / __/ / /_/ /_/ _ \/ ___/ __/ ___/
# / /___/ __/ __/  __/ /__/ /_(__  ) 
#/_____/_/ /_/  \___/\___/\__/____/  

# Warden - Disturb
# Makes a warden sense a disturbance at the provided location.

effect:
    patterns:
        disturb %livingentities% from %location%
    trigger:
        expr-1 = warden
        expr-1.setDisturbanceLocation(expr-2)
swoftlang
// ── Warden - Disturb (effect) ────────────────────────────────────────────
// no pathfinding hook to hand a disturbance to, so the closest observable
// behavior: the warden's listening cue plays and the sculk flares there.

export function disturb(w: Mob, from: Location) {
    play sound "entity.warden.listening" to all at from
    spawn particle "sculk_charge_pop" at from count 12 offset 0.3, 0.3, 0.3
}
Why it maps this way

Custom-mob AI is melee/passive/none, so the port expresses the disturbance as its observable signature rather than a pathfinding nudge: the warden's listening cue plays and the sculk flares at the spot — the part a player actually sees.

the anger-change event
skript
#    ______                 __      
#   / ____/   _____  ____  / /______
#  / __/ | | / / _ \/ __ \/ __/ ___/
# / /___ | |/ /  __/ / / / /_(__  ) 
#/_____/ |___/\___/_/ /_/\__/____/  

# On Warden - On Anger Change

import:
    io.papermc.paper.event.entity.WardenAngerChangeEvent

event "On Warden - On Anger Change":
    patterns:
        [on] warden anger chang(e[d]|ing)
    event-values: command sender, entity, entity type, location, world

on WardenAngerChangeEvent:
    set {_eventValues::commandSender} to event.getTarget()
    set {_eventValues::entity} to event.getEntity()
    set {_eventValues::entityType} to event.getEntityType()
    set {_eventValues::location} to event.getEntity()'s location
    set {_eventValues::world} to world of (event.getEntity()'s location)
    set {_event} to (custom event "On Warden - On Anger Change" using event-values {_eventValues::*})
    call {_event}
    if {_event}.isCancelled():
        cancel event
        event.setCancelled(true)
    set {-pastAnger::%event.getEntity()%} to event.getOldAnger()
    set {-futureAnger::%event.getEntity()%} to event.getNewAnger()
swoftlang
// ── Events ───────────────────────────────────────────────────────────────
// The .sk bridges WardenAngerChangeEvent into a custom Skript event by
// copying event-values into a list var and re-calling it. Here gameplay
// drives the same registry the listeners already watch: hitting a warden
// enrages it (vanilla raises attacker anger to ~100). MobDamage carries
// no attacker, so the nearest player takes the blame.

event MobDamage {
    execute {
        if event.mob.custom_id is "warden" {
            set best to 999999999.0
            set nearest to player("")
            loop all players as p {
                set dx to p.location.x - event.mob.location.x
                set dy to p.location.y - event.mob.location.y
                set dz to p.location.z - event.mob.location.z
                set d to dx * dx + dy * dy + dz * dz
                if d < best {
                    set best to d
                    set nearest to p
                }
            }
            if nearest exists {
                increase_warden_anger(event.mob, nearest, 100)
            }
        }
    }
}
Why it maps this way

The .sk registers a custom event, listens to the Bukkit one, copies event-values into a list var and re-fires. Here set_warden_anger calls the listener chain directly, and gameplay feeds the same registry: MobDamage bumps anger by 100 like vanilla. MobDamage reports the mob but not the attacker, so the port picks the nearest player as the aggressor — a small deliberate heuristic, and the registry it feeds is the one the listeners already watch.

sniff toggle
skript
# On Warden - On Sniff Toggle

import:
    org.bukkit.event.entity.EntityPoseChangeEvent

event "On Warden - on Sniff Toggle": # When using IntelliSkript, highlights/colors may not show.
    patterns:
        [on] warden (start:(sniff[ing] [start|begin]|start sniffing))
        [on] warden (stop:(sniff[ing] stop|stop sniffing))
        [on] warden (sniff toggle|toggle sniff)
    event-values: command sender, entity, entity type, location, pose, world, pose
    check:
        first element of parse tags is set
        if first element of parse tags contains "start":
            if pose != sniffing_pose:
                stop
            continue
        else if first element of parse tags contains "stop":
            if pose = sniffing_pose:
                stop
            continue

on entity pose change: # You need SkBee 3.20+
    event-entity = warden
    if any:
        event-pose = sniffing_pose
        past event-pose = sniffing_pose
    then:
        set {_eventValues::commandSender} to event-commandSender
        set {_eventValues::entity} to event-entity
        set {_eventValues::entityType} to event-entityType
        set {_eventValues::location} to event-location
        set {_eventValues::pose::pose} to event-pose
        set {_eventValues::world} to event-world
        set {_eventValues::pose::pastPose} to past event-pose
        set {_event} to (custom event "On Warden - on Sniff Toggle" using event-values {_eventValues::*})
        call {_event}
swoftlang
// ── Warden - Sniff toggle ────────────────────────────────────────────────
// The .sk's 'On Warden - On Sniff Toggle' fired when the vanilla warden
// entered or left its sniffing pose (SkBee's pose-change event plus parse
// tags). Pose is a writable entity row now, so the module DRIVES the pose
// instead of waiting to observe it: sniff_all puts every warden into (or out
// of) its sniffing pose and notifies the listeners with the new state.

var sniff_listeners = function(sniffing: Boolean) return 0

export function on_warden_sniff_toggle(handler) {
    set previous to sniff_listeners
    set sniff_listeners to function(sniffing: Boolean) {
        handler(sniffing)
        previous(sniffing)
        return 0
    }
}

export function sniff_all(sniffing: Boolean) {
    loop all_entities("WARDEN") as w {
        if sniffing {
            set w.pose to "sniffing"
        } else {
            set w.pose to "standing"
        }
    }
    sniff_listeners(sniffing)
}
Why it maps this way

pose is a writable entity row (an EntityPose enum, snake-case names validated at compile time). The .sk waited for the vanilla warden to enter its sniffing pose; the module drives it directly — sniff_all sets every warden's pose to sniffing (or back to standing) and notifies the listeners, the same first-class-lambda chain the anger events use.