You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

336 lines
25 KiB
JavaScript

// 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_<name>_l anchor / hs_<name>_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_<color> / bl_heart_<color> 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 {} } }
}
});