// features/brush.js the Color Brush recolour import { world, system, BlockPermutation, getDir, safeGetBlock } from "../core/mc.js"; import { VARIANT_SEQ } from "../core/variants.js"; import { particleFor, brushFx, debounced } from "../core/fx.js"; const _brushRecent = new Set(); // per-cell cooldown so holding the brush doesn't rapid-fire swaps import { CT_CELLS, ctParse, ctCellWorld, ctAnchorFrom, CT_CELLS_W, ctParseW, ctCellWorldW, ctAnchorFromW, LT_CELLS, ltParse, ltCellWorld, ltAnchorFrom, LT_CELLS_W, ltParseW, ltCellWorldW, ltAnchorFromW, PCT_ANCHOR, PCT_ANCHOR_W, pctParse, pctParseW } from "./mb_trees.js"; import { UMBRELLA_BAMBOO, UMBRELLA_BEACH, BAMBOO_TABLE, BT_COLORS, KOINO_CHILDREN, LOUNGER, LOUNGER_COLORS } from "./mb_grid.js"; import { koinoAnchorBrushable, koinoAnchorRecolor, spinnerIsSpin, spinnerSpawnAt, spinnerDespawnAt } from "./toggles.js"; import { displayAnchorBrushable, displayAnchorRecolor } from "./display.js"; import { DISPLAY_DIR_YAW } from "../core/grid.js"; const SPINNER_SETS = [ ["rzb_dcs:su01_1", "rzb_dcs:su01_2", "rzb_dcs:su01_3", "rzb_dcs:su01_4", "rzb_dcs:su01_5", "rzb_dcs:su01_6", "rzb_dcs:su01_7", "rzb_dcs:su01_rainbow"], ["rzb_dcs:sp35_robin", "rzb_dcs:sp35_blue_j"], ]; const SPINNER_NEXT = {}; for (const s of SPINNER_SETS) for (let i = 0; i < s.length; i++) SPINNER_NEXT[s[i]] = s[(i + 1) % s.length]; import { WEB4_L, WEB4_R, WEB4_VARIANTS, WEB4_RIGHT_OFF, FAIRY_L, FAIRY_R, FAIRY_THEMES, FAIRY_RIGHT_OFF, SWING_BASE, SWING_PIECES, SWING_VARIANTS, swingParse, swingAnchor, swingCells, ROPE_BASE, ROPE_VARIANTS, ropeParse, ropeBottom, TW_FRONT, TW_BACK, TW_COLORS, TW_BACK_OFF, LO_FRONT, LO_BACK, LO_COLORS, LO_BACK_OFF, LC_BASE, LC_AWNING, LC_COLORS, SC_VARIANTS, scParse, bsParse, BS_CYCLES, bhParse, BH_CYCLES } from "./mb_pairs.js"; import { SB_BASE, SB_TOP, SB_LB, SB_LF, SB_ORDER, SB_LAY_CELLS, SB_LAY_PREFIX, sbLayParse, sbLayCellWorld, sbLayAnchorFrom, BB_B, BB_FR, BB_ORDER, BB_CELLS, BB_PREFIX, bbParse, bbCellWorld, bbAnchorFrom, SLED_B, SLED_FR, SLED_ORDER, SLED_CELLS, SLED_PREFIX, sledParse, sledCellWorld, sledAnchorFrom } from "./rideables.js"; const umParse = UMBRELLA_BAMBOO.parse, buParse = UMBRELLA_BEACH.parse, btParse = BAMBOO_TABLE.parse; const UM_CELLS = UMBRELLA_BAMBOO.cells, BU_CELLS = UMBRELLA_BEACH.cells, BT_CELLS = BAMBOO_TABLE.cells; const UM_BASE = UMBRELLA_BAMBOO.prefix, BU_BASE = UMBRELLA_BEACH.prefix, BT_BASE = BAMBOO_TABLE.prefix; const KOINO_C = ["rzb_dcs:sp15_", "rzb_dcs:sp15_2_"]; function koinoParse(id) { if (!id) return null; if (id.startsWith("rzb_dcs:sp15_2_")) { const cell = id.slice(15); return (cell === "pole" || cell === "fish") ? { idx: 1, cell } : null; } if (id.startsWith("rzb_dcs:sp15_")) { const cell = id.slice(13); return (cell === "pole" || cell === "fish") ? { idx: 0, cell } : null; } return null; } // Grid tree recolor (christmas / light): swap the WHOLE tree between the rainbow and white block sets. They're independent // ids differing only by a "_w" suffix (geometry/collision identical), so we just re-resolve every cell to the other id. function convertGridTree(block, player, v) { // v: {CELLS, parse, cellWorld, anchorFrom} of the picked variant const cell = v.parse(block.typeId); if (!cell) return; const dir = getDir(block.permutation), a = v.anchorFrom(cell, block.x, block.y, block.z, dir), dim2 = block.dimension, loc = block.location; const toWhite = !cell.endsWith("_w"), pcol = toWhite ? "white" : "yellow"; system.runTimeout(() => { for (const c of Object.keys(v.CELLS)) { const p = v.cellWorld(c, a.x, a.y, a.z, dir), nb = safeGetBlock(dim2, p); if (nb && v.parse(nb.typeId) === c) { const other = toWhite ? c + "_w" : c.slice(0, -2); try { nb.setPermutation(BlockPermutation.resolve("rzb_dcs:" + other, { "minecraft:cardinal_direction": dir })); } catch {} } } brushFx(dim2, loc, pcol, player); }, 1); } // Grid-vehicle cell recolor (surfboard-lay / basket bike / sled): cycle ORDER, repaint every cell keeping facing. function gridCellTargets(block, dir, parse, ORDER, CELLS, PREFIX, cellWorld, anchorFrom) { const pk = parse(block.typeId); if (!pk) return null; const i = ORDER.indexOf(pk.color); if (i < 0) return null; const nextC = ORDER[(i + 1) % ORDER.length]; const a = anchorFrom(pk.cell, block.x, block.y, block.z, dir ?? "south"); return { targets: Object.keys(CELLS).map((c) => ({ pos: cellWorld(c, a.x, a.y, a.z, dir ?? "south"), id: PREFIX[c] + nextC, partner: c !== pk.cell })), colorSrc: nextC }; } function cycleVariant(block, player) { if (debounced(_brushRecent, block.x + "," + block.y + "," + block.z, 6)) return true; // ~0.3s per-cell cooldown: click swaps once, hold doesn't spam if (ctParse(block.typeId)) { convertGridTree(block, player, { CELLS: CT_CELLS, parse: ctParse, cellWorld: ctCellWorld, anchorFrom: ctAnchorFrom }); return true; } if (ctParseW(block.typeId)) { convertGridTree(block, player, { CELLS: CT_CELLS_W, parse: ctParseW, cellWorld: ctCellWorldW, anchorFrom: ctAnchorFromW }); return true; } if (ltParse(block.typeId)) { convertGridTree(block, player, { CELLS: LT_CELLS, parse: ltParse, cellWorld: ltCellWorld, anchorFrom: ltAnchorFrom }); return true; } if (ltParseW(block.typeId)) { convertGridTree(block, player, { CELLS: LT_CELLS_W, parse: ltParseW, cellWorld: ltCellWorldW, anchorFrom: ltAnchorFromW }); return true; } if (pctParse(block.typeId) || pctParseW(block.typeId)) { const white = !!pctParseW(block.typeId), anchorId = white ? PCT_ANCHOR_W : PCT_ANCHOR, parse = white ? pctParseW : pctParse; const ay = block.typeId === anchorId ? block.y : block.y - 1, dim2 = block.dimension, loc = block.location, toWhite = !white, pcol = toWhite ? "white" : "yellow"; let dir = "south"; try { dir = block.permutation.getState("minecraft:cardinal_direction") ?? "south"; } catch {} system.runTimeout(() => { for (const dy of [0, 1]) { const nb = safeGetBlock(dim2, { x: block.x, y: ay + dy, z: block.z }); if (nb && parse(nb.typeId)) { const other = toWhite ? nb.typeId + "_w" : nb.typeId.slice(0, -2); try { nb.setPermutation(BlockPermutation.resolve(other, { "minecraft:cardinal_direction": dir })); } catch {} } } brushFx(dim2, loc, pcol, player); }, 1); return true; } if (koinoAnchorBrushable(block.typeId)) { const b = block, dim2 = block.dimension, loc = block.location; system.runTimeout(() => { koinoAnchorRecolor(b); brushFx(dim2, loc, "pink", player); }, 1); return true; } if (displayAnchorBrushable(block.typeId)) { const b = block, dim2 = block.dimension, loc = block.location; system.runTimeout(() => { const col = displayAnchorRecolor(b); brushFx(dim2, loc, col || "white", player); }, 1); return true; } if (SPINNER_NEXT[block.typeId]) { // pinwheel / spinner bird: cycle colour in place, keep spin state + facing, respawn entity if spinning const cur = block.typeId, next = SPINNER_NEXT[cur], dim2 = block.dimension, loc = block.location, x = block.x, y = block.y, z = block.z; let sdir = "south"; try { sdir = block.permutation.getState("minecraft:cardinal_direction") ?? "south"; } catch {} const spinning = spinnerIsSpin(block.permutation); system.runTimeout(() => { const b = safeGetBlock(dim2, { x, y, z }); if (b) { try { b.setPermutation(BlockPermutation.resolve(next, { "minecraft:cardinal_direction": sdir, "rzb_dcs:spin": spinning })); } catch {} } if (spinning) { spinnerDespawnAt(dim2, cur, x, y, z); spinnerSpawnAt(dim2, next, x, y, z, DISPLAY_DIR_YAW[sdir] ?? 0); } brushFx(dim2, loc, particleFor(next), player); }, 1); return true; } const kp = koinoParse(block.typeId); // koinobori children: brushing a cell swaps ALL cells to the next colour variant if (kp) { const kdir = getDir(block.permutation), a = KOINO_CHILDREN.anchorFrom(kp.cell, block.x, block.y, block.z, kdir), dim2 = block.dimension, loc = block.location; const nextPrefix = KOINO_C[(kp.idx + 1) % KOINO_C.length]; system.runTimeout(() => { for (const c of Object.keys(KOINO_CHILDREN.cells)) { const p = KOINO_CHILDREN.cellWorld(c, a.x, a.y, a.z, kdir), nb = safeGetBlock(dim2, p); if (nb && koinoParse(nb.typeId)?.cell === c) { try { nb.setPermutation(BlockPermutation.resolve(nextPrefix + c, { "minecraft:cardinal_direction": kdir })); } catch {} } } brushFx(dim2, loc, "pink", player); }, 1); return true; } let dir; try { dir = block.permutation.getState("minecraft:cardinal_direction"); } catch {} const loc = block.location, dim = block.dimension; let targets, colorSrc; const isL = block.typeId.startsWith(FAIRY_L), isR = block.typeId.startsWith(FAIRY_R); if (isL || isR) { const theme = block.typeId.slice((isL ? FAIRY_L : FAIRY_R).length); const i = FAIRY_THEMES.indexOf(theme); if (i < 0) return false; const next = FAIRY_THEMES[(i + 1) % FAIRY_THEMES.length]; const off = FAIRY_RIGHT_OFF[dir ?? "south"] || [1, 0, 0]; const lp = isL ? loc : { x: block.x - off[0], y: block.y - off[1], z: block.z - off[2] }; const rp = isL ? { x: block.x + off[0], y: block.y + off[1], z: block.z + off[2] } : loc; targets = [{ pos: lp, id: FAIRY_L + next, partner: isR }, { pos: rp, id: FAIRY_R + next, partner: isL }]; colorSrc = FAIRY_L + next; } else if (block.typeId.startsWith(WEB4_L) || block.typeId.startsWith(WEB4_R)) { const isWL = block.typeId.startsWith(WEB4_L), isWR = block.typeId.startsWith(WEB4_R); const variant = block.typeId.slice((isWL ? WEB4_L : WEB4_R).length); const i = WEB4_VARIANTS.indexOf(variant); if (i < 0) return false; const next = WEB4_VARIANTS[(i + 1) % WEB4_VARIANTS.length]; const off = WEB4_RIGHT_OFF[dir ?? "south"] || [1, 0, 0]; const lp = isWL ? loc : { x: block.x - off[0], y: block.y - off[1], z: block.z - off[2] }; const rp = isWL ? { x: block.x + off[0], y: block.y + off[1], z: block.z + off[2] } : loc; targets = [{ pos: lp, id: WEB4_L + next, partner: isWR }, { pos: rp, id: WEB4_R + next, partner: isWL }]; colorSrc = WEB4_L + next; } else if (swingParse(block.typeId)) { const sw = swingParse(block.typeId); const i = SWING_VARIANTS.indexOf(sw.variant); if (i < 0) return false; const nextV = SWING_VARIANTS[(i + 1) % SWING_VARIANTS.length]; const a = swingAnchor(sw.piece, block.x, block.y, block.z, dir ?? "south"); const cells = swingCells(a.x, a.y, a.z, dir ?? "south"); targets = SWING_PIECES.map((p) => ({ pos: cells[p], id: SWING_BASE + p + "_" + nextV, partner: p !== sw.piece })); colorSrc = SWING_BASE + sw.piece + "_" + nextV; } else if (ropeParse(block.typeId)) { const rp = ropeParse(block.typeId); const i = ROPE_VARIANTS.indexOf(rp.variant); if (i < 0) return false; const nextV = ROPE_VARIANTS[(i + 1) % ROPE_VARIANTS.length]; const bot = ropeBottom(rp.piece, block.x, block.y, block.z); targets = [ { pos: bot, id: ROPE_BASE + "b_" + nextV, partner: rp.piece !== "b" }, { pos: { x: bot.x, y: bot.y + 1, z: bot.z }, id: ROPE_BASE + "t_" + nextV, partner: rp.piece !== "t" } ]; colorSrc = "pink"; } else if (umParse(block.typeId) || buParse(block.typeId)) { const bamboo = !!umParse(block.typeId); const CELLS = bamboo ? UM_CELLS : BU_CELLS, BASE = bamboo ? UM_BASE : BU_BASE; const cols = bamboo ? ["yellow", "green"] : ["black", "white", "orange", "magenta", "light_blue", "yellow", "lime", "pink", "gray", "light_gray", "cyan", "purple", "blue", "green", "red", "rainbow"]; // DecoCraft canonical order (rainbow appended) const up = bamboo ? umParse(block.typeId) : buParse(block.typeId); const i = cols.indexOf(up.color); if (i < 0) return false; const nextC = cols[(i + 1) % cols.length]; const o0 = CELLS[up.cell], ax = block.x - o0[0], ay = block.y - o0[1], az = block.z - o0[2]; targets = Object.keys(CELLS).map((cell) => { const oo = CELLS[cell]; return { pos: { x: ax + oo[0], y: ay + oo[1], z: az + oo[2] }, id: BASE + cell + "_" + nextC, partner: cell !== up.cell }; }); colorSrc = nextC; } else if (block.typeId.startsWith(TW_FRONT) || block.typeId.startsWith(TW_BACK)) { const isF = block.typeId.startsWith(TW_FRONT); const color = block.typeId.slice((isF ? TW_FRONT : TW_BACK).length); const i = TW_COLORS.indexOf(color); if (i < 0) return false; const nextC = TW_COLORS[(i + 1) % TW_COLORS.length]; const off = TW_BACK_OFF[dir ?? "south"] || [0, 0, -1]; const fp = isF ? loc : { x: block.x - off[0], y: block.y - off[1], z: block.z - off[2] }; const bp = isF ? { x: block.x + off[0], y: block.y + off[1], z: block.z + off[2] } : loc; targets = [{ pos: fp, id: TW_FRONT + nextC, partner: !isF }, { pos: bp, id: TW_BACK + nextC, partner: isF }]; colorSrc = nextC; } else if (block.typeId.startsWith(LO_FRONT) || block.typeId.startsWith(LO_BACK)) { const isF = block.typeId.startsWith(LO_FRONT); const color = block.typeId.slice((isF ? LO_FRONT : LO_BACK).length); const i = LO_COLORS.indexOf(color); if (i < 0) return false; const nextC = LO_COLORS[(i + 1) % LO_COLORS.length]; const off = LO_BACK_OFF[dir ?? "south"] || [0, 0, -1]; const fp = isF ? loc : { x: block.x - off[0], y: block.y - off[1], z: block.z - off[2] }; const bp = isF ? { x: block.x + off[0], y: block.y + off[1], z: block.z + off[2] } : loc; targets = [{ pos: fp, id: LO_FRONT + nextC, partner: !isF }, { pos: bp, id: LO_BACK + nextC, partner: isF }]; colorSrc = nextC; } else if (btParse(block.typeId)) { const p = btParse(block.typeId); const i = BT_COLORS.indexOf(p.color); if (i < 0) return false; const nextC = BT_COLORS[(i + 1) % BT_COLORS.length]; const o0 = BT_CELLS[p.cell], ax = block.x - o0[0], ay = block.y - o0[1], az = block.z - o0[2]; targets = Object.keys(BT_CELLS).map((cell) => { const oo = BT_CELLS[cell]; return { pos: { x: ax + oo[0], y: ay + oo[1], z: az + oo[2] }, id: BT_BASE + cell + "_" + nextC, partner: cell !== p.cell }; }); colorSrc = nextC; } else if (LOUNGER.parse(block.typeId)) { // pool lounger: cycle the whole 3-cell lounger to the next colour (rotating grid) const p = LOUNGER.parse(block.typeId); const i = LOUNGER_COLORS.indexOf(p.color); if (i < 0) return false; const nextC = LOUNGER_COLORS[(i + 1) % LOUNGER_COLORS.length]; const a = LOUNGER.anchorFrom(p.cell, block.x, block.y, block.z, dir ?? "south"); targets = Object.keys(LOUNGER.cells).map((cell) => ({ pos: LOUNGER.cellWorld(cell, a.x, a.y, a.z, dir ?? "south"), id: LOUNGER.idFor(cell, nextC), partner: cell !== p.cell })); colorSrc = nextC; } else if (scParse(block.typeId)) { // scarecrow: cycle the whole DESIGN (fa36_1/2/3) across both vertical cells const sp = scParse(block.typeId); const i = SC_VARIANTS.indexOf(sp.n); if (i < 0) return false; const nextN = SC_VARIANTS[(i + 1) % SC_VARIANTS.length]; const bp = sp.cell === "b" ? loc : { x: block.x, y: block.y - 1, z: block.z }; // bottom anchor pos const tp = { x: bp.x, y: bp.y + 1, z: bp.z }; targets = [{ pos: bp, id: "rzb_dcs:fa36_" + nextN + "_b", partner: sp.cell !== "b" }, { pos: tp, id: "rzb_dcs:fa36_" + nextN + "_t", partner: sp.cell !== "t" }]; colorSrc = "white"; } else if (bsParse(block.typeId)) { // balloons_small: cycle the bunch DESIGN across both vertical cells (within its regular/pastel group) const sp = bsParse(block.typeId), cyc = BS_CYCLES[sp.v]; if (!cyc) return false; // halloween/valentines aren't cycled const nextV = cyc[(cyc.indexOf(sp.v) + 1) % cyc.length]; const bp = sp.cell === "b" ? loc : { x: block.x, y: block.y - 1, z: block.z }; const tp = { x: bp.x, y: bp.y + 1, z: bp.z }; targets = [{ pos: bp, id: "rzb_dcs:bl_small_" + nextV + "_b", partner: sp.cell !== "b" }, { pos: tp, id: "rzb_dcs:bl_small_" + nextV + "_t", partner: sp.cell !== "t" }]; colorSrc = "white"; } else if (bhParse(block.typeId)) { // heart pairs: cycle combo across both cells (regular combos + pastel combos) const sp = bhParse(block.typeId), cyc = BH_CYCLES[sp.v]; if (!cyc) return false; const nextV = cyc[(cyc.indexOf(sp.v) + 1) % cyc.length]; const bp = sp.cell === "b" ? loc : { x: block.x, y: block.y - 1, z: block.z }; const tp = { x: bp.x, y: bp.y + 1, z: bp.z }; targets = [{ pos: bp, id: "rzb_dcs:bl_hearts_" + nextV + "_b", partner: sp.cell !== "b" }, { pos: tp, id: "rzb_dcs:bl_hearts_" + nextV + "_t", partner: sp.cell !== "t" }]; colorSrc = "white"; } else if (block.typeId.startsWith(LC_BASE) || block.typeId.startsWith(LC_AWNING)) { const isB = block.typeId.startsWith(LC_BASE); const color = block.typeId.slice((isB ? LC_BASE : LC_AWNING).length); const i = LC_COLORS.indexOf(color); if (i < 0) return false; const nextC = LC_COLORS[(i + 1) % LC_COLORS.length]; const bp = isB ? loc : { x: block.x, y: block.y - 1, z: block.z }; const aw = isB ? { x: block.x, y: block.y + 1, z: block.z } : loc; targets = [{ pos: bp, id: LC_BASE + nextC, partner: !isB }, { pos: aw, id: LC_AWNING + nextC, partner: isB }]; colorSrc = nextC; } else if (block.typeId.startsWith(SB_BASE) || block.typeId.startsWith(SB_TOP)) { const isB = block.typeId.startsWith(SB_BASE); const color = block.typeId.slice((isB ? SB_BASE : SB_TOP).length); const i = SB_ORDER.indexOf(color); if (i < 0) return false; const nextC = SB_ORDER[(i + 1) % SB_ORDER.length]; const bp = isB ? loc : { x: block.x, y: block.y - 1, z: block.z }; const tp = isB ? { x: block.x, y: block.y + 1, z: block.z } : loc; targets = [{ pos: bp, id: SB_BASE + nextC, partner: !isB }, { pos: tp, id: SB_TOP + nextC, partner: isB }]; colorSrc = nextC; } else if (block.typeId.startsWith(SB_LB) || block.typeId.startsWith(SB_LF)) { const r = gridCellTargets(block, dir, sbLayParse, SB_ORDER, SB_LAY_CELLS, SB_LAY_PREFIX, sbLayCellWorld, sbLayAnchorFrom); if (!r) return false; ({ targets, colorSrc } = r); } else if (block.typeId.startsWith(BB_B) || block.typeId.startsWith(BB_FR)) { const r = gridCellTargets(block, dir, bbParse, BB_ORDER, BB_CELLS, BB_PREFIX, bbCellWorld, bbAnchorFrom); if (!r) return false; ({ targets, colorSrc } = r); } else if (block.typeId.startsWith(SLED_B) || block.typeId.startsWith(SLED_FR)) { const r = gridCellTargets(block, dir, sledParse, SLED_ORDER, SLED_CELLS, SLED_PREFIX, sledCellWorld, sledAnchorFrom); if (!r) return false; ({ targets, colorSrc } = r); } else { const seq = VARIANT_SEQ[block.typeId]; if (!seq) return false; const next = seq[(seq.indexOf(block.typeId) + 1) % seq.length]; targets = [{ pos: loc, id: next }]; colorSrc = next; } const pcol = particleFor(colorSrc); system.runTimeout(() => { for (const t of targets) { const b = dim.getBlock(t.pos); if (!b) continue; if (t.partner && !(b.typeId.startsWith(WEB4_L) || b.typeId.startsWith(WEB4_R) || b.typeId.startsWith(FAIRY_L) || b.typeId.startsWith(FAIRY_R) || b.typeId.startsWith(SWING_BASE) || b.typeId.startsWith(ROPE_BASE) || b.typeId.startsWith(UM_BASE) || b.typeId.startsWith(BU_BASE) || b.typeId.startsWith("rzb_dcs:tw_") || b.typeId.startsWith("rzb_dcs:lo_") || b.typeId.startsWith(BT_BASE) || b.typeId.startsWith("rzb_dcs:su03_") || b.typeId.startsWith("rzb_dcs:lc_") || b.typeId.startsWith("rzb_dcs:fa36_") || b.typeId.startsWith("rzb_dcs:bl_small_") || b.typeId.startsWith("rzb_dcs:bl_hearts_") || b.typeId.startsWith(SB_BASE) || b.typeId.startsWith(SB_TOP) || b.typeId.startsWith(SB_LB) || b.typeId.startsWith(SB_LF) || b.typeId.startsWith(BB_B) || b.typeId.startsWith(BB_FR) || b.typeId.startsWith(SLED_B) || b.typeId.startsWith(SLED_FR))) continue; try { b.setPermutation(BlockPermutation.resolve(t.id, dir !== undefined ? { "minecraft:cardinal_direction": dir } : {})); } catch { try { b.setType(t.id); } catch {} } } brushFx(dim, loc, pcol, player); }, 1); return true; } const brushable = (id) => VARIANT_SEQ[id] || umParse(id) || buParse(id) || btParse(id) || LOUNGER.parse(id) || ctParse(id) || ctParseW(id) || ltParse(id) || ltParseW(id) || pctParse(id) || pctParseW(id) || koinoParse(id) || koinoAnchorBrushable(id) || displayAnchorBrushable(id) || SPINNER_NEXT[id] || scParse(id) || bsParse(id) || bhParse(id) || id.startsWith("rzb_dcs:tw_") || id.startsWith("rzb_dcs:lo_") || id.startsWith("rzb_dcs:lc_"); world.beforeEvents.playerBreakBlock?.subscribe((ev) => { if (ev.itemStack?.typeId !== "rzb_dcs:brush_color") return; if (brushable(ev.block.typeId)) { ev.cancel = true; cycleVariant(ev.block, ev.player); } }); world.beforeEvents.playerInteractWithBlock?.subscribe((ev) => { if (ev.itemStack?.typeId !== "rzb_dcs:brush_color") return; if (brushable(ev.block.typeId)) { ev.cancel = true; cycleVariant(ev.block, ev.player); } });