// features/mb_pairs.js — partner / vertical / 2x2 multiblocks. import { world, system, BlockPermutation, ItemStack, isCreative, getDir, safeGetBlock } from "../core/mc.js"; import { fcCanReplace } from "../core/grid.js"; import { undoPlace, breakAir } from "../core/fx.js"; import { registerVariants } from "../core/variants.js"; import { onPlace, onBreak } from "../core/events.js"; const AIR_ONLY = (bl) => !!bl && bl.typeId === "minecraft:air"; const CAN_REPLACE = (bl) => !!bl && fcCanReplace(bl); // Horizontal L/R pair: placing FRONT auto-builds BACK at the facing offset (OFF), breaking either removes the pair. // canReplace gates the partner cell (air-only for fairy/web, fcCanReplace for furniture). OFF must define every facing + south. function registerPrefixPairRig(FRONT, BACK, OFF, label, canReplace) { onPlace((ev) => { const b = ev.block; if (!b.typeId.startsWith(FRONT)) return; const key = b.typeId.slice(FRONT.length), dir = getDir(b.permutation), off = OFF[dir] || OFF.south; const rb = safeGetBlock(b.dimension, { x: b.x + off[0], y: b.y + off[1], z: b.z + off[2] }); if (!canReplace(rb)) { undoPlace(b, ev.player, FRONT + key, label); return; } try { rb.setPermutation(BlockPermutation.resolve(BACK + key, { "minecraft:cardinal_direction": dir })); } catch {} }); onBreak((ev) => { const perm = ev.brokenBlockPermutation, id = perm?.type?.id || ""; let pair = null, s = 1; if (id.startsWith(FRONT)) { pair = BACK + id.slice(FRONT.length); s = 1; } else if (id.startsWith(BACK)) { pair = FRONT + id.slice(BACK.length); s = -1; } if (!pair) return; let dir; try { dir = perm.getState("minecraft:cardinal_direction"); } catch {} const off = OFF[dir] || OFF.south, b = ev.block; const nb = safeGetBlock(b.dimension, { x: b.x + s * off[0], y: b.y + s * off[1], z: b.z + s * off[2] }); if (nb && nb.typeId === pair) { try { breakAir(nb); } catch {} } }); } // Vertical b->t stack: placing the "b" cell (parse().cell==="b") builds the "t" cell one up; breaking either removes the pair. // idFor(p, cell) rebuilds a block id for the parsed piece p at "b" or "t". function registerVerticalStack(parse, idFor, label) { onPlace((ev) => { const b = ev.block, p = parse(b.typeId); if (!p || p.cell !== "b") return; const dir = getDir(b.permutation), ab = safeGetBlock(b.dimension, { x: b.x, y: b.y + 1, z: b.z }); if (!ab || !fcCanReplace(ab)) { undoPlace(b, ev.player, idFor(p, "b"), label); return; } try { ab.setPermutation(BlockPermutation.resolve(idFor(p, "t"), { "minecraft:cardinal_direction": dir })); } catch {} }); onBreak((ev) => { const p = parse(ev.brokenBlockPermutation?.type?.id || ""); if (!p) return; const b = ev.block, dy = p.cell === "b" ? 1 : -1, other = idFor(p, p.cell === "b" ? "t" : "b"); const nb = b.dimension.getBlock({ x: b.x, y: b.y + dy, z: b.z }); if (nb && nb.typeId === other) { try { breakAir(nb); } catch {} } }); } export const FAIRY_L = "rzb_dcs:sp13_", FAIRY_R = "rzb_dcs:sp29_"; export const FAIRY_THEMES = ["clovers", "hearts_red", "hearts_pink", "hearts_white_red", "hearts_white_pink", "hearts_red_pink"]; export const FAIRY_RIGHT_OFF = { north: [1, 0, 0], south: [-1, 0, 0], east: [0, 0, 1], west: [0, 0, -1] }; registerVariants(FAIRY_THEMES.map((t) => FAIRY_L + t)); registerVariants(FAIRY_THEMES.map((t) => FAIRY_R + t)); export const FAIRY_THEMES_OCEAN = ["seahorse", "pufferfish", "clownfish", "ocean"]; export const FAIRY_THEMES_HALLOWEEN = ["ghosts", "bats", "pumpkins"]; for (const set of [FAIRY_THEMES_OCEAN, FAIRY_THEMES_HALLOWEEN]) { registerVariants(set.map((t) => FAIRY_L + t)); registerVariants(set.map((t) => FAIRY_R + t)); } registerPrefixPairRig(FAIRY_L, FAIRY_R, FAIRY_RIGHT_OFF, "fairy lights", AIR_ONLY); // --- Holiday signs (hs__l anchor / hs__r auto): 2-cell directional banner, same L/R recipe as fairy lights. --- export const HS_PREFIX = "rzb_dcs:hs_", HS_RIGHT_OFF = FAIRY_RIGHT_OFF; onPlace((ev) => { const b = ev.block; if (!b.typeId.startsWith(HS_PREFIX) || !b.typeId.endsWith("_l")) return; const base = b.typeId.slice(0, -2), dir = getDir(b.permutation), off = HS_RIGHT_OFF[dir] || [1, 0, 0]; const rb = safeGetBlock(b.dimension, { x: b.x + off[0], y: b.y + off[1], z: b.z + off[2] }); if (!rb || rb.typeId !== "minecraft:air") { undoPlace(b, ev.player, b.typeId, "holiday sign"); return; } try { rb.setPermutation(BlockPermutation.resolve(base + "_r", { "minecraft:cardinal_direction": dir })); } catch {} }); onBreak((ev) => { const perm = ev.brokenBlockPermutation, id = perm?.type?.id || ""; if (!id.startsWith(HS_PREFIX)) return; let pair = null, s = 1; if (id.endsWith("_l")) { pair = id.slice(0, -2) + "_r"; s = 1; } else if (id.endsWith("_r")) { pair = id.slice(0, -2) + "_l"; s = -1; } if (!pair) return; let dir; try { dir = perm.getState("minecraft:cardinal_direction"); } catch {} const off = HS_RIGHT_OFF[dir] || HS_RIGHT_OFF.south, b = ev.block; const nb = safeGetBlock(b.dimension, { x: b.x + s * off[0], y: b.y + s * off[1], z: b.z + s * off[2] }); if (nb && nb.typeId === pair) { try { breakAir(nb); } catch {} } }); export const WEB4_L = "rzb_dcs:fa25_l_", WEB4_R = "rzb_dcs:fa25_r_"; export const WEB4_VARIANTS = ["a", "b"]; export const WEB4_RIGHT_OFF = { north: [1, 0, 0], south: [-1, 0, 0], east: [0, 0, 1], west: [0, 0, -1] }; registerVariants(WEB4_VARIANTS.map((v) => WEB4_L + v)); registerVariants(WEB4_VARIANTS.map((v) => WEB4_R + v)); registerPrefixPairRig(WEB4_L, WEB4_R, WEB4_RIGHT_OFF, "web", AIR_ONLY); const OFR_BASE = "rzb_dcs:fa26_", OFR_PIECES = ["bl", "br", "tl", "tr"]; // 3 colour sets: "" (red, default) / _orange / _yellow const OFR_RIGHT = { north: [1, 0, 0], south: [-1, 0, 0], east: [0, 0, 1], west: [0, 0, -1] }; // N/S flipped, E/W original (per-axis cardinal quirk, verified in-game) const ofrParse = (id) => { if (!id || !id.startsWith(OFR_BASE)) return null; const m = id.slice(OFR_BASE.length).match(/^(bl|br|tl|tr)(_orange|_yellow)?$/); return m ? { piece: m[1], color: m[2] || "" } : null; }; function ofrOffsets(dir) { const r = OFR_RIGHT[dir] || [1, 0, 0]; return { bl: [0, 0, 0], br: [r[0], r[1], r[2]], tl: [0, 1, 0], tr: [r[0], 1 + r[1], r[2]] }; } function ofrCells(ax, ay, az, dir) { const o = ofrOffsets(dir), c = {}; for (const p of OFR_PIECES) c[p] = { x: ax + o[p][0], y: ay + o[p][1], z: az + o[p][2] }; return c; } function ofrAnchor(piece, x, y, z, dir) { const o = ofrOffsets(dir)[piece]; return { x: x - o[0], y: y - o[1], z: z - o[2] }; } onPlace((ev) => { const b = ev.block, pr = ofrParse(b.typeId); if (!pr || pr.piece !== "bl") return; const col = pr.color, dir = getDir(b.permutation), cells = ofrCells(b.x, b.y, b.z, dir); for (const q of ["br", "tl", "tr"]) { const nb = safeGetBlock(b.dimension, cells[q]); if (!nb || nb.typeId !== "minecraft:air") { undoPlace(b, ev.player, OFR_BASE + "bl" + col, "ofrenda"); return; } } for (const q of ["br", "tl", "tr"]) { const nb = b.dimension.getBlock(cells[q]); try { nb.setPermutation(BlockPermutation.resolve(OFR_BASE + q + col, { "minecraft:cardinal_direction": dir })); } catch {} } }); onBreak((ev) => { const pr = ofrParse(ev.brokenBlockPermutation?.type?.id || ""); if (!pr) return; const col = pr.color, dir = getDir(ev.brokenBlockPermutation), b = ev.block, a = ofrAnchor(pr.piece, b.x, b.y, b.z, dir), cells = ofrCells(a.x, a.y, a.z, dir); for (const q of OFR_PIECES) { if (q === pr.piece) continue; const nb = b.dimension.getBlock(cells[q]); const np = nb && ofrParse(nb.typeId); if (np && np.piece === q && np.color === col) { try { breakAir(nb); } catch {} } } }); export const SWING_BASE = "rzb_dcs:sp31_"; export const SWING_PIECES = ["bl", "br", "tl", "tr"]; export const SWING_VARIANTS = ["black", "cherry", "ebony", "oak", "palm", "spruce", "white"]; const SWING_RIGHT = { north: [-1, 0, 0], south: [1, 0, 0], east: [0, 0, 1], west: [0, 0, -1] }; export function swingParse(typeId) { if (!typeId || !typeId.startsWith(SWING_BASE)) return null; const rest = typeId.slice(SWING_BASE.length), us = rest.indexOf("_"), piece = rest.slice(0, us), variant = rest.slice(us + 1); return SWING_PIECES.includes(piece) ? { piece, variant } : null; } function swingOffsets(dir) { const r = SWING_RIGHT[dir] || [1, 0, 0]; return { bl: [0, 0, 0], br: [r[0], r[1], r[2]], tl: [0, 1, 0], tr: [r[0], 1 + r[1], r[2]] }; } export function swingCells(ax, ay, az, dir) { const o = swingOffsets(dir), c = {}; for (const p of SWING_PIECES) c[p] = { x: ax + o[p][0], y: ay + o[p][1], z: az + o[p][2] }; return c; } export function swingAnchor(piece, x, y, z, dir) { const o = swingOffsets(dir)[piece]; return { x: x - o[0], y: y - o[1], z: z - o[2] }; } onPlace((ev) => { const b = ev.block, s = swingParse(b.typeId); if (!s || s.piece !== "bl") return; const dir = getDir(b.permutation), cells = swingCells(b.x, b.y, b.z, dir); for (const p of ["br", "tl", "tr"]) { const nb = safeGetBlock(b.dimension, cells[p]); if (!nb || nb.typeId !== "minecraft:air") { undoPlace(b, ev.player, SWING_BASE + "bl_" + s.variant, "swing"); return; } } for (const p of ["br", "tl", "tr"]) { const nb = b.dimension.getBlock(cells[p]); try { nb.setPermutation(BlockPermutation.resolve(SWING_BASE + p + "_" + s.variant, { "minecraft:cardinal_direction": dir })); } catch {} } }); onBreak((ev) => { const s = swingParse(ev.brokenBlockPermutation?.type?.id || ""); if (!s) return; const dir = getDir(ev.brokenBlockPermutation), b = ev.block, a = swingAnchor(s.piece, b.x, b.y, b.z, dir), cells = swingCells(a.x, a.y, a.z, dir); for (const p of SWING_PIECES) { if (p === s.piece) continue; const nb = b.dimension.getBlock(cells[p]); if (nb && swingParse(nb.typeId)?.piece === p) { try { breakAir(nb); } catch {} } } }); for (const p of SWING_PIECES) registerVariants(SWING_VARIANTS.map((v) => SWING_BASE + p + "_" + v)); export const ROPE_BASE = "rzb_dcs:sp32_"; export const ROPE_PIECES = ["b", "t"]; export const ROPE_VARIANTS = ["1", "2", "3", "4", "5", "6", "7", "8"]; export function ropeParse(typeId) { if (!typeId || !typeId.startsWith(ROPE_BASE)) return null; const rest = typeId.slice(ROPE_BASE.length), us = rest.indexOf("_"), piece = rest.slice(0, us), variant = rest.slice(us + 1); return ROPE_PIECES.includes(piece) ? { piece, variant } : null; } export function ropeBottom(piece, x, y, z) { return piece === "b" ? { x, y, z } : { x, y: y - 1, z }; } onPlace((ev) => { const b = ev.block, s = ropeParse(b.typeId); if (!s || s.piece !== "b") return; const dir = getDir(b.permutation), tb = safeGetBlock(b.dimension, { x: b.x, y: b.y + 1, z: b.z }); if (!tb || tb.typeId !== "minecraft:air") { undoPlace(b, ev.player, ROPE_BASE + "b_" + s.variant, "swing"); return; } try { tb.setPermutation(BlockPermutation.resolve(ROPE_BASE + "t_" + s.variant, { "minecraft:cardinal_direction": dir })); } catch {} }); onBreak((ev) => { const s = ropeParse(ev.brokenBlockPermutation?.type?.id || ""); if (!s) return; const b = ev.block, bot = ropeBottom(s.piece, b.x, b.y, b.z), otherPiece = s.piece === "b" ? "t" : "b"; const otherPos = otherPiece === "t" ? { x: bot.x, y: bot.y + 1, z: bot.z } : bot; const nb = b.dimension.getBlock(otherPos); if (nb && ropeParse(nb.typeId)?.piece === otherPiece) { try { breakAir(nb); } catch {} } }); for (const p of ROPE_PIECES) registerVariants(ROPE_VARIANTS.map((v) => ROPE_BASE + p + "_" + v)); const SP14_BASE = "rzb_dcs:sp14_base"; const isBalloon = (id) => !!id && id.startsWith("rzb_dcs:sp14_") && id !== SP14_BASE; registerVariants(["rzb_dcs:sp14_red", "rzb_dcs:sp14_orange", "rzb_dcs:sp14_yellow", "rzb_dcs:sp14_green", "rzb_dcs:sp14_cyan", "rzb_dcs:sp14_blue", "rzb_dcs:sp14_purple", "rzb_dcs:sp14_pink", "rzb_dcs:sp14_berry", "rzb_dcs:sp14_p_red", "rzb_dcs:sp14_p_orange", "rzb_dcs:sp14_p_yellow", "rzb_dcs:sp14_p_lime", "rzb_dcs:sp14_p_green", "rzb_dcs:sp14_p_cyan", "rzb_dcs:sp14_p_blue", "rzb_dcs:sp14_p_purple", "rzb_dcs:sp14_p_pink"]); onPlace((ev) => { const b = ev.block; if (!isBalloon(b.typeId)) return; const dir = getDir(b.permutation), below = safeGetBlock(b.dimension, { x: b.x, y: b.y - 1, z: b.z }); if (below && below.isAir) { try { below.setPermutation(BlockPermutation.resolve(SP14_BASE, { "minecraft:cardinal_direction": dir })); } catch {} } }); onBreak((ev) => { const id = ev.brokenBlockPermutation?.type?.id || "", b = ev.block; if (isBalloon(id)) { const below = safeGetBlock(b.dimension, { x: b.x, y: b.y - 1, z: b.z }); if (below && below.typeId === SP14_BASE) { try { below.setType("minecraft:air"); } catch {} } } else if (id === SP14_BASE) { const above = safeGetBlock(b.dimension, { x: b.x, y: b.y + 1, z: b.z }); if (above && isBalloon(above.typeId)) { try { above.setType("minecraft:air"); } catch {} } } }); export const TW_FRONT = "rzb_dcs:tw_fr_", TW_BACK = "rzb_dcs:tw_b_"; export const TW_COLORS = ["black", "white", "orange", "magenta", "light_blue", "yellow", "lime", "pink", "gray", "light_gray", "cyan", "purple", "blue", "green", "red", "rainbow"]; export const TW_BACK_OFF = { south: [0, 0, -1], north: [0, 0, 1], east: [-1, 0, 0], west: [1, 0, 0] }; registerPrefixPairRig(TW_FRONT, TW_BACK, TW_BACK_OFF, "beach towel", CAN_REPLACE); export const LO_FRONT = "rzb_dcs:lo_fr_", LO_BACK = "rzb_dcs:lo_b_"; export const LO_COLORS = ["birch", "oak", "palm", "spruce", "white"]; export const LO_BACK_OFF = { south: [0, 0, -1], north: [0, 0, 1], east: [-1, 0, 0], west: [1, 0, 0] }; registerPrefixPairRig(LO_FRONT, LO_BACK, LO_BACK_OFF, "lounge chair", CAN_REPLACE); export const LC_BASE = "rzb_dcs:lc_b_", LC_AWNING = "rzb_dcs:lc_a_"; export const LC_COLORS = ["white", "orange", "magenta", "light_blue", "yellow", "lime", "pink", "cyan", "purple", "blue", "red"]; onPlace((ev) => { const b = ev.block; if (!b.typeId.startsWith(LC_BASE)) return; const color = b.typeId.slice(LC_BASE.length), dir = getDir(b.permutation), ab = safeGetBlock(b.dimension, { x: b.x, y: b.y + 1, z: b.z }); if (!ab || !fcCanReplace(ab)) { undoPlace(b, ev.player, LC_BASE + color, "lifeguard chair"); return; } try { ab.setPermutation(BlockPermutation.resolve(LC_AWNING + color, { "minecraft:cardinal_direction": dir })); } catch {} }); onBreak((ev) => { const id = ev.brokenBlockPermutation?.type?.id || "", b = ev.block; if (id.startsWith(LC_BASE)) { const nb = b.dimension.getBlock({ x: b.x, y: b.y + 1, z: b.z }); if (nb && nb.typeId === LC_AWNING + id.slice(LC_BASE.length)) { try { breakAir(nb); } catch {} } } else if (id.startsWith(LC_AWNING)) { const nb = b.dimension.getBlock({ x: b.x, y: b.y - 1, z: b.z }); if (nb && nb.typeId === LC_BASE + id.slice(LC_AWNING.length)) { try { breakAir(nb); } catch {} } } }); export const CK_BOTTOM = "rzb_dcs:fa05_b", CK_TOP = "rzb_dcs:fa05_t"; onPlace((ev) => { const b = ev.block; if (b.typeId !== CK_BOTTOM) return; const dir = getDir(b.permutation), ab = safeGetBlock(b.dimension, { x: b.x, y: b.y + 1, z: b.z }); if (!ab || !fcCanReplace(ab)) { undoPlace(b, ev.player, CK_BOTTOM, "casket"); return; } try { ab.setPermutation(BlockPermutation.resolve(CK_TOP, { "minecraft:cardinal_direction": dir })); } catch {} }); onBreak((ev) => { const id = ev.brokenBlockPermutation?.type?.id || "", b = ev.block; if (id === CK_BOTTOM) { const nb = b.dimension.getBlock({ x: b.x, y: b.y + 1, z: b.z }); if (nb && nb.typeId === CK_TOP) { try { breakAir(nb); } catch {} } } else if (id === CK_TOP) { const nb = b.dimension.getBlock({ x: b.x, y: b.y - 1, z: b.z }); if (nb && nb.typeId === CK_BOTTOM) { try { breakAir(nb); } catch {} } } }); // Casket Ground: laying-down 2-cell horizontal pair. Place head (fa48) -> build foot (fa48_b) one block along facing. export const CG_HEAD = "rzb_dcs:fa48", CG_FOOT = "rzb_dcs:fa48_b"; export const CG_OFF = { south: [0, 0, 1], north: [0, 0, -1], east: [1, 0, 0], west: [-1, 0, 0] }; onPlace((ev) => { const b = ev.block; if (b.typeId !== CG_HEAD) return; const dir = getDir(b.permutation), off = CG_OFF[dir] || CG_OFF.south; const fb = safeGetBlock(b.dimension, { x: b.x + off[0], y: b.y + off[1], z: b.z + off[2] }); if (!fb || !fcCanReplace(fb)) { undoPlace(b, ev.player, CG_HEAD, "casket ground"); return; } try { fb.setPermutation(BlockPermutation.resolve(CG_FOOT, { "minecraft:cardinal_direction": dir })); } catch {} }); onBreak((ev) => { const id = ev.brokenBlockPermutation?.type?.id || "", b = ev.block; const dir = getDir(ev.brokenBlockPermutation) || "south", off = CG_OFF[dir] || CG_OFF.south; if (id === CG_HEAD) { const nb = b.dimension.getBlock({ x: b.x + off[0], y: b.y + off[1], z: b.z + off[2] }); if (nb && nb.typeId === CG_FOOT) { try { breakAir(nb); } catch {} } } else if (id === CG_FOOT) { const nb = b.dimension.getBlock({ x: b.x - off[0], y: b.y - off[1], z: b.z - off[2] }); if (nb && nb.typeId === CG_HEAD) { try { breakAir(nb); } catch {} } } }); export const SC_VARIANTS = ["1", "2", "3"]; export const scParse = (id) => { const m = /^rzb_dcs:fa36_([123])_(b|t)$/.exec(id || ""); return m ? { n: m[1], cell: m[2] } : null; }; registerVerticalStack(scParse, (p, cell) => "rzb_dcs:fa36_" + p.n + "_" + cell, "scarecrow"); export const bsParse = (id) => { const m = /^rzb_dcs:bl_small_(.+)_(b|t)$/.exec(id || ""); return m ? { v: m[1], cell: m[2] } : null; }; export const BS_CYCLES = { "1": ["1", "2", "3"], "2": ["1", "2", "3"], "3": ["1", "2", "3"], "p_1": ["p_1", "p_2", "p_3"], "p_2": ["p_1", "p_2", "p_3"], "p_3": ["p_1", "p_2", "p_3"] }; // brush cycles (halloween/valentines are standalone) registerVerticalStack(bsParse, (p, cell) => "rzb_dcs:bl_small_" + p.v + "_" + cell, "balloons"); export const bhParse = (id) => { const m = /^rzb_dcs:bl_hearts_(.+)_(b|t)$/.exec(id || ""); return m ? { v: m[1], cell: m[2] } : null; }; const BH_REG = ["cyan_red", "orange_blue", "pink", "pink_white", "purple_yellow", "rainbow", "yellow_white", "berry_green"]; const BH_PAS = ["p_blue_red", "p_cyan_yellow", "p_lime_orange", "p_pink_yellow", "p_purple_green"]; export const BH_CYCLES = {}; for (const v of BH_REG) BH_CYCLES[v] = BH_REG; for (const v of BH_PAS) BH_CYCLES[v] = BH_PAS; // brush: regular combos + pastel combos registerVerticalStack(bhParse, (p, cell) => "rzb_dcs:bl_hearts_" + p.v + "_" + cell, "heart balloons"); export const bbnParse = (id) => { const m = /^rzb_dcs:bl_bunch_(.+)_(b|t)$/.exec(id || ""); return m ? { v: m[1], cell: m[2] } : null; }; registerVerticalStack(bbnParse, (p, cell) => "rzb_dcs:bl_bunch_" + p.v + "_" + cell, "balloons"); export const BR_FRONT = "rzb_dcs:br_fr_", BR_BACK = "rzb_dcs:br_b_"; export const BR_BACK_OFF = { south: [0, 0, 1], north: [0, 0, -1], east: [1, 0, 0], west: [-1, 0, 0] }; registerPrefixPairRig(BR_FRONT, BR_BACK, BR_BACK_OFF, "bike rack", CAN_REPLACE); export const BO_BOTTOM = "rzb_dcs:bo_b", BO_TOP = "rzb_dcs:bo_t"; export const boCanReplace = (nb) => nb && (fcCanReplace(nb) || nb.typeId === "minecraft:water" || nb.typeId === "minecraft:flowing_water"); const _buoyRecent = new Set(); const markBuoy = (id) => { _buoyRecent.add(id); system.runTimeout(() => _buoyRecent.delete(id), 5); }; onPlace((ev) => { const b = ev.block; if (b.typeId !== BO_BOTTOM) return; if (ev.player) markBuoy(ev.player.id); const ab = safeGetBlock(b.dimension, { x: b.x, y: b.y + 1, z: b.z }); if (!boCanReplace(ab)) { undoPlace(b, ev.player, BO_BOTTOM, "buoy"); return; } try { ab.setPermutation(BlockPermutation.resolve(BO_TOP)); } catch {} }); onBreak((ev) => { const id = ev.brokenBlockPermutation?.type?.id || "", b = ev.block; if (id === BO_BOTTOM) { const nb = safeGetBlock(b.dimension, { x: b.x, y: b.y + 1, z: b.z }); if (nb && nb.typeId === BO_TOP) { try { breakAir(nb); } catch {} } } else if (id === BO_TOP) { const nb = safeGetBlock(b.dimension, { x: b.x, y: b.y - 1, z: b.z }); if (nb && nb.typeId === BO_BOTTOM) { try { breakAir(nb); } catch {} } } }); world.afterEvents.itemUse?.subscribe((ev) => { const p = ev.source; if (ev.itemStack?.typeId !== BO_BOTTOM || !p) return; let hit; try { hit = p.getBlockFromViewDirection({ includeLiquidBlocks: true, includePassableBlocks: true, maxDistance: 8 }); } catch {} if (!hit || !hit.block || !/water/.test(hit.block.typeId)) return; const dim = p.dimension, wx = hit.block.x, wz = hit.block.z; let wy = hit.block.y; for (let i = 0; i < 40; i++) { const up = safeGetBlock(dim, { x: wx, y: wy + 1, z: wz }); if (up && /water/.test(up.typeId)) wy++; else break; } const pid = p.id; system.run(() => { if (_buoyRecent.has(pid)) return; const bb = safeGetBlock(dim, { x: wx, y: wy + 1, z: wz }), tt = safeGetBlock(dim, { x: wx, y: wy + 2, z: wz }); if (!boCanReplace(bb) || !boCanReplace(tt)) return; markBuoy(pid); try { bb.setPermutation(BlockPermutation.resolve(BO_BOTTOM)); tt.setPermutation(BlockPermutation.resolve(BO_TOP)); } catch { return; } try { if (!isCreative(p)) { const inv = p.getComponent("minecraft:inventory")?.container, i = p.selectedSlotIndex, it = inv?.getItem(i); if (it && it.typeId === BO_BOTTOM) { if (it.amount > 1) { it.amount -= 1; inv.setItem(i, it); } else inv.setItem(i, undefined); } } } catch {} }); }); const SNOW_B = "rzb_dcs:wi02_b", SNOW_T = "rzb_dcs:wi02_t"; onPlace((ev) => { const b = ev.block; if (b?.typeId !== SNOW_B) return; const dim = b.dimension, dir = getDir(b.permutation), ab = safeGetBlock(dim, { x: b.x, y: b.y + 1, z: b.z }); if (!ab || !fcCanReplace(ab)) { undoPlace(b, ev.player, SNOW_B, "snowman"); return; } try { ab.setPermutation(BlockPermutation.resolve(SNOW_T, { "minecraft:cardinal_direction": dir })); } catch {} }); onBreak((ev) => { const id = ev.brokenBlockPermutation?.type?.id; if (id !== SNOW_B && id !== SNOW_T) return; const b = ev.block, dim = b.dimension, py = id === SNOW_B ? b.y + 1 : b.y - 1, want = id === SNOW_B ? SNOW_T : SNOW_B; const nb = safeGetBlock(dim, { x: b.x, y: py, z: b.z }); if (nb && nb.typeId === want) { try { breakAir(nb); } catch {} } }); const ORN_C = "rzb_dcs:wi12_c"; const ORN_PARTS = [{ id: "rzb_dcs:wi12_r", off: [-1, 0, -1] }, { id: "rzb_dcs:wi12_p0", off: [-1, 0, 0] }]; const ORN_ANG = { south: 0, north: 180, east: -90, west: 90 }; const ornRot = (o, dir) => { const a = (ORN_ANG[dir] || 0) * Math.PI / 180, c = Math.round(Math.cos(a)), s = Math.round(Math.sin(a)); return [o[0] * c - o[2] * s, o[1] || 0, o[0] * s + o[2] * c]; }; onPlace((ev) => { const b = ev.block; if (b?.typeId !== ORN_C) return; const dim = b.dimension, dir = getDir(b.permutation); const targets = ORN_PARTS.map((p) => { const o = ornRot(p.off, dir); return { id: p.id, blk: safeGetBlock(dim, { x: b.x + o[0], y: b.y + o[1], z: b.z + o[2] }) }; }); if (targets.some((t) => !t.blk || !fcCanReplace(t.blk))) { undoPlace(b, ev.player, ORN_C, "ornament pair"); return; } for (const t of targets) { try { t.blk.setPermutation(BlockPermutation.resolve(t.id, { "minecraft:cardinal_direction": dir })); } catch {} } }); onBreak((ev) => { const perm = ev.brokenBlockPermutation, id = perm?.type?.id; if (!id || (id !== ORN_C && !ORN_PARTS.some((p) => p.id === id))) return; let dir; try { dir = perm.getState("minecraft:cardinal_direction"); } catch {} dir = dir || "south"; const b = ev.block, self = ORN_PARTS.find((p) => p.id === id); let ax = b.x, ay = b.y, az = b.z; if (self) { const o = ornRot(self.off, dir); ax -= o[0]; ay -= o[1]; az -= o[2]; } for (const p of [{ id: ORN_C, off: [0, 0, 0] }, ...ORN_PARTS]) { if (p.id === id) continue; const o = ornRot(p.off, dir); const nb = safeGetBlock(b.dimension, { x: ax + o[0], y: ay + o[1], z: az + o[2] }); if (nb && nb.typeId === p.id) { try { breakAir(nb); } catch {} } } }); // Single balloon upper hitbox: bl_ / bl_heart_ get an invisible wide hitbox one cell up (for the balloon // bulge above the thin string block). Placing the balloon adds it; breaking the balloon removes it; breaking the top // removes + drops the balloon below. Excludes the 2-cell balloons (bl_small_/bl_hearts_/bl_bunch_) and the tops themselves. export const BL_TOP = "rzb_dcs:bl_top", BL_HEART_TOP = "rzb_dcs:bl_heart_top"; export const singleBalloonTop = (id) => { if (!id || !id.startsWith("rzb_dcs:bl_") || id === BL_TOP || id === BL_HEART_TOP) return null; if (/_(small|hearts|bunch)_/.test(id)) return null; return /^rzb_dcs:bl_heart_/.test(id) ? BL_HEART_TOP : BL_TOP; }; onPlace((ev) => { const b = ev.block, top = singleBalloonTop(b.typeId); if (!top) return; const ab = safeGetBlock(b.dimension, { x: b.x, y: b.y + 1, z: b.z }); if (ab && ab.isAir) { try { ab.setPermutation(BlockPermutation.resolve(top)); } catch {} } }); onBreak((ev) => { const id = ev.brokenBlockPermutation?.type?.id || "", b = ev.block; if (singleBalloonTop(id)) { // broke a balloon -> remove the top hitbox above it const ab = safeGetBlock(b.dimension, { x: b.x, y: b.y + 1, z: b.z }); if (ab && (ab.typeId === BL_TOP || ab.typeId === BL_HEART_TOP)) { try { breakAir(ab); } catch {} } } else if (id === BL_TOP || id === BL_HEART_TOP) { // broke the top -> remove + drop the balloon below const bb = safeGetBlock(b.dimension, { x: b.x, y: b.y - 1, z: b.z }); if (bb && singleBalloonTop(bb.typeId)) { const balloon = bb.typeId; try { breakAir(bb); } catch {} if (ev.player && !isCreative(ev.player)) { try { b.dimension.spawnItem(new ItemStack(balloon, 1), { x: b.x + 0.5, y: b.y - 0.5, z: b.z + 0.5 }); } catch {} } } } });