import crypto from "node:crypto"; import { abs, // type Coordinates, clear, execute, fill, type GAMERULES, gamerule, give, type JSONTextComponent, kill, LootTable, type LootTableEntry, type LootTableJSON, raw, rel, type Score, Selector, SelectorClass, // type SingleEntityArgument, setblock, summon, teleport, tellraw, time as timeCmd, data, Variable, } from "sandstone"; import type { ATTRIBUTES, BLOCKS, ENTITY_TYPES, ITEMS } from "sandstone/arguments/generated"; import { Commands } from "./commands"; import { ALL, DISPLAY_TAG, END_STATE_KEY, KRADLE_STORAGE } from "./constants"; import type { LiteralStringUnion } from "./utils"; /// Targets that can be used in Actions export type TargetNames = LiteralStringUnion<"all" | "self"> | SelectorClass; export function mapTarget(target: string | SelectorClass): SelectorClass | string { switch (target) { case "all": return ALL; case "self": return "@s"; } if (target instanceof SelectorClass) { return target; } // check if the target is a valid entity type if (target.startsWith("minecraft:")) { return Selector("@e", { type: target }); } return Selector("@a", { tag: target }); // Assuming the target is a team name } export const Actions = { /** * Send a chat message to everyone. * @param {JSONTextComponent} message - The message to send. */ announce: ({ message }: { message: JSONTextComponent }) => { tellraw("@a", ["\n", { text: DISPLAY_TAG, color: "aqua" }, " | ", message, "\n"]); }, /** * Clear the inventory of a target. * @param {TargetNames} target - The target to clear the inventory of. */ clear: ({ target }: { target: TargetNames }) => { clear(mapTarget(target)) }, /** * Custom action allowing to run any Sandstone command. * * @param {() => void} callback - The function to execute. */ custom: (callback: () => void) => { callback(); }, /** * Fill a region with a block. * @param {BLOCKS} block - The block to fill the region with. * @param {number} x1 - The x coordinate of the region. * @param {number} y1 - The y coordinate of the region. * @param {number} z1 - The z coordinate of the region. * @param {number} x2 - The x coordinate of the region. * @param {number} y2 - The y coordinate of the region. * @param {number} z2 - The z coordinate of the region. * @param {boolean} absolute - Whether the coordinates are absolute or relative. */ fill: ({ block, x1, y1, z1, x2, y2, z2, absolute, mode, }: { block: BLOCKS; x1: number; y1: number; z1: number; x2: number; y2: number; z2: number; absolute: boolean; mode: "fill" | "line" | "pyramid"; }) => { // fill the region with the block if (mode === "fill") { const coordinates1 = absolute ? abs(x1, y1, z1) : rel(x1, y1, z1); const coordinates2 = absolute ? abs(x2, y2, z2) : rel(x2, y2, z2); fill(coordinates1, coordinates2, block); return; } // draw a line from the first coordinate to the second coordinate if (mode === "line") { const dx = x2 - x1; const dy = y2 - y1; const dz = z2 - z1; const length = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz)); const stepX = dx / length || 0; const stepY = dy / length || 0; const stepZ = dz / length || 0; for (let i = 0; i <= length; i++) { const x = Math.round(x1 + stepX * i); const y = Math.round(y1 + stepY * i); const z = Math.round(z1 + stepZ * i); setblock(absolute ? abs(x, y, z) : rel(x, y, z), block); } return; } if (mode === "pyramid") { const height = Math.abs(y2); const direction = Math.sign(y2); // +1 for up, -1 for down for (let i = 0; i < height; i++) { const y = y1 + i * direction; const layerRadius = (height - 1 - i) * 2; const minX = x1 - layerRadius; const maxX = x1 + layerRadius; const minZ = z1 - layerRadius; const maxZ = z1 + layerRadius; for (let x = minX; x <= maxX; x++) { for (let z = minZ; z <= maxZ; z++) { setblock(absolute ? abs(x, y, z) : rel(x, y, z), block); } } } return; } // if mode is not fill or line, throw an error throw new Error(`Invalid fill mode: ${mode}`); }, /** * Give an item to a target. * @param {ITEMS} item - The item to give. * @param {TargetNames} target - The target to give the item to. * @param {number} count - The number of items to give. */ give: ({ item, target, count = 1 }: { item: ITEMS; target: TargetNames; count?: number }) => { give(mapTarget(target), item, count); }, /** * Give loot to a target with a weighted chance for selecting one of the items. * @param {{ name: ITEMS, count: number, weight: number }[]} items - The items to give. * @param {TargetNames} target - The target to give the item to. */ giveLoot: ({ items, target }: { items: [{ name: ITEMS; count: number; weight: number }]; target: TargetNames }) => { // sort incoming items and create a hash for table re-use const lootItemsSorted = [...items].sort((a, b) => a.name.localeCompare(b.name)); const lootItemsJson = JSON.stringify(lootItemsSorted); const lootItemsHash = crypto.createHash("sha256").update(lootItemsJson).digest("hex"); const lootTableName = `loot_${lootItemsHash}`.slice(0, 16); // create the entries for the loot table const entries: LootTableEntry[] = items.map((item) => ({ type: "minecraft:item", name: item.name, weight: item.weight, functions: [ { function: "set_count", count: item.count, }, ], })); // create the loot table with simple settings const lootTable: LootTableJSON = { type: "minecraft:generic", pools: [ { rolls: 1, entries, }, ], }; // on conflict, ignore because we can re-use duplicate loot tables LootTable(lootTableName, lootTable, { onConflict: "ignore" }).give(mapTarget(target)); }, /** * Set a gamerule. * @param {GAMERULES} rule - The name of the gamerule. * @param {boolean | number} value - The value to set the gamerule to. */ gamerule: ({ rule, value }: { rule: GAMERULES; value: boolean | number }) => { gamerule(rule, value); }, /** * Kill entities matching a selector. * @param {SelectorArgument} selector - The entities to kill. */ kill: ({ selector }: { selector: TargetNames }) => { kill(mapTarget(selector)); }, /** * Set an attribute for a target. * @param {ATTRIBUTES} attribute_ - The attribute to set. * @param {number} value - The value to set the attribute to. * @param {TargetNames} target - The target to set the attribute for. */ setAttribute: ({ attribute_, value, target }: { attribute_: ATTRIBUTES; value: number; target: TargetNames }) => { execute.as(mapTarget(target)).run.attribute("@s", attribute_).baseSet(value); }, /** * Set the time of day. * @param {'day' | 'night'} time_ - The time to set. */ setTime: ({ time }: { time: "day" | "night" }) => { timeCmd.set(time); }, /** * Summon multiple entities at a specific location. */ summonMultiple: (params: { entity: ENTITY_TYPES; count: number; x: number; y: number; z: number; absolute: boolean; }) => { const coordinates = params.absolute ? abs(params.x, params.y, params.z) : rel(params.x, params.y, params.z); for (let i = 0; i < params.count; i++) { summon(params.entity, coordinates); } }, /** * Set the end state of the challenge. * @param {string} end_state - The end state to set. */ setEndState: ({ endState }: { endState: string }) => { data.modify.storage(KRADLE_STORAGE, END_STATE_KEY).set.value(endState); }, /** * Set a block at a specific location. */ setBlock: (params: { block: BLOCKS; x: number; y: number; z: number; absolute: boolean }) => { const coordinates = params.absolute ? abs(params.x, params.y, params.z) : rel(params.x, params.y, params.z); setblock(coordinates, params.block); }, // teleport // TODO: allow destination to be a SingleEntityArgument | Coordinates // - removed for now to allow XYZ from UI until we implement better form field for nested objects /** * Teleport entities to a specific location. * @param {TargetNames} target - The entities to teleport. * @param {number} x - The x coordinate of the destination. * @param {number} y - The y coordinate of the destination. * @param {number} z - The z coordinate of the destination. * @param {boolean} absolute - Whether the coordinates are absolute or relative. */ teleport: ({ target, x, y, z, absolute = true, }: { target: TargetNames; x: number; y: number; z: number; absolute: boolean; }) => { const coordinates = absolute ? abs(x, y, z) : rel(x, y, z); teleport(mapTarget(target), coordinates); }, /** * Send a chat message to a target. * @param {JSONTextComponent} message - The message to send. * @param {TargetNames} target - The target to send the message to. */ tellraw: ({ message, target }: { message: JSONTextComponent; target: TargetNames }) => { tellraw(mapTarget(target), message); }, // Score operations /** * Increment a score variable by 1. * @param variable - The score variable to increment. */ increment: ({ variable }: { variable: Score }) => { variable.add(1); }, /** * Decrement a score variable by 1. * @param variable - The score variable to decrement. */ decrement: ({ variable }: { variable: Score }) => { variable.remove(1); }, /** * Set a score variable to a specific value. * @param variable - The score variable to set. * @param value - The value to set the score variable to, which can be a number or another score variable. */ set: ({ variable, value }: { variable: Score; value: number | Score }) => { variable.set(value); }, /** * log a message with the watcher * @param {string} message - The message to send. * @param {Score} variable - The variable to log. * @param {boolean} store - Whether to store the variable in the backend. */ // WARNING: the logs must have precisely this structure to be read by the watcher. DO NOT CHANGE THE STRUCTURE. log_variable: ({ message, variable, store }: { message: string; variable: Score; store: boolean }) => { Commands.logVariable(message, variable, store); }, /** * Summon an item at a specific location. * @param {ITEMS} item - The item to summon. * @param {number} x - The x coordinate of the location. * @param {number} y - The y coordinate of the location. * @param {number} z - The z coordinate of the location. * @param {boolean} absolute - Whether the coordinates are absolute or relative. */ summonItem: ({ item, x, y, z, absolute }: { item: ITEMS; x: number; y: number; z: number; absolute: boolean }) => { const abs = absolute ? "" : "~"; raw(`summon item ${abs}${x} ${abs}${y} ${abs}${z} {Item:{id:"${item}",Count:1b}}`); }, /** * Count the number of a specific item in a target's inventory. * @param params - The parameters object. * @param params.target - The target to count items for. * @param params.item - The item to count. * @returns The variable containing the item count. */ countItems: ({ target, item }: { target: TargetNames; item: ITEMS }) => { const variable = Variable(); execute.store.result.score(variable).run.clear(mapTarget(target), item, 0); return variable; }, /** * Get the current player's position as x, y, z Score variables. * Must be called in a player context (e.g., inside forEveryPlayer or when @s is a player). * @returns An object with x, y, z Score variables containing the player's coordinates. */ getCurrentPlayerPosition: () => { const x = Variable(); const y = Variable(); const z = Variable(); execute.store.result.score(x).run.data.get.entity("@s", "Pos[0]"); execute.store.result.score(y).run.data.get.entity("@s", "Pos[1]"); execute.store.result.score(z).run.data.get.entity("@s", "Pos[2]"); return { x, y, z }; }, } satisfies Record any>;