Script API
Thematic script API. Scripts are ES5 JavaScript plus arrow functions, with no Java access. Ids without a namespace mean thematic:. Generated from the engine's declarations. The same API as TypeScript declarations is in thematic.d.ts; the full data is script-api.json.
Globals
function main(drone: drone, owner: owner, args: {[input: string]: any})Define this in every program, as a generator: function* main(drone, owner, args) { ... }. The program runs from it; it yields drone actions to wait on them, and ends when it returns. `args` holds what the player picked for the inputs the program declares in its header comments, one line each: `// @input <name> <kind> "<prompt>" [optional] [default=owner|look|here]`. Before the program runs the console walks the player through them (a click, or a zone or route drawn) and shows the prompt; an optional one can be skipped with Esc or Enter. The kinds and what args.<name> is: `point` and `chest` (a container block) are {x, y, z}; `area` is {center: {x, y, z}, radius} (radius 1 to 16); `entity` is what a scan gives of a body ({id, type, name, x, y, z, ...}); `path` is a list of {x, y, z} (at most 32). An optional input left out with no default is not a property, so args.<name> is undefined: test it before use. default=owner uses the owner's own spot (or the owner, for an entity), default=look the block (or body) they are looking at, default=here the spot the drone stands on. A program with no inputs gets an empty args. A program runs in a locked-down scope: Promise, Map, Set, WeakMap, WeakSet, Symbol, BigInt, ArrayBuffer and the typed arrays, XML and RegExp are not defined (nothing queues work behind the program's back, and a regular expression cannot run away), so a regular expression literal or any regex-taking String method (match, search, split or replace with a pattern) is an error; split(","), replace("a", "b" or a function), replaceAll, indexOf and includes work on plain strings. Arrays and strings are capped at 100000 elements or characters (fill, join, repeat, padStart, padEnd and the other array methods throw past it; join and JSON.stringify refuse results over a million characters or 200000 values). A program that holds more than about 16 MB of values stops with "out of memory"; a step that runs over 200000 instructions, nests structures too deeply for the stack or fails any other way crashes that program alone.
function plan(ctx: {tick: number, dt: number, params: {kind: string, radius: number, height: number, speed: number, passes: boolean, tighten: number, defendOnly: boolean, retreatBelow: number, engageRange: number, spreadRadius: number}, owner: {id: string, pos: {x: number, y: number, z: number}, look: {x: number, y: number, z: number}, velocity: {x: number, y: number, z: number}, pool: number, health: number, maxHealth: number, yaw: number, anchorYaw: number, age: number, onGround: boolean}, target: {id: string, type: string, pos: {x: number, y: number, z: number}, velocity: {x: number, y: number, z: number}, pool: number, distance: number, airborne: boolean, hostile: boolean, player: boolean, effects: object} | null, enemies: {id: string, type: string, pos: {x: number, y: number, z: number}, velocity: {x: number, y: number, z: number}, pool: number, distance: number, airborne: boolean, hostile: boolean, player: boolean, effects: object}[], units: {index: number, slot: number, id: string, pos: {x: number, y: number, z: number}, velocity: {x: number, y: number, z: number}, pool: number, target: string | null, ready: object, order: string | null, busy: boolean, fallingBack: boolean}[], state: object})A formation script defines this, and the game calls it every few ticks (the file's @cadence, 5 to 10, slower when the server is under load) once for the whole squad. ctx is a snapshot, capped (at most 16 enemies and 16 units) and rebuilt each plan: ctx.tick (the world's time), ctx.dt (ticks since the last plan), ctx.params (numbers from the squad's definition), ctx.owner (the body the squad guards: its position, look, velocity and yaw are the owner's, or the stand-in body's while the owner has left one behind; id and health stay the owner's), ctx.target (the squad's main target, or null), ctx.enemies, ctx.units (each with ready: which of its moves are off cooldown) and ctx.state, an object that is yours and keeps what you store on it from one plan to the next. Return {units: [...]} with one entry per ctx.units entry, in order (null leaves a unit to the default). An entry is {point, face?, target?, prefer?, hold?, speed?, pass?, direct?}: point is where the unit should be (the game steers it there round walls and moves it to open air with a line to its target, unless direct); face a point to look at; target an enemy id from ctx.enemies; prefer a list of move ids to favour when the unit picks a move (it still cannot use one on cooldown, out of range or without a line of sight); hold stands still; speed is blocks a tick; pass marks a fly-through. The result may also say defendOnly: true (fight only what attacks the owner or the squad) and scout: true. A plan may run 50,000 instructions. If it throws, overruns or returns nonsense (a point more than 80 blocks from the owner, NaN), the last good plan is kept, then a ring round the owner; a built-in only logs, but a formation of a player's takes their suit offline like a crashed program. The file's first lines are its header: // @formation "Name" makes it a formation (in a program-store file it also keeps the editor from running it as a drone program); // @description is a line for the picker; // @cadence N plans every N ticks (5 to 10, default 5); // @pattern surround|escort|ring takes ctx.params from the squad's pattern of that kind; // @order N sorts the built-ins.
vec(x: number, y: number, z: number)A point.
add(a: {x: number, y: number, z: number}, b: {x: number, y: number, z: number})a + b.
sub(a: {x: number, y: number, z: number}, b: {x: number, y: number, z: number})a - b.
scale(a: {x: number, y: number, z: number}, k: number)a times k.
len(a: {x: number, y: number, z: number})The length of a.
dist(a: {x: number, y: number, z: number}, b: {x: number, y: number, z: number})The distance between two points.
flat(a: {x: number, y: number, z: number})a with y set to 0.
norm(a: {x: number, y: number, z: number})a scaled to length 1 ({0, 0, 0} for a point at the origin).
rotY(a: {x: number, y: number, z: number}, radians: number)a turned round the vertical axis.
yawVec(degrees: number)The horizontal unit vector a Minecraft yaw faces: 0 is +z, 90 is -x. owner.yaw and owner.anchorYaw are in these degrees.
toward(from: {x: number, y: number, z: number}, to: {x: number, y: number, z: number}, step: number)from moved toward to by at most step (to itself when it is that close).
clamp(v: number, lo: number, hi: number)v kept between lo and hi.
component(body: (component: Component) => void) | component(id, body)Ability scripts (data/<namespace>/abilities/<any folders>/<name>.js): declares an ability as a component -- the parts it is built from, and the phases it can be in. A file that declares one may leave it unnamed and the file name is its id; a file may declare any number in loops, naming each (component(id, ...)), over the read-only lists registry(), tag(), tags() and data(). The body runs once at load and builds the same definition the JSON format holds, through the builder it is handed; it never runs on the key press. Every builder method: section ability_script, or docs/thematic.d.ts.
ability(id, type, options?)Ability scripts: defines an ability of a built-in Java type (docs/ability-types.md), configured as its JSON file would be.
preset(id: string, spec: PresetSpec, apply: (def: ContentBuilder, options: any) => void): voidPreset libraries: data/<namespace>/content_prefabs/<any folders>/<file>.js. A file declares any number of presets, preset(id, { kind, doc, options }, (def, options) => { ... }), each a bundle of fields and hooks a definition of that kind applies by name with def.use(id, options). The function is handed the definition's own builder, so it calls the same set() and hook methods the file would. A bare id is thematic:.
contentSample(body: (def: ContentBuilder) => void): void | contentSample(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | contentSample(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidA sample kind that proves the scripted-content pipeline end to end; the engine ships no files of it. data/<namespace>/content_samples/<any folders>/<id>.json or .js. Files in data/<namespace>/content_samples/ call it: once, when the file name is the id, or once per id, naming each (a family file).
statusEffect(body: (def: ContentBuilder) => void): void | statusEffect(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | statusEffect(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidStatus effects: data/<namespace>/status_effects/<any folders>/<id>.json or .js. The file name is the effect's id (status_effects/stun.json is thematic:stun). Each id registers its own status effect at startup, so a new one, or a changed category, colour, attribute modifier or stun flag, needs a restart; everything else reloads with /reload. How the effect looks on a suit is an effect_visuals file of the same id. Files in data/<namespace>/status_effects/ call it: once, when the file name is the id, or once per id, naming each (a family file).
damageElement(body: (def: ContentBuilder) => void): void | damageElement(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | damageElement(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidWhat a hit can be made of, for the weakness, resistance and immunity system: data/<namespace>/damage_elements/<any folders>/<id>.json or .js. The file name is the element's word (damage_elements/fire.json is "fire", the word suits, ability options and the element option use). The damage types themselves stay vanilla damage_type JSON; an element says which of them, and which damage-type tags, count as it. A hit also counts when its type is in the tag <namespace>:is_<id> (when the element has a damage_type) or <namespace>:compat/<mod>/<id> (while that compat module bridges damage types). Read on each side at startup and reloaded with /reload; a pack overrides an engine element by writing the same id. Files in data/<namespace>/damage_elements/ call it: once, when the file name is the id, or once per id, naming each (a family file).
vfxPreset(body: (def: ContentBuilder) => void): void | vfxPreset(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | vfxPreset(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidNamed effects the vfx action plays: data/<namespace>/vfx/<any folders>/<id>.json or .js. The file name is the id (vfx/sonar_beam.json is thematic:sonar_beam); the old upper-case preset names still find them ("SONAR_BEAM" is thematic:sonar_beam). Read on the server, which sends the finished effect to every client near it, and reloaded with /reload. Files in data/<namespace>/vfx/ call it: once, when the file name is the id, or once per id, naming each (a family file).
block(body: (def: ContentBuilder) => void): void | block(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | block(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidBlocks: data/<namespace>/blocks/<any folders>/<id>.json or .js registers the block <namespace>:<id> and its block item. Blocks are registry entries, read once at startup from mod jars, the dev tree and thematic/packs: a new block or a changed identity field needs a restart. The type is the block's shape and rules (a vanilla shape: solid, slab, stairs, wall, door...); a .json file names it (or a parent that does), a script's block(def => ...) is solid when it names none. Its model, blockstate, loot table and lang lines are ordinary resource files under the same id. Files in data/<namespace>/blocks/ call it: once, when the file name is the id, or once per id, naming each (a family file).
item(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | item(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidItems: data/<namespace>/items/<any folders>/<id>.json or .js registers the item <namespace>:<id>. Items are registry entries, read once at startup from mod jars, the dev tree and thematic/packs: a new item or a changed identity field needs a restart, while hooks, stats and looks retune on /reload. Files in data/<namespace>/items/ call it: once, when the file name is the id, or once per id, naming each (a family file).
arrow(body: (def: ContentBuilder) => void): void | arrow(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | arrow(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidTrick arrows: data/<namespace>/arrows/<any folders>/<id>.json or .js. Each id registers its own arrow item and entity type at startup (a restart is needed for a new one); the definition is what its hits do, how it looks and how it flies, and it reloads with /reload. Files in data/<namespace>/arrows/ call it: once, when the file name is the id, or once per id, naming each (a family file).
throwable(body: (def: ContentBuilder) => void): void | throwable(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | throwable(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidThings thrown and flying: gadgets, grenades, vials, thrown items. data/<namespace>/throwables/<any folders>/<id>.json or .js. Every one is the entity thematic:thrown; the definition is how it flies, looks and what its hits do. A definition with an item block also registers that item (a restart is needed for a new one). Files in data/<namespace>/throwables/ call it: once, when the file name is the id, or once per id, naming each (a family file).
criminal(body: (def: ContentBuilder) => void): void | criminal(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | criminal(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidCriminals: data/<namespace>/criminals/<any folders>/<id>.json or .js. Each id registers its own entity type, attributes, spawn rules and spawn egg (<id>_spawn_egg) at startup (a restart is needed for a new one, or for new stats); the look, the fight and the hooks reload with /reload. A crew file defines many ids, criminal("<id>", "thematic:criminal", c => c.use("thematic:criminal_<role>", { ...crew })). Files in data/<namespace>/criminals/ call it: once, when the file name is the id, or once per id, naming each (a family file).
entity(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | entity(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidEntity types: data/<namespace>/entities/<any folders>/<id>.json or .js. "type" names the body (a generic entity: zone, tether, owner_copy, mob, vehicle...) and the file sets its options. Each id registers its own entity type at startup (and a spawn egg <id>_spawn_egg with spawn_egg), so a new one, or a changed size, group, tracking, attributes, egg or boss flag, needs a restart; the render block and the body's options reload with /reload. Files in data/<namespace>/entities/ call it: once, when the file name is the id, or once per id, naming each (a family file).
profession(body: (def: ContentBuilder) => void): void | profession(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | profession(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidVillager professions: data/<namespace>/professions/<id>.json. The file name is the profession's id, and an unemployed villager near the workstation block takes the job, restocking there like any villager. Its name is the lang key entity.minecraft.villager.<file name>. A new id or a changed workstation needs a restart; the look and trades reload. Files in data/<namespace>/professions/ call it: once, when the file name is the id, or once per id, naming each (a family file).
suit(body: (def: ContentBuilder) => void): void | suit(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | suit(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidSuits: data/<namespace>/armors/<collection>/<any folders>/<id>.json or .js. The first folder under armors/ is the suit's collection (data/<namespace>/collections/<collection>.json describes it). A suit's id is always thematic:<file name>, whatever namespace the file is in. A file with "parent", or one under a folder named alt/, is an alt (a skin variant) of its parent. Each id registers its own suit item at startup (a restart is needed for a new one); the kit reloads with /reload. When two packs define one suit id, the higher pack's file is the one /reload applies, whatever folder it sits in, unless the other sets a higher "priority". Files in data/<namespace>/armors/ call it: once, when the file name is the id, or once per id, naming each (a family file).
abilityEffect(body: (def: ContentBuilder) => void): void | abilityEffect(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | abilityEffect(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidAbility effects: data/<namespace>/ability_effects/<any folders>/<id>.json or .js. What a beam, a bolt or a spray carries (an ability's "effects": ["thematic:laser", {"id": "thematic:blindness", "duration": 3.5}]) and does to what it hits. Read before abilities are, and again on /reload. Files in data/<namespace>/ability_effects/ call it: once, when the file name is the id, or once per id, naming each (a family file).
effectVisual(body: (def: ContentBuilder) => void): void | effectVisual(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | effectVisual(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidHow a status effect shows on a suit: data/<namespace>/effect_visuals/<any folders>/<effect>.json or .js. The file name is the status effect's id (effect_visuals/frozen.json is thematic:frozen), or its effect field names one. Synced to clients and reloaded with /reload; an effect with no file shows nothing on the suit. Files in data/<namespace>/effect_visuals/ call it: once, when the file name is the id, or once per id, naming each (a family file).
animController(body: (def: ContentBuilder) => void): void | animController(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | animController(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidThe GeckoLib controllers every suit carries: data/<namespace>/anim_controllers/<any folders>/<id>.json or .js. The engine ships thematic:armor (anim_controllers/armor.js); a pack changes it by shipping the same path, or adds to it with a file of its own id. Every definition is read together, thematic:armor first and the rest in id order: a later one's idle wins, its states are tried before the earlier ones', its while_active controllers and trigger_clips_from are added, and its effect_clips replace the same effect's. What plays reloads with /reload; which controllers a suit has is fixed when the suit is first drawn. Synced to clients. Files in data/<namespace>/anim_controllers/ call it: once, when the file name is the id, or once per id, naming each (a family file).
encounter(body: (def: ContentBuilder) => void): void | encounter(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | encounter(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidReady-made missions: data/<namespace>/encounters/<any folders>/<id>.json or .js. The file name is the template's name (/thematic dev encounter template <name>, /thematic dev encounter import <name>, an NPC start). The fields are the wave spawner's own encounter NBT, key for key, so a mission authored in game and exported with /thematic dev encounter export is already a valid file. Read by the server and reloaded with /reload; a file in config/thematic/encounters/ with the same name wins. Files in data/<namespace>/encounters/ call it: once, when the file name is the id, or once per id, naming each (a family file).
arenaTheme(body: (def: ContentBuilder) => void): void | arenaTheme(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | arenaTheme(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidRemembered places shown round a mission: data/<namespace>/arena_themes/<any folders>/<id>.json or .js. An encounter names one in Arena.Memory (a bare name means thematic:). The place is vanilla structure templates, dissolved at their edges and set in a ring beyond the fight, shown only to the participants. Read by the server and reloaded with /reload. Files in data/<namespace>/arena_themes/ call it: once, when the file name is the id, or once per id, naming each (a family file).
pocketDimension(body: (def: ContentBuilder) => void): void | pocketDimension(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | pocketDimension(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidPocket dimensions a wearer is sent into for a while and then brought back from: data/<namespace>/pocket_dimensions/<any folders>/<id>.json or .js. The file name is the pocket's id, which the Java that sends someone in names. The dimension itself stays vanilla data (dimension/<id>.json and its dimension_type, whose coordinate_scale scales the walk back out). Read by the server and reloaded with /reload. Files in data/<namespace>/pocket_dimensions/ call it: once, when the file name is the id, or once per id, naming each (a family file).
multiblock(body: (def: ContentBuilder) => void): void | multiblock(type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): void | multiblock(id: string, type: string, body?: ((def: ContentBuilder) => void) | { [field: string]: unknown }): voidStructures built from blocks the player places in a layout: data/<namespace>/multiblocks/<any folders>/<id>.json or .js. Using a placed part runs the thematic:assemble action, which swaps the layout for another set of blocks (a model block and its fillers); breaking any of them runs thematic:disassemble, which swaps the parts back. Read by the server and reloaded with /reload. Files in data/<namespace>/multiblocks/ call it: once, when the file name is the id, or once per id, naming each (a family file).
gun(body: (gun: Gun) => void) | gun(type, body)Gun scripts (data/<namespace>/guns/<any folders>/<id>.js; gun_effects/ calls gunEffect(type, fields), huds/ calls hud(body)): declares the file's gun, effect or HUD, built once at load into the same JSON the .json format holds. Every builder method: section gun_script, or docs/thematic.d.ts.
drone
The unit the program runs on: a body it can read, move and make work. Calls that read answer at once; calls that return an action do their work over time when the program yields them (yield drone.moveTo(p)), evaluating to {ok: true, ...} or {ok: false, reason}. The drone acts as its owner and only where its owner may: within 32 blocks of them, in loaded chunks, never in a claim or spawn protection, never on bedrock or anything harder than a drone may break. Block actions reach 5 blocks from the drone.
drone.pos(): {x: number, y: number, z: number}Where the drone is.
drone.facing(): {x: number, y: number, z: number}The way the drone faces, as a horizontal unit vector (y is 0).
drone.health(): numberThe drone's health, in the units the action bar shows.
drone.inventory(): ({id: string, count: number} | null)[]What the drone carries: nine slots, each {id, count} or null when empty. What it digs and harvests goes here.
drone.count(itemId: string): numberHow many of an item ("minecraft:torch") the drone carries.
drone.free(): numberHow many of its nine slots are empty.
drone.isFull(): booleanWhether nothing more fits: no empty slot and no stack with room.
drone.blockAt(p: {x: number, y: number, z: number} | [number, number, number]): {id: string, hardness: number, loaded: boolean, mature?: boolean}The block at p: its id, its hardness (negative: unbreakable) and, for a crop, whether it is fully grown. Anything outside 32 blocks of the owner or in an unloaded chunk reads {id: "unloaded", hardness: -1, loaded: false}.
drone.scanBlocks(options?: {center?: {x: number, y: number, z: number} | [number, number, number], radius?: number, match?: string, limit?: number}): {x: number, y: number, z: number, id: string}[]Blocks near a point (center: the drone), nearest first. radius is at most 16 and the answer at most 64 blocks. match is a block id ("minecraft:diamond_ore"), a block tag ("#minecraft:logs"), "mature_crop" or "young_crop"; none matches every non-air block. Scans, blocks and entities together, run at most four a second; past that the answer is an empty list.
drone.scanEntities(options?: {center?: {x: number, y: number, z: number} | [number, number, number], radius?: number, type?: string, where?: {healthBelow?: number, inCombatWithin?: number, hostile?: boolean, player?: boolean, mob?: boolean, owned?: boolean, teammate?: boolean, entity?: string | string[], airborne?: boolean, onGround?: boolean, hasEffect?: string | string[], effectLevel?: {id: string, at_least: number}, flag?: string | string[], not?: any, any?: any[], all?: any[]}, limit?: number}): {id: string, type: string, name: string, x: number, y: number, z: number, health?: number, maxHealth?: number, player: boolean, hostile: boolean, teammate: boolean, ally: boolean, owned: boolean, airborne: boolean, entity: string, distance?: number, item?: string, count?: number}[]Entities near a point (center: the drone), nearest first, never the drone itself. radius is at most 16 and the answer at most 64. type is "hostile", "player", "living" (the default), "item" or an entity type id. `where` keeps only the bodies that pass a guard, e.g. {where: {hostile: true, airborne: true}}: the same guard objects an ability's `when:` takes, limited to the keys that ask about a body (see check). Same scan rate as scanBlocks.
drone.moveTo(p: {x: number, y: number, z: number} | [number, number, number], options?: {within?: number}): actionFlies to the block at p, until within `within` blocks (0.4, at least 0.3). Refused when p is out of the owner's range or unloaded.
drone.follow(who?: "owner" | {id: string} | string, options?: {distance?: number}): actionGoes to someone and finishes once within `distance` blocks (6): the owner by default, which also sends the drone back to its place beside them.
drone.patrol(points: {x: number, y: number, z: number} | [number, number, number][], options?: {loops?: number}): actionFlies the points in order, `loops` times (1; 0 for until interrupted). At most 32 points.
drone.breakBlock(p: {x: number, y: number, z: number} | [number, number, number]): actionBreaks the block at p as the owner, taking what drops into the drone's inventory (the overflow is dropped at the drone). Takes longer for harder blocks; at most one block every four ticks. Refused for bedrock, anything harder than 3, a container with items in it, anything unbreakable, or beyond 5 blocks of the drone. Evaluates to {ok, id, items}.
drone.placeBlock(p: {x: number, y: number, z: number} | [number, number, number], itemId: string): actionPlaces a block item from the drone's inventory at p, which must be air or something replaceable.
drone.deposit(chest: {x: number, y: number, z: number} | [number, number, number], options?: {keep?: object}): actionWalks to the container at `chest` (a chest input: `// @input chest "..."`, then args.chest) and puts the drone's items in it, except those `keep` names, e.g. {"minecraft:torch": 16} keeps 16 torches. Evaluates to {ok, moved}; {ok: false} when no chest was given, so check `args.chest` first and use setDelivery("owner") when it is undefined.
drone.giveToOwner(): actionGoes to the owner and hands over everything it carries.
drone.setDelivery(mode: "owner" | "inventory"): stringWhere what the drone collects (pickUp, collectNearby, what a break or harvest drops) goes: "inventory" (the default) keeps it in the drone's nine slots; "owner" puts it straight into the owner's inventory while they are online, in the drone's dimension and within 32 blocks of it, and drops what does not fit at their feet where only they can pick it up. Out of range or offline, it falls back to the drone's inventory. A trail of sparks marks each delivery. Returns the mode.
drone.pickUp(item: {id: string} | string | string): actionFlies to a dropped item (an entity from scanEntities({type: "item"}), or an item id such as "minecraft:cobblestone" for the nearest one) and picks it up, into the inventory or the owner's per setDelivery. Waits out the item's pickup delay. Refused for an item another player threw in the last 30 seconds, one that belongs to another player, one within a block of a player who is not the owner, or one out of the owner's range. Evaluates to {ok, count}.
drone.collectNearby(options?: {radius?: number, filter?: string | string[], limit?: number}): actionPicks up the items round the drone, nearest first, under pickUp's rules, until none is left or `limit` items are in (2304 by default). radius is at most 16 (8 by default); filter is an item id, an item tag ("#minecraft:logs") or a list of them. Evaluates to {ok, count}, ok even when there was nothing to take.
drone.take(p: {x: number, y: number, z: number} | [number, number, number], itemId: string, n: number): actionWalks to the container at p and takes up to n of an item into the drone's inventory.
drone.harvest(p: {x: number, y: number, z: number} | [number, number, number]): actionBreaks a fully grown crop at p and keeps what it drops; remembers its seed for replant. Refused when it is not grown.
drone.replant(p: {x: number, y: number, z: number} | [number, number, number]): actionPlants again the crop last harvested at p, from the drone's own seeds.
drone.bonemeal(p: {x: number, y: number, z: number} | [number, number, number]): actionUses one bone meal from the drone's inventory on the crop at p.
drone.attack(who: {id: string} | string): actionFights one body with its own moves until it is dead or a minute has passed. Never the owner, their team, their units, or (unless the server allows it) players. Evaluates to {ok, killed}.
drone.focus(options?: {type?: string}): actionFights up to eight bodies near it, nearest first, in place of its own choice; type is as scanEntities'. Finishes when they are dead or a minute has passed.
drone.guard(center: {x: number, y: number, z: number} | [number, number, number], radius: number, options?: {ticks?: number}): actionHolds the circle of that radius (2 to 48) round center, fighting hostiles that enter it, for `ticks` (until interrupted by default).
drone.useMove(abilityId: string, target?: {id: string} | string): actionFires one of the drone's own moves, e.g. "thematic:tractor_beam" (or just "tractor_beam": a bare id means thematic:), at a target, on the move's usual cooldown. Refused when the drone does not have the move or it is not ready. A player may be the target only when the server allows combat against players, or they set off an enemyAbility event in the last ten seconds.
drone.sleep(seconds: number): actionDoes nothing for a while: the way to wait, in seconds as the pack's wait(seconds) is, e.g. yield drone.sleep(1.5). At least one tick; at most an hour.
drone.wait(ticks: number): actionDEPRECATED: use sleep(seconds). Does nothing for a while, counted in TICKS (20 is a second), where a pack's wait counts seconds. Still works; yield 20 (a bare number yields ticks) does the same.
drone.notify(text: string, options?: {color?: string, chat?: boolean}): booleanShows the owner (only them) a line of text, on their action bar or, with chat: true, in their chat: the pack's notify (color is a Minecraft formatting such as "red", "gold", "aqua"). At most four a second; answers false when one was dropped. log is for you in the editor; notify is for the player in the world.
drone.check(guard: {healthBelow?: number, inCombatWithin?: number, hostile?: boolean, player?: boolean, mob?: boolean, owned?: boolean, teammate?: boolean, entity?: string | string[], airborne?: boolean, onGround?: boolean, hasEffect?: string | string[], effectLevel?: {id: string, at_least: number}, flag?: string | string[], not?: any, any?: any[], all?: any[]}, who?: {id: string} | string): booleanAsks a guard about the drone, or about `who` (an entity from a scan or event, or "owner"), measured against the owner: owned and teammate say how the body stands to them. A guard is the same object an ability's `when:` takes and is read by the same evaluator, limited to the keys that ask about a body: healthBelow (health-bar units, 1000 is full), inCombatWithin (seconds), hostile, player, mob, owned, teammate, entity (type id or #tag), airborne, onGround, hasEffect, effectLevel, flag (read only), and not, any (or) and all (and) to combine. Any other key is an error that lists these. e.g. drone.check({hostile: true, not: {airborne: true}}, target).
drone.log(...text: any): voidAdds a line to the program's log, the one the editor shows.
drone.on(event: string, fn: (event: {ability: string, tags: string[], caster: {id: string, type: string, name: string, x: number, y: number, z: number, health?: number, maxHealth?: number, player: boolean, hostile: boolean, teammate: boolean, ally: boolean, owned: boolean, airborne: boolean, entity: string, distance?: number, item?: string, count?: number}, target?: {id: string, type: string, name: string, x: number, y: number, z: number, health?: number, maxHealth?: number, player: boolean, hostile: boolean, teammate: boolean, ally: boolean, owned: boolean, airborne: boolean, entity: string, distance?: number, item?: string, count?: number}, direction: {x: number, y: number, z: number}, tick: number}) => void, options?: {ability?: string | string[]}): booleanRuns fn each time an event fires, under the program's own budget. The one event is "enemyAbility": another player's ability, heard when they carry the console's gate effect at its level (a status effect at its cap, for a summoner's units) within 32 blocks of the owner and are not on the owner's side; the server decides whether it is on at all. The event is {ability, tags, caster, target, direction, tick}. Each subscription costs capacity. Returns false when this console gives no such event. options.ability keeps only those moves, e.g. {ability: "feral_leap"}: a bare id means thematic:, as everywhere in a pack. Inside fn, drone.interrupt(...) swaps the program's current action.
drone.interrupt(action: action): voidReplaces the action the program is waiting on with this one; the program carries on when the new action finishes. For an event handler: drone.interrupt(drone.useMove(id, event.caster)).
owner
The player the drone works for. Read only; every call answers at once.
owner.pos(): {x: number, y: number, z: number}Where the owner is.
owner.health(): numberTheir health, in the units the action bar shows (a full bar is 1000 in a suit).
owner.maxHealth(): numberTheir full health, in those units.
owner.pool(): numberTheir health as a fraction of full, 0 to 1.
owner.overshield(): numberTheir overshield (absorption), in the same units.
owner.inCombat(): booleanWhether they took or dealt damage in the last ten seconds.
owner.facing(): {x: number, y: number, z: number}The way the owner faces, as a horizontal unit vector (y is 0): where a tunnel dug "the way I am looking" runs.
owner.distance(): numberHow far they are from the drone.
owner.check(guard: {healthBelow?: number, inCombatWithin?: number, hostile?: boolean, player?: boolean, mob?: boolean, owned?: boolean, teammate?: boolean, entity?: string | string[], airborne?: boolean, onGround?: boolean, hasEffect?: string | string[], effectLevel?: {id: string, at_least: number}, flag?: string | string[], not?: any, any?: any[], all?: any[]}): booleanAsks a guard about the owner: owner.check({healthBelow: 300}) is true when they are below 300 on the health bar (1000 is full; owner.pool() is the fraction). The same guard objects an ability's `when:` takes; see drone.check for the keys.
action
Something a program waits on: what a drone call returns. yield it; it evaluates to {ok: true, ...} or {ok: false, reason}.