import { _, type JSONTextComponent, type Score, Selector, type SelectorClass, tellraw } from "sandstone"; import { END_STATE_KEY, KRADLE_STORAGE, WATCHER, WINNER_TAG } from "./constants"; /** * Converts a Sandstone Selector to a JSON Text Component that will be displayed like a JSON array in the chat. * This is used to send selectors as part of JSON messages. * * @warning * This does NOT work with empty selectors (they will be represented as `[""]`). * Make sure to check if the selector matches any entities before using this function. * * @param selector The selector to convert. * @returns An array of JSON Text Components that will be displayed as a JSON array in the chat. * * @example * const selector = Selector("@a", { tag: "kradle_participant" }); * const jsonArray = selectorToJSONArray(selector); * tellraw("@a", selectorToJSONArray(selector)); * * // Everyone will see this in the chat: * // ["Player1","Player2","Player3"] */ function selectorToJSONArray(selector: SelectorClass): JSONTextComponent[] { return ['["', { selector: selector.toString(), separator: '","' } as any, '"]']; } /** * Converts a Sandstone Selector to a JSON string that will be displayed like a JSON string in the chat. * If the selector matches several entities, it will display a comma-separated list of their names. * * @param selector The selector to convert. * @returns A JSON string that will be displayed like a JSON string in the chat. * * @example * const selector = Selector("@s"); * const jsonString = selectorToJSONString(selector); * tellraw("@a", jsonString); * * // Everyone will see this in the chat: * // "Player1" */ function selectorToJSONString(selector: SelectorClass): JSONTextComponent[] { return ['"', { selector: selector.toString(), separator: "," } as any, '"']; } /** * Converts a storage to a JSON string that will be displayed like a JSON string in the chat. * @param storage The storage holding the value to convert. * @param storageKey The key of the value to convert in the storage. * @returns A JSON string that will be displayed like a JSON string in the chat. * * * If the value is found in the storage, it will be sent as a JSON string. * Ex: { "end_state": "win" } * * If the value is not found in the storage, an empty string will be sent. * Ex: { "end_state": "" } * */ function storageToJSONString(storage: string, storageKey: string): JSONTextComponent[] { return ['"', { storage: storage, nbt: storageKey, }, '"']; } /** * Commands for interacting with Kradle's Watcher. */ export const Commands = { /** * Raw command - sends a command to Kradle's Watcher. * * Arguments are passed as a JSON object. * @param command The command to send. * @param args The JSON Text Component arguments to send with the command. * * @warning * The arguments must resolve to a JSON-serializable object. This cannot be verified at compile time. * * @example * Commands.raw("start_challenge", { name: "My Challenge" }); * // The watcher will receive the following payload:: * // {"type":"kradle_command","command":"start_challenge","arguments":{"name":"My Challenge"}} * * Commands.raw("fetch", {url: "https://goldprice.org", "target_score": "gold_price", callback: "kradle:on_gold_price_fetched"}) * // The watcher will receive the following payload: * // {"type":"kradle_command","command":"fetch","arguments":{"url":"https://goldprice.org","target_score":"gold_price","callback":"kradle:on_gold_price_fetched"}} */ raw: (command: string, args: Record) => { const basePayload: string = JSON.stringify({ command, type: "kradle_command", arguments: {}, }); // We need to insert our arguments into the "arguments" field of the base payload. const splitIndex = basePayload.indexOf("}"); const leftText = basePayload.slice(0, splitIndex); const rightText = basePayload.slice(splitIndex); function parseValue(value: JSONTextComponent): JSONTextComponent { if (typeof value === "boolean") { return value.toString(); } if (typeof value === "string") { try { JSON.parse(value); } catch (error) { throw new Error(`When using Commands.raw with string values, they must represent a valid JSON object. Invalid JSON string: ${value}`); } } return value; } const argumentsPayload = Object.entries(args).flatMap(([key, value], index) => { const result = [`"${key}":`, parseValue(value)]; if (index > 0) { return [",", ...result]; } return result; }); tellraw(WATCHER, [leftText, argumentsPayload, rightText]); }, /** * Game over command - sends a command to Kradle's Watcher to end the challenge. * This will also announce the winners and the end state. * * The `winners` argument of the payload will be a JSON list of player names. * Ex: { "winners": ["player1", "player2"] } * * If no winners are selected, an empty array will be sent. * Ex: { "winners": [] } * * The `end_state` argument of the payload will be a JSON string that represents the end state of the challenge. * Ex: { "end_state": "win" } * * If no end state is selected, an empty string will be sent. * Ex: { "end_state": "" } */ gameOver: () => { const selector = Selector("@a", { tag: WINNER_TAG }); // If no winners are selected, we send them as a JSON array. _.if(selector, () => { Commands.raw("game_over", { winners: selectorToJSONArray(selector), end_state: storageToJSONString(KRADLE_STORAGE, END_STATE_KEY), }); }).else(() => { // Else, we send an empty array. Commands.raw("game_over", { winners: JSON.stringify([]), end_state: storageToJSONString(KRADLE_STORAGE, END_STATE_KEY), }); }); }, logVariable: (message: string, variable: Score, store: boolean) => { const objectiveName = variable.objective.name; const variableName = variable.target.toString(); return Commands.raw("log_variable", { message: JSON.stringify(message), store, variable: JSON.stringify({ objective: objectiveName, selector: variableName, }), value: JSON.stringify(variable), targets: selectorToJSONString(Selector(variableName as any)), }); }, };