initial commit

This commit is contained in:
MomokoKoigakubo
2026-08-07 17:00:46 -04:00
commit 4a48f0bae9
5578 changed files with 138786 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
// features/bounce.js; springy blocks that launch a player who lands on them.
// Trampoline builds energy while you keep jumping (higher the longer you pump / hold jump) and decays it when you don't.
import { world, system, safeGetBlock, InputButton, ButtonState } from "../core/mc.js";
const BOUNCE_BLOCKS = new Set(["rzb_dcs:dv_mid", "rzb_dcs:dv_end"]); // diving board planks (front/stairs is a stepup, not springy)
const TRAMP_BLOCKS = new Set(["rzb_dcs:tr_fl", "rzb_dcs:tr_fr", "rzb_dcs:tr_bl", "rzb_dcs:tr_br"]); // trampoline mat cells (full-cube: stood ON TOP)
const FALL_MIN = 0.12; // ignore near-zero impact (walking) so standing never bounces
const SURFACE_MAX = 0.72; // act only when feet are AT the plank surface, not the band above (avoids mid-air re-bounces)
const REST = 0.96, MAX = 1.5; // diving board: strength = min(impact*REST, MAX)
// trampoline energy model (vertical launch speed, persists across bounces)
const TRAMP_MAX = 1.7; // cap on launch
const PUMP = 0.3; // added per bounce while jumping; also the floor of an active pump
const DAMP = 0.82; // passive bounces shrink by this each time, so you settle when you stop
const SEED = 0.85; // fresh landing starts from this fraction of your fall speed...
const SEED_MIN = 0.62; // ...but never less than this, so the first bounce always pops
const SETTLE = 0.15; // below this a decaying bounce stops, so you can stand
const JUMP_GRACE = 8; // treat jump as "held" if pressed within this many ticks of contact
const _last = new Map(); // player.id -> tick of last bounce (debounce)
const _lastVy = new Map(); // player.id -> previous tick's vy (pre-collision impact; contact-tick vy is clamped to ~0)
const _energy = new Map(); // player.id -> current trampoline energy (absent = not in a bounce cycle)
const _jumpTick = new Map();// player.id -> last tick the jump button was down
const jumpDown = (p) => { try { return p.inputInfo.getButtonState(InputButton.Jump) === ButtonState.Pressed; } catch { return false; } };
system.runInterval(() => {
const t = system.currentTick;
for (const p of world.getAllPlayers()) {
if (jumpDown(p)) _jumpTick.set(p.id, t);
let v; try { v = p.getVelocity(); } catch { continue; }
const vy = v ? v.y : 0, prevVy = _lastVy.get(p.id) ?? 0;
_lastVy.set(p.id, vy);
const l = p.location, by = Math.floor(l.y), x = Math.floor(l.x), z = Math.floor(l.z);
const feet = safeGetBlock(p.dimension, { x, y: by, z });
let tramp = false, ok = false;
if (feet && BOUNCE_BLOCKS.has(feet.typeId)) { ok = (l.y - by) <= SURFACE_MAX; } // plank: feet AT surface
else { const below = safeGetBlock(p.dimension, { x, y: by - 1, z }); if (below && TRAMP_BLOCKS.has(below.typeId)) { tramp = true; ok = true; } }
let grounded = false; try { grounded = p.isOnGround; } catch {}
if (!ok) { if (grounded) _energy.delete(p.id); continue; } // landed off the trampoline -> reset its energy
if (grounded) { try { p.addEffect("jump_boost", 8, { amplifier: tramp ? 1 : 0, showParticles: false }); } catch {} }
const impact = Math.max(-vy, -prevVy); // more-negative of this/last tick (contact vy is clamped by collision)
if (impact < FALL_MIN) continue;
if (t - (_last.get(p.id) ?? -99) < 4) continue; // one bounce per landing
_last.set(p.id, t);
let strength;
if (tramp) {
const held = t - (_jumpTick.get(p.id) ?? -99) <= JUMP_GRACE;
let e = _energy.get(p.id);
if (e === undefined) e = Math.min(Math.max(impact * SEED, SEED_MIN), TRAMP_MAX); // fresh landing: seed from the fall, floored
else if (held) e = Math.min(Math.max(e, 0) + PUMP, TRAMP_MAX); // pumping/holding jump: build up
else e = e * DAMP; // passive: shrink and settle
if (held) e = Math.max(e, PUMP); // a jumped bounce always pops
if (e < SETTLE) { _energy.delete(p.id); continue; } // fully settled: let them stand
_energy.set(p.id, e);
strength = e;
} else {
strength = Math.min(impact * REST, MAX);
}
try { p.applyKnockback({ x: 0, z: 0 }, strength); } catch { try { p.applyKnockback(0, 0, 0, strength); } catch {} } // 2.0.0 vs older signature
}
}, 1);
+12
View File
@@ -0,0 +1,12 @@
// features/break_particle.js — custom block break particle
import { system } from "../core/mc.js";
system.beforeEvents.startup.subscribe(({ blockComponentRegistry }) => {
blockComponentRegistry.registerCustomComponent("rzb_dcs:breakblock_particle", {
onPlayerBreak(event) {
const { block } = event;
try { block.dimension.runCommand(`particle rzb_dcs:breakdeco ${block.location.x} ${block.location.y + 0.5} ${block.location.z}`); } catch {}
},
});
});
+254
View File
@@ -0,0 +1,254 @@
// 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); }
});
+98
View File
@@ -0,0 +1,98 @@
// features/containers.js (presents).
import { world, system, isCreative, BlockPermutation } from "../core/mc.js";
import { onPlace, onBreak } from "../core/events.js";
import { brushFx, particleFor, breakFx } from "../core/fx.js";
const FAMILY = "rzb_dcs_present_inv";
const OFF_Y = 0.1;
const BRUSH = "rzb_dcs:brush_color";
const heldItem = (e) => { try { return e.getComponent("minecraft:inventory")?.container?.getItem(e.selectedSlotIndex)?.typeId; } catch { return undefined; } };
const PRESENT_SEQ = {};
const _reg = (ids) => { for (const id of ids) PRESENT_SEQ[id] = ids; };
const RAINBOW = ["white", "orange", "magenta", "light_blue", "yellow", "lime", "pink", "cyan", "purple", "blue", "green", "red"]; // DecoCraft canonical order
for (const t of ["wi30", "wi31", "wi32", "wi33"]) _reg(RAINBOW.map((c) => "rzb_dcs:" + t + "_" + c)); // 12-colour presents
_reg(["wi34_1", "wi34_2", "wi34_3", "wi34_4", "wi34_5"].map((x) => "rzb_dcs:" + x)); // present trio (designs)
_reg(["wi35_6", "wi35_7", "wi35_8", "wi35_9", "wi35_10", "wi35_11"].map((x) => "rzb_dcs:" + x)); // present trio (designs)
function recolorPresent(dim, bx, by, bz, player) {
let blk = null; try { blk = dim.getBlock({ x: bx, y: by, z: bz }); } catch {}
const seq = blk && PRESENT_SEQ[blk.typeId]; if (!seq) return false;
const next = seq[(seq.indexOf(blk.typeId) + 1) % seq.length];
let dir; try { dir = blk.permutation.getState("minecraft:cardinal_direction"); } catch {}
try { blk.setPermutation(BlockPermutation.resolve(next, dir !== undefined ? { "minecraft:cardinal_direction": dir } : {})); }
catch { try { blk.setType(next); } catch {} }
brushFx(dim, { x: bx, y: by, z: bz }, particleFor(next), player);
return true;
}
const PRESENTS = {
"rzb_dcs:wi30_": "rzb_dcs:present_inv", // Present
"rzb_dcs:wi31_": "rzb_dcs:present_inv", // Present Long
"rzb_dcs:wi32_": "rzb_dcs:present_inv", // Present Round
"rzb_dcs:wi33_": "rzb_dcs:present_inv", // Present Small
"rzb_dcs:wi34_": "rzb_dcs:present_inv", // Present Trio
"rzb_dcs:wi35_": "rzb_dcs:present_inv" // Present Trio
};
function presentEntity(typeId) { if (!typeId) return null; for (const p in PRESENTS) if (typeId.startsWith(p)) return PRESENTS[p]; return null; }
const invEntsAt = (dim, at, max = 0.5) => { try { return dim.getEntities({ families: [FAMILY], location: at, maxDistance: max }); } catch { return []; } };
function dropContents(dim, ent, at) {
try {
const c = ent.getComponent("minecraft:inventory")?.container; if (!c) return;
for (let i = 0; i < c.size; i++) { const it = c.getItem(i); if (it) { try { dim.spawnItem(it, at); } catch {} } }
} catch {}
}
// Place a present -> spawn its container entity, once.
onPlace((ev) => {
const b = ev.block; if (!b) return;
const entId = presentEntity(b.typeId); if (!entId) return;
const at = { x: b.x + 0.5, y: b.y + OFF_Y, z: b.z + 0.5 };
if (invEntsAt(b.dimension, at, 0.4).length) return; // don't double up
try { b.dimension.spawnEntity(entId, at); } catch {}
});
// Break a present > drop the container's contents + despawn it.
onBreak((ev) => {
const id = ev.brokenBlockPermutation?.type?.id; if (!presentEntity(id)) return;
const b = ev.block, at = { x: b.x + 0.5, y: b.y + OFF_Y, z: b.z + 0.5 };
for (const e of invEntsAt(b.dimension, at)) { dropContents(b.dimension, e, at); try { e.remove(); } catch {} }
});
world.afterEvents.entityHitEntity?.subscribe((ev) => {
const t = ev.hitEntity; if (!t || !t.typeId.startsWith("rzb_dcs:present_inv")) return;
const dim = t.dimension, l = t.location, bx = Math.floor(l.x), by = Math.floor(l.y), bz = Math.floor(l.z);
if (heldItem(ev.damagingEntity) === BRUSH) { recolorPresent(dim, bx, by, bz, ev.damagingEntity); return; }
const at = { x: bx + 0.5, y: by + 0.3, z: bz + 0.5 };
dropContents(dim, t, at);
let blk = null; try { blk = dim.getBlock({ x: bx, y: by, z: bz }); } catch {}
if (blk && presentEntity(blk.typeId)) {
if (!isCreative(ev.damagingEntity)) { try { const is = blk.getItemStack(1, false); if (is) dim.spawnItem(is, at); } catch {} }
try { blk.setType("minecraft:air"); } catch {}
}
try { t.remove(); } catch {}
breakFx(dim, bx + 0.5, by + 0.5, bz + 0.5); // entity attack-to-pickup: block is script-removed (no vanilla particle) -> puff breakdeco
});
world.beforeEvents.playerInteractWithEntity?.subscribe((ev) => {
const t = ev.target; if (!t || !t.typeId.startsWith("rzb_dcs:present_inv")) return;
if (ev.itemStack?.typeId !== BRUSH) return;
ev.cancel = true;
const dim = t.dimension, l = t.location, bx = Math.floor(l.x), by = Math.floor(l.y), bz = Math.floor(l.z);
system.run(() => recolorPresent(dim, bx, by, bz, ev.player));
});
system.runInterval(() => {
for (const name of ["overworld", "nether", "the_end"]) {
let dim; try { dim = world.getDimension(name); } catch { continue; }
let ents; try { ents = dim.getEntities({ families: [FAMILY] }); } catch { continue; }
for (const e of ents) {
const l = e.location; let blk = null;
try { blk = dim.getBlock({ x: Math.floor(l.x), y: Math.floor(l.y), z: Math.floor(l.z) }); } catch {}
if (blk && !presentEntity(blk.typeId)) { dropContents(dim, e, l); try { e.remove(); } catch {} }
}
}
}, 40);
+79
View File
@@ -0,0 +1,79 @@
// features/decor.js — standalone brush recolorable decoration blocks.
import { registerVariants } from "../core/variants.js";
registerVariants(["rzb_dcs:sp22_white", "rzb_dcs:sp22_yellow", "rzb_dcs:sp22_pink", "rzb_dcs:sp22_purple", "rzb_dcs:sp22_red", "rzb_dcs:sp22_salmon"]); // rose vase
registerVariants(["rzb_dcs:sp25_pink", "rzb_dcs:sp25_red"]); // valentine card
registerVariants(["rzb_dcs:sp21_white", "rzb_dcs:sp21_yellow", "rzb_dcs:sp21_pink", "rzb_dcs:sp21_red"]); // potted cherry branch
registerVariants(["rzb_dcs:sp12_silver", "rzb_dcs:sp12_gold", "rzb_dcs:sp12_rosegold"]); // engagement ring (metals — natural order)
registerVariants(["rzb_dcs:sp11_yellow", "rzb_dcs:sp11_pink", "rzb_dcs:sp11_white"]); // easter basket
registerVariants(["rzb_dcs:sp26_tulips", "rzb_dcs:sp26_flowers1", "rzb_dcs:sp26_flowers2"]); // watering can (flower types — natural order)
registerVariants(["rzb_dcs:sp27_plain", "rzb_dcs:sp27_spring"]); // welcome mat (styles — natural order)
registerVariants(["rzb_dcs:sp37_white", "rzb_dcs:sp37_red"]); // spider lily bouquet
registerVariants(["rzb_dcs:sp39_yellow", "rzb_dcs:sp39_pink", "rzb_dcs:sp39_purple", "rzb_dcs:sp39_blue", "rzb_dcs:sp39_green"]); // spring armchair (canonical cycle order)
registerVariants(["rzb_dcs:sp40_yellow", "rzb_dcs:sp40_pink", "rzb_dcs:sp40_purple", "rzb_dcs:sp40_blue", "rzb_dcs:sp40_green"]); // spring couch left
registerVariants(["rzb_dcs:sp41_yellow", "rzb_dcs:sp41_pink", "rzb_dcs:sp41_purple", "rzb_dcs:sp41_blue", "rzb_dcs:sp41_green"]); // spring couch middle
registerVariants(["rzb_dcs:sp42_yellow", "rzb_dcs:sp42_pink", "rzb_dcs:sp42_purple", "rzb_dcs:sp42_blue", "rzb_dcs:sp42_green"]); // spring couch right
registerVariants(["rzb_dcs:su08_yellow", "rzb_dcs:su08_green"]); // bamboo armchair
registerVariants(["rzb_dcs:su09_yellow", "rzb_dcs:su09_green"]); // bamboo couch left
registerVariants(["rzb_dcs:su10_yellow", "rzb_dcs:su10_green"]); // bamboo couch middle
registerVariants(["rzb_dcs:su11_yellow", "rzb_dcs:su11_green"]); // bamboo couch right
registerVariants(["rzb_dcs:su12_yellow", "rzb_dcs:su12_green"]); // bamboo side table
registerVariants(["rzb_dcs:su13_yellow", "rzb_dcs:su13_green"]); // bamboo torch
registerVariants(["rzb_dcs:su15_yellow", "rzb_dcs:su15_green"]); // bamboo coffee table round
registerVariants(["rzb_dcs:su16_yellow", "rzb_dcs:su16_green"]); // bamboo coffee table square
registerVariants(["rzb_dcs:su17_yellow", "rzb_dcs:su17_green"]); // bamboo chair
registerVariants(["rzb_dcs:su37_yellow", "rzb_dcs:su37_green"]); // potted bamboo
registerVariants(["rzb_dcs:sp44_blue_and_purple", "rzb_dcs:sp44_pink_and_green"]); // candle lanterns (spring, 3-frame flipbook)
registerVariants(["rzb_dcs:wi49_blue", "rzb_dcs:wi49_green", "rzb_dcs:wi49_red", "rzb_dcs:wi49_white"]); // candle holder / advent candlestick (winter, 3-frame flipbook)
registerVariants(["rzb_dcs:sp45", "rzb_dcs:sp45_rainbow"]); // wind chime 2 (spring) OFF-block form; brushing the animated entity recolours via toggles.js
// rose vine wall (spring) — 3 shape pieces for building vine chains, each brush-cyclable through its 11 colours
registerVariants(["rzb_dcs:sp47_red", "rzb_dcs:sp47_pink", "rzb_dcs:sp47_pink_alt", "rzb_dcs:sp47_orange", "rzb_dcs:sp47_yellow", "rzb_dcs:sp47_yellow_alt", "rzb_dcs:sp47_green", "rzb_dcs:sp47_blue", "rzb_dcs:sp47_purple", "rzb_dcs:sp47_white", "rzb_dcs:sp47_black"]); // middle
registerVariants(["rzb_dcs:sp48_red", "rzb_dcs:sp48_pink", "rzb_dcs:sp48_pink_alt", "rzb_dcs:sp48_orange", "rzb_dcs:sp48_yellow", "rzb_dcs:sp48_yellow_alt", "rzb_dcs:sp48_green", "rzb_dcs:sp48_blue", "rzb_dcs:sp48_purple", "rzb_dcs:sp48_white", "rzb_dcs:sp48_black"]); // diagonal left
registerVariants(["rzb_dcs:sp49_red", "rzb_dcs:sp49_pink", "rzb_dcs:sp49_pink_alt", "rzb_dcs:sp49_orange", "rzb_dcs:sp49_yellow", "rzb_dcs:sp49_yellow_alt", "rzb_dcs:sp49_green", "rzb_dcs:sp49_blue", "rzb_dcs:sp49_purple", "rzb_dcs:sp49_white", "rzb_dcs:sp49_black"]); // diagonal right
registerVariants(["rzb_dcs:su22_black", "rzb_dcs:su22_white", "rzb_dcs:su22_orange", "rzb_dcs:su22_magenta", "rzb_dcs:su22_light_blue", "rzb_dcs:su22_yellow", "rzb_dcs:su22_lime", "rzb_dcs:su22_pink", "rzb_dcs:su22_gray", "rzb_dcs:su22_light_gray", "rzb_dcs:su22_cyan", "rzb_dcs:su22_purple", "rzb_dcs:su22_blue", "rzb_dcs:su22_green", "rzb_dcs:su22_red"]); // beach stool
registerVariants(["rzb_dcs:su23_orange", "rzb_dcs:su23_magenta", "rzb_dcs:su23_blue", "rzb_dcs:su23_green", "rzb_dcs:su23_red"]); // camping chair
registerVariants(["rzb_dcs:su35_black", "rzb_dcs:su35_white", "rzb_dcs:su35_orange", "rzb_dcs:su35_magenta", "rzb_dcs:su35_light_blue", "rzb_dcs:su35_yellow", "rzb_dcs:su35_lime", "rzb_dcs:su35_pink", "rzb_dcs:su35_gray", "rzb_dcs:su35_light_gray", "rzb_dcs:su35_cyan", "rzb_dcs:su35_purple", "rzb_dcs:su35_blue", "rzb_dcs:su35_green", "rzb_dcs:su35_red"]); // beach chair
// --- Winter presents (wi30-33) + poinsettia (wi29) + present trios (wi34/35): gift boxes; brush cycles the colour ---
const PRESENT_12 = ["white", "orange", "magenta", "light_blue", "yellow", "lime", "pink", "cyan", "purple", "blue", "green", "red"]; // DecoCraft canonical order
registerVariants(["rzb_dcs:wi29_white", "rzb_dcs:wi29_pink", "rzb_dcs:wi29_red"]); // poinsettia
registerVariants(PRESENT_12.map((c) => "rzb_dcs:wi30_" + c)); // present
registerVariants(PRESENT_12.map((c) => "rzb_dcs:wi31_" + c)); // present long
registerVariants(PRESENT_12.map((c) => "rzb_dcs:wi32_" + c)); // present round
registerVariants(PRESENT_12.map((c) => "rzb_dcs:wi33_" + c)); // present small
registerVariants([1, 2, 3, 4, 5].map((n) => "rzb_dcs:wi34_" + n)); // present trio 1 (numbered arrangements)
registerVariants([6, 7, 8, 9, 10, 11].map((n) => "rzb_dcs:wi35_" + n)); // present trio 2
registerVariants(["rzb_dcs:wi38_rainbow", "rzb_dcs:wi38_white", "rzb_dcs:wi38_color"]); // string light stars
registerVariants(["rzb_dcs:wi24", "rzb_dcs:wi25"]); // wreath 2 <-> wreath 3
registerVariants(["rzb_dcs:wi26", "rzb_dcs:wi27"]); // wreath 4 <-> wreath 5
registerVariants(["rzb_dcs:wi39_blue", "rzb_dcs:wi39_green", "rzb_dcs:wi39_red"]); // stocking 1
registerVariants(["rzb_dcs:wi40_blue", "rzb_dcs:wi40_green", "rzb_dcs:wi40_red"]); // stocking 2
registerVariants(["rzb_dcs:wi45_blue", "rzb_dcs:wi45_green", "rzb_dcs:wi45_red"]); // winter armchair
registerVariants(["rzb_dcs:wi46_blue", "rzb_dcs:wi46_green", "rzb_dcs:wi46_red"]); // winter couch left
registerVariants(["rzb_dcs:wi47_blue", "rzb_dcs:wi47_green", "rzb_dcs:wi47_red"]); // winter couch middle
registerVariants(["rzb_dcs:wi48_blue", "rzb_dcs:wi48_green", "rzb_dcs:wi48_red"]); // winter couch right
// --- Fall (fa) ---
registerVariants(["rzb_dcs:fa03_ghost", "rzb_dcs:fa03_pumpkin", "rzb_dcs:fa03_purple", "rzb_dcs:fa03_spider", "rzb_dcs:fa03_witch"]); // candy bag (5 designs)
registerVariants(["rzb_dcs:fa07", "rzb_dcs:fa07_nuts", "rzb_dcs:fa07_sludge", "rzb_dcs:fa07_sprinkles"]); // caramel apples
registerVariants(["rzb_dcs:fa09_bone", "rzb_dcs:fa09_candy_corn", "rzb_dcs:fa09_eye", "rzb_dcs:fa09_gravestone", "rzb_dcs:fa09_witch"]); // halloween cupcakes
registerVariants(["rzb_dcs:fa23", "rzb_dcs:fa23_b"]); // web 2 (a/b)
registerVariants(["rzb_dcs:fa30_black", "rzb_dcs:fa30_white", "rzb_dcs:fa30_orange", "rzb_dcs:fa30_magenta", "rzb_dcs:fa30_light_blue", "rzb_dcs:fa30_yellow", "rzb_dcs:fa30_lime", "rzb_dcs:fa30_pink", "rzb_dcs:fa30_gray", "rzb_dcs:fa30_light_gray", "rzb_dcs:fa30_cyan", "rzb_dcs:fa30_purple", "rzb_dcs:fa30_blue", "rzb_dcs:fa30_brown", "rzb_dcs:fa30_green", "rzb_dcs:fa30_red"]); // papel picado (16 colors)
registerVariants(["rzb_dcs:fa41_brown", "rzb_dcs:fa41_orange", "rzb_dcs:fa41_green", "rzb_dcs:fa41_white"]); // fall armchair
registerVariants(["rzb_dcs:fa42_brown", "rzb_dcs:fa42_orange", "rzb_dcs:fa42_green", "rzb_dcs:fa42_white"]); // fall couch left
registerVariants(["rzb_dcs:fa43_brown", "rzb_dcs:fa43_orange", "rzb_dcs:fa43_green", "rzb_dcs:fa43_white"]); // fall couch middle
registerVariants(["rzb_dcs:fa44_brown", "rzb_dcs:fa44_orange", "rzb_dcs:fa44_green", "rzb_dcs:fa44_white"]); // fall couch right
// Paper lanterns (pl_*): 3 brush cycles matching the season split (solids share the plain geo; themed share the tassel geo).
registerVariants(["rzb_dcs:pl_black", "rzb_dcs:pl_white", "rzb_dcs:pl_orange", "rzb_dcs:pl_magenta", "rzb_dcs:pl_light_blue", "rzb_dcs:pl_yellow", "rzb_dcs:pl_lime", "rzb_dcs:pl_pink", "rzb_dcs:pl_gray", "rzb_dcs:pl_cyan", "rzb_dcs:pl_purple", "rzb_dcs:pl_blue", "rzb_dcs:pl_green", "rzb_dcs:pl_red", "rzb_dcs:pl_cream", "rzb_dcs:pl_lavender", "rzb_dcs:pl_light_yellow", "rzb_dcs:pl_mint"]); // 18 solid colors (Summer)
registerVariants(["rzb_dcs:pl_bats", "rzb_dcs:pl_ghost", "rzb_dcs:pl_pumpkin", "rzb_dcs:pl_skeleton"]); // themed (Fall)
registerVariants(["rzb_dcs:pl_cherry_blossom", "rzb_dcs:pl_chinese_new_year"]); // themed (Spring)
// --- Balloons (bl): separate regular/pastel cycles per type (single balloons -> Summer, hearts -> Spring). ---
registerVariants(["rzb_dcs:bl_white", "rzb_dcs:bl_orange", "rzb_dcs:bl_yellow", "rzb_dcs:bl_pink", "rzb_dcs:bl_cyan", "rzb_dcs:bl_purple", "rzb_dcs:bl_blue", "rzb_dcs:bl_green", "rzb_dcs:bl_red", "rzb_dcs:bl_rainbow"]); // balloons (regular)
registerVariants(["rzb_dcs:bl_p_orange", "rzb_dcs:bl_p_yellow", "rzb_dcs:bl_p_lime", "rzb_dcs:bl_p_pink", "rzb_dcs:bl_p_cyan", "rzb_dcs:bl_p_purple", "rzb_dcs:bl_p_blue", "rzb_dcs:bl_p_green", "rzb_dcs:bl_p_red"]); // balloons (pastel)
registerVariants(["rzb_dcs:bl_heart_white", "rzb_dcs:bl_heart_orange", "rzb_dcs:bl_heart_yellow", "rzb_dcs:bl_heart_pink", "rzb_dcs:bl_heart_cyan", "rzb_dcs:bl_heart_purple", "rzb_dcs:bl_heart_blue", "rzb_dcs:bl_heart_green", "rzb_dcs:bl_heart_red", "rzb_dcs:bl_heart_berry", "rzb_dcs:bl_heart_rainbow"]); // heart balloons (regular)
registerVariants(["rzb_dcs:bl_heart_p_orange", "rzb_dcs:bl_heart_p_yellow", "rzb_dcs:bl_heart_p_lime", "rzb_dcs:bl_heart_p_pink", "rzb_dcs:bl_heart_p_cyan", "rzb_dcs:bl_heart_p_purple", "rzb_dcs:bl_heart_p_blue", "rzb_dcs:bl_heart_p_green", "rzb_dcs:bl_heart_p_red"]); // heart balloons (pastel)
// (balloons_small is a 2-cell multiblock -> brush-cycled via a both-cells branch in brush.js, NOT registerVariants.)
// jack 'o lantern carvings — two groups so brushing keeps the on/off state (off cycles off, lit cycles lit); facing preserved
registerVariants(["rzb_dcs:fa33_1", "rzb_dcs:fa33_2", "rzb_dcs:fa33_3", "rzb_dcs:fa33_4"]); // carvings (off)
registerVariants(["rzb_dcs:fa33_1_on", "rzb_dcs:fa33_2_on", "rzb_dcs:fa33_3_on", "rzb_dcs:fa33_4_on"]); // carvings (lit)
+177
View File
@@ -0,0 +1,177 @@
// features/decostation.js: Decobench (make decorations from materials) + Decomposer (break them back).
// Backend only: the station models and clickable UI entities are built separately. Economy mirrors DecoCraft:
// each placed station holds its own material balance on scoreboards (participant = the station entity), and the
// UI drives everything through /scriptevent. Costs come from the auto-derived DECO_COST table.
//
// The decomposer turns blocks into raw_material (dirt 8:1, cobblestone 4:1, any log/wood 2:1, like DecoCraft) and
// turns a decoration back into its materials. The decobench spends stored materials to make a decoration.
//
// UI fires these (sourceEntity = the clicked entity, kept within ~5 blocks of its station):
// deco_craft / deco_decompose / decomp_block / deco_refresh
// feed_mat / feed_red / feed_green / feed_blue / feed_yellow / feed_cyan / feed_purple
// drop_mat / drop_red / drop_green / drop_blue
// A craft/decompose slot picks its decoration with int property rzb_dcs:deco_index (0..DECO_LIST.length-1).
// Wiring writes back rzb_dcs:affordable + cost_* on the slot, and meter_* on the station, for the model to show.
import { world, system, ItemStack } from "../core/mc.js";
import { DECO_COST } from "../core/deco_costs.js";
// stable index -> deco id; hand this to the UI so a browsed decoration maps to its deco_index.
export const DECO_LIST = Object.keys(DECO_COST);
const CAP = { m: 250, r: 1000, g: 1000, b: 1000 }; // mat holds less than dye (DecoCraft)
const UNITS = { m: 1, r: 8, g: 8, b: 8 }; // mat 1/item, dye 8/item
const STATIONS = new Set(["rzb_dcs:decobench", "rzb_dcs:decomposer"]);
const OBJ = { m: "rzb_dcs_deco_mat", r: "rzb_dcs_deco_red", g: "rzb_dcs_deco_green", b: "rzb_dcs_deco_blue" };
const FEED_ITEM = { m: "rzb_dcs:raw_material", r: "minecraft:red_dye", g: "minecraft:green_dye", b: "minecraft:blue_dye" };
const CHANNELS = ["m", "r", "g", "b"];
// block -> how many make 1 raw_material (DecoCraft rates). dirt 8, cobblestone 4, every log/wood 2.
const DECOMP = { "minecraft:dirt": 8, "minecraft:cobblestone": 4 };
for (const s of ["oak", "spruce", "birch", "jungle", "acacia", "dark_oak", "cherry", "mangrove", "pale_oak"])
for (const f of [s + "_log", s + "_wood", "stripped_" + s + "_log", "stripped_" + s + "_wood"]) DECOMP["minecraft:" + f] = 2;
for (const s of ["crimson", "warped"])
for (const f of [s + "_stem", s + "_hyphae", "stripped_" + s + "_stem", "stripped_" + s + "_hyphae"]) DECOMP["minecraft:" + f] = 2;
// scoreboards, one balance per placed station
function objective(key) {
let o = world.scoreboard.getObjective(OBJ[key]);
if (!o) { try { o = world.scoreboard.addObjective(OBJ[key], OBJ[key]); } catch { o = world.scoreboard.getObjective(OBJ[key]); } }
return o;
}
system.run(() => { for (const k of CHANNELS) objective(k); try { world.scoreboard.getObjective("rzb_dcs_logs_decomposed") || world.scoreboard.addObjective("rzb_dcs_logs_decomposed", "rzb_dcs_logs_decomposed"); } catch {} });
const get = (station, key) => { try { return objective(key).getScore(station) ?? 0; } catch { return 0; } };
const setRaw = (station, key, v) => { try { objective(key).setScore(station, Math.max(0, Math.min(CAP[key], v))); } catch {} };
const addUnits = (station, key, delta) => setRaw(station, key, get(station, key) + delta);
const balance = (station) => ({ m: get(station, "m"), r: get(station, "r"), g: get(station, "g"), b: get(station, "b") });
// mirror the balance onto the station's meter_* props so the model can show fill
function syncMeters(station) {
if (!station?.isValid) return;
for (const k of CHANNELS) { try { station.setProperty("rzb_dcs:meter_" + k, get(station, k)); } catch {} }
}
// the UI entity sits near its station; players sit near the UI entity
function nearestStation(src) {
if (!src?.isValid) return null;
if (STATIONS.has(src.typeId)) return src;
for (const type of STATIONS) { const e = src.dimension.getEntities({ location: src.location, type, closest: 1, maxDistance: 5 })[0]; if (e) return e; }
return null;
}
const nearestPlayer = (src) => src?.dimension.getPlayers({ location: src.location, closest: 1, maxDistance: 8 })[0] ?? null;
// pull up to `count` of itemId from a player; returns how many came out
function takeFromPlayer(player, itemId, count) {
const inv = player?.getComponent("minecraft:inventory")?.container; if (!inv) return 0;
let need = count;
for (let i = 0; i < inv.size && need > 0; i++) {
const it = inv.getItem(i); if (it?.typeId !== itemId) continue;
const take = Math.min(it.amount, need); need -= take;
if (it.amount - take <= 0) inv.setItem(i, undefined);
else { it.amount -= take; inv.setItem(i, it); }
}
return count - need;
}
function givePlayer(player, itemId, count) {
const inv = player?.getComponent("minecraft:inventory")?.container;
const stack = new ItemStack(itemId, Math.max(1, Math.min(64, count)));
const leftover = inv ? inv.addItem(stack) : stack;
if (leftover) try { player.dimension.spawnItem(leftover, player.location); } catch {}
}
const dropAt = (dim, loc, itemId, count) => { try { for (let left = count; left > 0; left -= 64) dim.spawnItem(new ItemStack(itemId, Math.min(64, left)), loc); } catch {} };
// which decoration a slot points at (rzb_dcs:deco_index -> DECO_LIST)
function slotDeco(slot) {
let idx; try { idx = slot.getProperty("rzb_dcs:deco_index"); } catch {}
if (typeof idx !== "number" || idx < 0 || idx >= DECO_LIST.length) return null;
const id = DECO_LIST[idx];
return { id, cost: DECO_COST[id] };
}
const affordable = (station, cost) => { const b = balance(station); return b.m >= (cost.m || 0) && b.r >= (cost.r || 0) && b.g >= (cost.g || 0) && b.b >= (cost.b || 0); };
function writeSlotState(slot, station, cost) {
try { slot.setProperty("rzb_dcs:affordable", affordable(station, cost)); } catch {}
for (const k of CHANNELS) { try { slot.setProperty("rzb_dcs:cost_" + k, cost[k] || 0); } catch {} }
}
// transactions
function craft(station, slot) {
const d = slotDeco(slot); if (!d || !d.cost) return;
if (!affordable(station, d.cost)) { try { slot.setProperty("rzb_dcs:affordable", false); } catch {} return; }
for (const k of CHANNELS) if (d.cost[k]) addUnits(station, k, -d.cost[k]);
const player = nearestPlayer(station);
if (player) givePlayer(player, d.id, 1); else dropAt(station.dimension, station.location, d.id, 1);
syncMeters(station); writeSlotState(slot, station, d.cost);
}
function decompose(station, slot) {
const d = slotDeco(slot); if (!d || !d.cost) return;
const player = nearestPlayer(station); if (!player) return;
if (takeFromPlayer(player, d.id, 1) < 1) return; // must hold the decoration to break it
for (const k of CHANNELS) if (d.cost[k]) givePlayer(player, FEED_ITEM[k], d.cost[k]); // hand its materials back as items
try { world.scoreboard.getObjective("rzb_dcs_logs_decomposed")?.addScore(station, 1); } catch {}
}
// blocks -> raw_material, from the player's held stack (floor by rate, leftover stays), like DecoCraft
function decompBlock(station) {
const player = nearestPlayer(station); if (!player) return;
const inv = player.getComponent("minecraft:inventory")?.container; if (!inv) return;
const idx = player.selectedSlotIndex ?? 0, held = inv.getItem(idx);
const rate = held && DECOMP[held.typeId]; if (!rate) return;
const toraw = Math.floor(held.amount / rate); if (toraw < 1) return;
const used = toraw * rate;
if (held.amount - used <= 0) inv.setItem(idx, undefined);
else { held.amount -= used; inv.setItem(idx, held); }
givePlayer(player, FEED_ITEM.m, toraw);
}
function feed(station, key) {
const player = nearestPlayer(station); if (!player) return;
const space = CAP[key] - get(station, key); if (space <= 0) return;
const per = UNITS[key];
const maxItems = Math.floor(space / per) + (space % per ? 1 : 0); // last item may partly fill
const took = takeFromPlayer(player, FEED_ITEM[key], maxItems); if (!took) return;
addUnits(station, key, Math.min(space, took * per));
syncMeters(station);
}
// yellow/cyan/purple each split 4 units into two channels (DecoCraft)
function feedCompound(station, itemId, kA, kB) {
const player = nearestPlayer(station); if (!player) return;
const per = 4, spaceA = CAP[kA] - get(station, kA), spaceB = CAP[kB] - get(station, kB);
const room = Math.max(spaceA, spaceB); if (room <= 0) return;
const took = takeFromPlayer(player, itemId, Math.ceil(room / per)); if (!took) return;
addUnits(station, kA, Math.min(spaceA, took * per));
addUnits(station, kB, Math.min(spaceB, took * per));
syncMeters(station);
}
function drop(station, key) {
const stored = get(station, key); const per = UNITS[key]; if (stored < per) return; // whole items only
const items = Math.floor(stored / per);
setRaw(station, key, stored - items * per);
dropAt(station.dimension, station.location, FEED_ITEM[key], items);
syncMeters(station);
}
function refresh(station, slot) { const d = slotDeco(slot); if (d?.cost) writeSlotState(slot, station, d.cost); }
// the /scriptevent router; the UI drives everything through here
const HANDLERS = {
"rzb_dcs:deco_craft": (st, src) => craft(st, src),
"rzb_dcs:deco_decompose": (st, src) => decompose(st, src),
"rzb_dcs:decomp_block": (st) => decompBlock(st),
"rzb_dcs:deco_refresh": (st, src) => refresh(st, src),
"rzb_dcs:feed_mat": (st) => feed(st, "m"),
"rzb_dcs:feed_red": (st) => feed(st, "r"),
"rzb_dcs:feed_green": (st) => feed(st, "g"),
"rzb_dcs:feed_blue": (st) => feed(st, "b"),
"rzb_dcs:feed_yellow": (st) => feedCompound(st, "minecraft:yellow_dye", "r", "g"),
"rzb_dcs:feed_cyan": (st) => feedCompound(st, "minecraft:cyan_dye", "g", "b"),
"rzb_dcs:feed_purple": (st) => feedCompound(st, "minecraft:purple_dye", "r", "b"),
"rzb_dcs:drop_mat": (st) => drop(st, "m"),
"rzb_dcs:drop_red": (st) => drop(st, "r"),
"rzb_dcs:drop_green": (st) => drop(st, "g"),
"rzb_dcs:drop_blue": (st) => drop(st, "b"),
};
system.afterEvents.scriptEventReceive.subscribe((ev) => {
const fn = HANDLERS[ev.id]; if (!fn) return;
const station = nearestStation(ev.sourceEntity); if (!station) return;
fn(station, ev.sourceEntity);
}, { namespaces: ["rzb_dcs"] });
// set meters when a station loads so the model shows the right fill right away
world.afterEvents.entitySpawn?.subscribe((ev) => { if (STATIONS.has(ev.entity?.typeId)) syncMeters(ev.entity); });
+52
View File
@@ -0,0 +1,52 @@
// features/display.js — animated display pieces (place a block -> spawn an entity).
import { DISPLAY_DIR_YAW } from "../core/grid.js";
import { summonDisplay, breakFx, particleFor } from "../core/fx.js";
import { onPlace, onBreak } from "../core/events.js";
import { BlockPermutation } from "../core/mc.js";
const DISPLAY_BLOCKS = {
"rzb_dcs:su36_horchata": { entity: "rzb_dcs:su36_horchata_e", y: 0 },
"rzb_dcs:su36_jamaica": { entity: "rzb_dcs:su36_jamaica_e", y: 0 },
"rzb_dcs:su36_tamarind": { entity: "rzb_dcs:su36_tamarind_e", y: 0 },
"rzb_dcs:su36_lemonade": { entity: "rzb_dcs:su36_lemonade_e", y: 0 },
"rzb_dcs:su36_lemonade_pink": { entity: "rzb_dcs:su36_lemonade_pink_e", y: 0 },
"rzb_dcs:su36_limeade": { entity: "rzb_dcs:su36_limeade_e", y: 0 },
"rzb_dcs:su36_orangeade": { entity: "rzb_dcs:su36_orangeade_e", y: 0 },
"rzb_dcs:wi37": { entity: "rzb_dcs:pinata", y: 0 },
"rzb_dcs:wi43": { entity: "rzb_dcs:globe_igloo", y: 0 },
"rzb_dcs:wi44": { entity: "rzb_dcs:globe_tree", y: 0 }
};
onPlace((ev) => {
const b = ev.block, cfg = DISPLAY_BLOCKS[b.typeId]; if (!cfg) return;
let yaw = 0; try { yaw = DISPLAY_DIR_YAW[b.permutation.getState("minecraft:cardinal_direction")] ?? 0; } catch {}
summonDisplay(b.dimension, cfg.entity, b.x + 0.5, b.y + (cfg.y ?? 0), b.z + 0.5, yaw);
});
onBreak((ev) => {
const id = ev.brokenBlockPermutation?.type?.id, cfg = id && DISPLAY_BLOCKS[id]; if (!cfg) return;
const b = ev.block, at = { x: b.x + 0.5, y: b.y + (cfg.y ?? 0), z: b.z + 0.5 };
for (const ent of b.dimension.getEntities({ type: cfg.entity, location: at, maxDistance: 0.55 })) ent.remove();
breakFx(b.dimension, at.x, b.y + 0.5, at.z);
});
const DISPLAY_BRUSH = {};
const regDisplayBrush = (ids) => { for (const id of ids) DISPLAY_BRUSH[id] = ids; };
regDisplayBrush(["rzb_dcs:su36_jamaica", "rzb_dcs:su36_orangeade", "rzb_dcs:su36_lemonade", "rzb_dcs:su36_limeade", "rzb_dcs:su36_lemonade_pink", "rzb_dcs:su36_horchata", "rzb_dcs:su36_tamarind"]);
export const displayAnchorBrushable = (id) => !!DISPLAY_BRUSH[id];
export function displayAnchorRecolor(block) { // returns the new variant's brush-particle colour (via BRUSH_ALIAS), or false
const cur = block?.typeId, seq = DISPLAY_BRUSH[cur]; if (!seq) return false;
const nextId = seq[(seq.indexOf(cur) + 1) % seq.length];
const cfg = DISPLAY_BLOCKS[cur], nextCfg = DISPLAY_BLOCKS[nextId]; if (!cfg || !nextCfg) return false;
const dim = block.dimension, ax = block.x, ay = block.y, az = block.z;
let dir = "south"; try { dir = block.permutation.getState("minecraft:cardinal_direction") ?? "south"; } catch {}
try { block.setPermutation(BlockPermutation.resolve(nextId, { "minecraft:cardinal_direction": dir })); } catch {}
const at = { x: ax + 0.5, y: ay + (cfg.y ?? 0), z: az + 0.5 };
for (const e of dim.getEntities({ type: cfg.entity, location: at, maxDistance: 0.6 })) { try { e.remove(); } catch {} }
summonDisplay(dim, nextCfg.entity, ax + 0.5, ay + (nextCfg.y ?? 0), az + 0.5, DISPLAY_DIR_YAW[dir] ?? 0);
return particleFor(nextId);
}
+135
View File
@@ -0,0 +1,135 @@
// features/interact.js — right-click lids/toggles + the interact fall-through dispatcher.
import { world, system, BlockPermutation, getDir, safeGetBlock } from "../core/mc.js";
import { tubeOverWater } from "../core/grid.js";
import { debounced } from "../core/fx.js";
import { registerVariants } from "../core/variants.js";
import { LT_CELLS, ltParse, ltCellWorld, ltAnchorFrom, LT_CELLS_W, ltParseW, ltCellWorldW, ltAnchorFromW } from "./mb_trees.js";
import { sit } from "./seating.js";
import { EMBER_BLOCKS, emberPos, clearEmberAt, spawnEmberFor } from "./maintenance.js";
const _boxRecent = new Set();
// Box of Chocolates: right-click swaps CLOSED (sp51, base/anchor) <-> OPEN (sp51_open, hidden state), keeping facing.
function boxToggle(block) {
const id = block?.typeId; if (id !== "rzb_dcs:sp51" && id !== "rzb_dcs:sp51_open") return false;
if (debounced(_boxRecent, block.x + "," + block.y + "," + block.z, 4)) return true;
const to = id === "rzb_dcs:sp51" ? "rzb_dcs:sp51_open" : "rzb_dcs:sp51";
let dir; try { dir = block.permutation.getState("minecraft:cardinal_direction"); } catch {}
try { block.setPermutation(BlockPermutation.resolve(to, dir ? { "minecraft:cardinal_direction": dir } : {})); } catch {}
return true;
}
const _coolerRecent = new Set();
function coolerToggle(block) {
const from = block?.typeId; const m = from?.match(/^rzb_dcs:(su14|su19)_(closed_)?(.+)$/); if (!m) return false;
if (debounced(_coolerRecent, block.x + "," + block.y + "," + block.z, 4)) return true;
const to = m[2] ? "rzb_dcs:" + m[1] + "_" + m[3] : "rzb_dcs:" + m[1] + "_closed_" + m[3];
let dir; try { dir = block.permutation.getState("minecraft:cardinal_direction"); } catch {}
try { block.setPermutation(BlockPermutation.resolve(to, dir ? { "minecraft:cardinal_direction": dir } : {})); } catch {}
// open-grill smoke follows the state (setPermutation doesn't fire onPlace/onBreak): spawn the emitter on open, clear on close
try {
const dim = block.dimension, base = { x: block.x, y: block.y, z: block.z };
clearEmberAt(dim, emberPos({ ...base, typeId: (from in EMBER_BLOCKS) ? from : to }));
if (to in EMBER_BLOCKS) dim.spawnEntity(EMBER_BLOCKS[to].ent, emberPos({ ...base, typeId: to }));
} catch {}
return true;
}
registerVariants(["rzb_dcs:su14_black", "rzb_dcs:su14_orange", "rzb_dcs:su14_gray", "rzb_dcs:su14_light_gray", "rzb_dcs:su14_blue", "rzb_dcs:su14_green", "rzb_dcs:su14_red"]); // cooler (open)
registerVariants(["rzb_dcs:su14_closed_black", "rzb_dcs:su14_closed_orange", "rzb_dcs:su14_closed_gray", "rzb_dcs:su14_closed_light_gray", "rzb_dcs:su14_closed_blue", "rzb_dcs:su14_closed_green", "rzb_dcs:su14_closed_red"]); // cooler (closed)
registerVariants(["rzb_dcs:su19_black", "rzb_dcs:su19_blue", "rzb_dcs:su19_red", "rzb_dcs:su19_silver"]); // grill (open)
registerVariants(["rzb_dcs:su19_closed_black", "rzb_dcs:su19_closed_blue", "rzb_dcs:su19_closed_red", "rzb_dcs:su19_closed_silver"]); // grill (closed)
// ---- Christmas lights: right-click toggles a light's flicker<->cycle STATE (rzb_dcs:mode). Brush swaps the colour. ----
const XMAS_MODES = { "rzb_dcs:wi05_color": 2, "rzb_dcs:wi05_white": 2, "rzb_dcs:wi05_rainbow": 2, "rzb_dcs:wi06_rainbow": 2, "rzb_dcs:wi09_rainbow": 2, "rzb_dcs:wi09_white": 2, "rzb_dcs:wi09_yellow": 2 };
registerVariants(["rzb_dcs:wi05_rainbow", "rzb_dcs:wi05_white", "rzb_dcs:wi05_color"]); // christmas lights style 1
registerVariants(["rzb_dcs:wi06_rainbow", "rzb_dcs:wi06_white", "rzb_dcs:wi06_color"]); // christmas lights style 2
registerVariants(["rzb_dcs:wi07_rainbow", "rzb_dcs:wi07_white", "rzb_dcs:wi07_color"]); // ground lights centre strip
registerVariants(["rzb_dcs:wi08_rainbow", "rzb_dcs:wi08_white", "rzb_dcs:wi08_color"]); // ground lights edge strip
registerVariants(["rzb_dcs:wi09_rainbow", "rzb_dcs:wi09_yellow", "rzb_dcs:wi09_white"]); // meteor lights
registerVariants(["rzb_dcs:wi16_chocolate_chip", "rzb_dcs:wi16_mmm", "rzb_dcs:wi16_raisins"]); // cookie plate (shared-geo types)
registerVariants(["rzb_dcs:wi18_rainbow", "rzb_dcs:wi18_white"]); // tinsel 1
registerVariants(["rzb_dcs:wi19_bg", "rzb_dcs:wi19_rb", "rzb_dcs:wi19_rg", "rzb_dcs:wi19_sg"]); // tinsel 2
const _xmasRecent = new Set();
function xmasLightCycle(block) {
const n = XMAS_MODES[block?.typeId]; if (!n) return false;
if (debounced(_xmasRecent, block.x + "," + block.y + "," + block.z, 4)) return true;
let mode = 0; try { mode = block.permutation.getState("rzb_dcs:mode") ?? 0; } catch {}
try { block.setPermutation(block.permutation.withState("rzb_dcs:mode", (mode + 1) % n)); } catch {}
return true;
}
const _ltFlickerRecent = new Set();
function ltFlickerToggle(block) {
const rainbow = ltParse(block?.typeId), cell = rainbow || ltParseW(block?.typeId); if (!cell) return false; // rainbow OR white light tree
if (debounced(_ltFlickerRecent, block.x + "," + block.y + "," + block.z, 4)) return true;
const CELLS = rainbow ? LT_CELLS : LT_CELLS_W, parse = rainbow ? ltParse : ltParseW, cellWorld = rainbow ? ltCellWorld : ltCellWorldW, anchorFrom = rainbow ? ltAnchorFrom : ltAnchorFromW;
const dir = getDir(block.permutation), a = anchorFrom(cell, block.x, block.y, block.z, dir), dim = block.dimension;
let fl = 0; try { fl = block.permutation.getState("rzb_dcs:fl") ?? 0; } catch {}
const next = (fl + 1) % 2;
system.runTimeout(() => { for (const c of Object.keys(CELLS)) { const p = cellWorld(c, a.x, a.y, a.z, dir), nb = safeGetBlock(dim, p); if (nb && parse(nb.typeId) === c) { try { nb.setPermutation(nb.permutation.withState("rzb_dcs:fl", next)); } catch {} } } }, 1);
return true;
}
const _emberRecent = new Set();
function emberToggle(block) {
if (!(block?.typeId in EMBER_BLOCKS)) return false;
if (debounced(_emberRecent, block.x + "," + block.y + "," + block.z, 4)) return true;
let lit = 1; try { lit = block.permutation.getState("rzb_dcs:lit") ?? 1; } catch {}
const next = lit ? 0 : 1;
try { block.setPermutation(block.permutation.withState("rzb_dcs:lit", next)); } catch {}
clearEmberAt(block.dimension, emberPos(block));
if (next) spawnEmberFor(block); // re-lit -> fresh emitter at the flame
return true;
}
const LIGHT_TOGGLE = new Set(["rzb_dcs:sp34", "rzb_dcs:su24", "rzb_dcs:su28"]); // garden + camping lantern + jellyfish lamp
const _lanternRecent = new Set();
function lanternToggle(block) {
if (!LIGHT_TOGGLE.has(block?.typeId)) return false;
if (debounced(_lanternRecent, block.x + "," + block.y + "," + block.z, 4)) return true;
let lit = 1; try { lit = block.permutation.getState("rzb_dcs:lit") ?? 1; } catch {}
try { block.setPermutation(block.permutation.withState("rzb_dcs:lit", lit ? 0 : 1)); } catch {}
return true;
}
const _jolRecent = new Set();
function jolToggle(block) {
const id = block?.typeId; if (!id || !/^rzb_dcs:fa3[34](_[1-4])?(_on)?$/.test(id)) return false;
if (debounced(_jolRecent, block.x + "," + block.y + "," + block.z, 4)) return true;
const to = id.endsWith("_on") ? id.slice(0, -3) : id + "_on";
try { block.setPermutation(BlockPermutation.resolve(to, { "minecraft:cardinal_direction": getDir(block.permutation) })); } catch {}
return true;
}
world.afterEvents.playerInteractWithBlock?.subscribe((ev) => {
if (ev.itemStack?.typeId === "rzb_dcs:brush_color") return; // brush handled in beforeEvents (brush.js)
if (coolerToggle(ev.block)) return;
if (boxToggle(ev.block)) return;
if (xmasLightCycle(ev.block)) return;
if (ltFlickerToggle(ev.block)) return;
if (emberToggle(ev.block)) return;
if (lanternToggle(ev.block)) return;
if (jolToggle(ev.block)) return;
// su02 tube / su03 lounger over water are boats (ride_* component boards them) — don't also sit.
const id = ev.block?.typeId;
if ((id?.startsWith("rzb_dcs:su02_") || id?.startsWith("rzb_dcs:su03_")) && tubeOverWater(ev.block)) return;
sit(ev.player, ev.block);
});
system.beforeEvents.startup.subscribe((init) => {
const reg = init.blockComponentRegistry;
reg.registerCustomComponent("rzb_dcs:toggle_open", { onPlayerInteract(e) { const b = e.block; try { b.setPermutation(b.permutation.withState("rzb_dcs:open", !b.permutation.getState("rzb_dcs:open"))); } catch {} } });
reg.registerCustomComponent("rzb_dcs:grill", { onPlayerInteract(e) { coolerToggle(e.block); } });
reg.registerCustomComponent("rzb_dcs:cooler", { onPlayerInteract(e) { coolerToggle(e.block); } });
reg.registerCustomComponent("rzb_dcs:box_toggle", { onPlayerInteract(e) { boxToggle(e.block); } });
reg.registerCustomComponent("rzb_dcs:xmas_cycle", { onPlayerInteract(e) { xmasLightCycle(e.block); } });
reg.registerCustomComponent("rzb_dcs:lt_flicker", { onPlayerInteract(e) { ltFlickerToggle(e.block); } });
reg.registerCustomComponent("rzb_dcs:firepit", { onPlayerInteract(e) { emberToggle(e.block); } }); // firepit on<->off
reg.registerCustomComponent("rzb_dcs:torch", { onPlayerInteract(e) { emberToggle(e.block); } }); // bamboo torch on<->off
reg.registerCustomComponent("rzb_dcs:lantern", { onPlayerInteract(e) { lanternToggle(e.block); } }); // garden/camping lantern on<->off
reg.registerCustomComponent("rzb_dcs:jol", { onPlayerInteract(e) { // jack 'o lantern off<->on swap (brush cycles the carving instead)
const inv = e.player?.getComponent("minecraft:inventory")?.container;
if (inv?.getItem(e.player?.selectedSlotIndex ?? 0)?.typeId === "rzb_dcs:brush_color") return; // brush -> carving cycle (brush.js), not a toggle
jolToggle(e.block);
} });
});
+153
View File
@@ -0,0 +1,153 @@
// features/maintenance.js — pick-block anchor redirect + firepit embers.
import { world, system, ItemStack, safeGetBlock, isCreative } from "../core/mc.js";
import { onPlace, onBreak } from "../core/events.js";
import { UMBRELLA_BAMBOO, UMBRELLA_BEACH, BAMBOO_TABLE } from "./mb_grid.js";
import { BL_TOP, BL_HEART_TOP, singleBalloonTop } from "./mb_pairs.js";
const UM_ANCHOR = UMBRELLA_BAMBOO.anchorKey, BU_ANCHOR = UMBRELLA_BEACH.anchorKey, BT_ANCHOR = BAMBOO_TABLE.anchorKey;
function anchorItem(typeId) {
if (!typeId || !typeId.startsWith("rzb_dcs:")) return null;
const id = typeId.slice(8);
if (id.startsWith("fc_") && id !== "fc_3_0_0") return "rzb_dcs:fc_3_0_0"; // flower cart
if (id.startsWith("ic_") && id !== "ic_1_0_0") return "rzb_dcs:ic_1_0_0"; // ice cream cart
if (id.startsWith("ct_") && id !== "ct_1_1_0_1" && id !== "ct_1_1_0_1_w") return id.endsWith("_w") ? "rzb_dcs:ct_1_1_0_1_w" : "rzb_dcs:ct_1_1_0_1"; // christmas tree (sub-cell -> matching rainbow/white anchor)
if (id.startsWith("lt_") && id !== "lt_1_1_0_1" && id !== "lt_1_1_0_1_w") return id.endsWith("_w") ? "rzb_dcs:lt_1_1_0_1_w" : "rzb_dcs:lt_1_1_0_1"; // light tree
if (id.startsWith("pct_") && id !== "pct_b" && id !== "pct_b_w") return id.endsWith("_w") ? "rzb_dcs:pct_b_w" : "rzb_dcs:pct_b"; // potted christmas tree (top -> anchor)
if (id.startsWith("at_") && id !== "at_1_1_0_1") return "rzb_dcs:at_1_1_0_1"; // antique christmas tree
if (id.startsWith("mn_") && id !== "mn_1_1_0_0") return "rzb_dcs:mn_1_1_0_0"; // christmas mantel
{ const lg = id.match(/^su03_(\d+_\d+_\d+)_(.+)$/); if (lg && lg[1] !== "0_0_0") return "rzb_dcs:su03_0_0_0_" + lg[2]; } // pool lounger sub-cell -> that colour's anchor
if (id.startsWith("dv_") && id !== "dv_front") return "rzb_dcs:dv_front"; // diving board
if (id.startsWith("jc_") && id !== "jc_fr_mid") return "rzb_dcs:jc_fr_mid"; // jacuzzi
if (id.startsWith("sp38_") && id !== "sp38_fr_l") return "rzb_dcs:sp38_fr_l"; // picnic table
if (id.startsWith("tr_") && id !== "tr_fl") return "rzb_dcs:tr_fl"; // trampoline
{ const um = id.match(/^um_(\d+_\d+_\d+)_(green|yellow)$/); if (um && um[1] !== UM_ANCHOR) return "rzb_dcs:um_" + UM_ANCHOR + "_" + um[2]; } // bamboo umbrella
{ const bu = id.match(/^bu_(\d+_\d+_\d+)_(.+)$/); if (bu && bu[1] !== BU_ANCHOR) return "rzb_dcs:bu_" + BU_ANCHOR + "_" + bu[2]; } // beach umbrella
{ const cc = id.match(/^su14_closed_(.+)$/); if (cc) return "rzb_dcs:su14_" + cc[1]; } // closed cooler -> open cooler item
{ const gc = id.match(/^su19_closed_(.+)$/); if (gc) return "rzb_dcs:su19_" + gc[1]; } // closed grill -> open grill item
if (id === "sp51_open") return "rzb_dcs:sp51"; // box of chocolates OPEN state -> closed anchor item
{ const tb = id.match(/^tw_b_(.+)$/); if (tb) return "rzb_dcs:tw_fr_" + tb[1]; } // beach towel back -> front (menu) item
{ const lb = id.match(/^lo_b_(.+)$/); if (lb) return "rzb_dcs:lo_fr_" + lb[1]; } // lounge chair back -> front (menu) item
if (id === "fa05_t") return "rzb_dcs:fa05_b"; // casket 1 top -> bottom (menu) item
if (id === "fa48_b") return "rzb_dcs:fa48"; // casket ground foot -> head (menu) item
{ const w = id.match(/^fa25_r_(.+)$/); if (w) return "rzb_dcs:fa25_l_" + w[1]; } // web 4 right cell -> left (menu) item
{ const o = id.match(/^fa26_(br|tl|tr)(_orange|_yellow)?$/); if (o) return "rzb_dcs:fa26_bl" + (o[2] || ""); } // ofrenda cells -> matching-colour bl (menu) item
{ const m = id.match(/^(fa3[34](?:_[1-4])?)_on$/); if (m) return "rzb_dcs:" + m[1]; } // lit jack 'o lantern -> off (menu) item
{ const m = id.match(/^fa36_([123])_t$/); if (m) return "rzb_dcs:fa36_" + m[1] + "_b"; } // scarecrow top -> bottom (menu) item
{ const m = id.match(/^bl_small_(.+)_t$/); if (m) return "rzb_dcs:bl_small_" + m[1] + "_b"; } // balloons small top -> bottom (menu) item
{ const m = id.match(/^bl_hearts_(.+)_t$/); if (m) return "rzb_dcs:bl_hearts_" + m[1] + "_b"; } // heart-pair top -> bottom (menu) item
{ const m = id.match(/^bl_bunch_(.+)_t$/); if (m) return "rzb_dcs:bl_bunch_" + m[1] + "_b"; } // balloons bunch top -> bottom (menu) item
{ const la = id.match(/^lc_a_(.+)$/); if (la) return "rzb_dcs:lc_b_" + la[1]; } // lifeguard awning -> base (menu) item
{ const bk = id.match(/^br_b_(.+)$/); if (bk) return "rzb_dcs:br_fr_" + bk[1]; } // bike rack back -> front (menu) item
if (id === "bo_t") return "rzb_dcs:bo_b"; // buoy top -> bottom (menu) item
if (id === "wi02_t") return "rzb_dcs:wi02_b"; // snowman top -> base (menu) item
if (/^wi12_(r|p\d+)$/.test(id)) return "rzb_dcs:wi12_c"; // ornament small/collision -> center (menu) item
{ const bt = id.match(/^bt_(\d+_\d+_\d+)_(.+)$/); if (bt && bt[1] !== BT_ANCHOR) return "rzb_dcs:bt_" + BT_ANCHOR + "_" + bt[2]; } // bamboo table non-anchor -> anchor
let m = id.match(/^sp31_(?:bl|br|tl|tr)_(.+)$/); if (m && !id.startsWith("sp31_bl_")) return "rzb_dcs:sp31_bl_" + m[1]; // garden swing
m = id.match(/^sp32_(?:b|t)_(.+)$/); if (m && !id.startsWith("sp32_b_")) return "rzb_dcs:sp32_b_" + m[1]; // rose swing
m = id.match(/^sp29_(.+)$/); if (m) return "rzb_dcs:sp13_" + m[1]; // fairy lights right -> left menu
{ const hs = id.match(/^hs_(.+)_r$/); if (hs) return "rzb_dcs:hs_" + hs[1] + "_l"; } // holiday sign right -> left menu
{ const sb = id.match(/^su30_(?:t|lb|lf)_(.+)$/); if (sb) return "rzb_dcs:su30_b_" + sb[1]; } // surfboard sub-parts -> base
{ const bb = id.match(/^su31_fr_(.+)$/); if (bb) return "rzb_dcs:su31_b_" + bb[1]; } // basket bike front -> base
{ const sl = id.match(/^wi41_fr_(.+)$/); if (sl) return "rzb_dcs:wi41_b_" + sl[1]; } // sled front -> back (menu) item
if (/^tent_(fr_r|bl|br)$/.test(id)) return "rzb_dcs:tent_fr_l"; // tent sub-cells -> anchor
if (/^sp15_2_(pole|fish)$/.test(id)) return "rzb_dcs:sp15_2"; // koinobori children colour 2 cells -> main item
if (/^sp15_(pole|fish)$/.test(id)) return "rzb_dcs:sp15"; // koinobori children static cells -> main item
if (/^sp16_(pole|fish)$/.test(id)) return "rzb_dcs:sp16"; // koinobori parents
if (/^sp18_(pole|fish)$/.test(id)) return "rzb_dcs:sp18"; // koinobori top
return null;
}
const EGG_TO_ITEM = {
"rzb_dcs:sprinkler_spawn_egg": "rzb_dcs:su34",
"rzb_dcs:buoy_spawn_egg": "rzb_dcs:bo_b",
"rzb_dcs:sp35_robin_e_spawn_egg": "rzb_dcs:sp35_robin",
"rzb_dcs:sp35_blue_j_e_spawn_egg": "rzb_dcs:sp35_blue_j",
};
// The single-balloon top hitbox (bl_top / bl_heart_top) is shared across colours, so picking it can't name a colour by id.
// Instead read the balloon directly below the top the player is looking at, and hand back THAT balloon's item.
const balloonBelowView = (p, topId) => { try { const bl = p.getBlockFromViewDirection({ includePassableBlocks: true, maxDistance: 8 })?.block; if (!bl || bl.typeId !== topId) return null; const below = safeGetBlock(bl.dimension, { x: bl.x, y: bl.y - 1, z: bl.z }); return (below && singleBalloonTop(below.typeId)) ? below.typeId : null; } catch { return null; } };
system.runInterval(() => {
for (const p of world.getAllPlayers()) {
let inv; try { inv = p.getComponent("minecraft:inventory")?.container; } catch { continue; }
if (!inv) continue;
for (let i = 0; i < inv.size; i++) {
const it = inv.getItem(i); if (!it) continue;
let a = anchorItem(it.typeId) || EGG_TO_ITEM[it.typeId] || null;
if (!a && (it.typeId === BL_TOP || it.typeId === BL_HEART_TOP)) a = balloonBelowView(p, it.typeId); // balloon top hitbox -> the balloon beneath it
if (a) { try { inv.setItem(i, new ItemStack(a, it.amount)); } catch {} }
}
}
}, 1);
export const EMBER_ENT = "rzb_dcs:ember_emitter", TORCH_ENT = "rzb_dcs:torch_emitter", CAULDRON_ENT = "rzb_dcs:cauldron_emitter", GRILL_ENT = "rzb_dcs:grill_emitter", SPARKLE_ENT = "rzb_dcs:sparkle_emitter";
const EMBER_ENTS = [EMBER_ENT, TORCH_ENT, CAULDRON_ENT, GRILL_ENT, SPARKLE_ENT];
export const EMBER_BLOCKS = {
"rzb_dcs:su26": { y: 0.35, ent: EMBER_ENT },
"rzb_dcs:su13_green": { y: 0.85, ent: TORCH_ENT },
"rzb_dcs:su13_yellow": { y: 0.85, ent: TORCH_ENT },
"rzb_dcs:fa37": { y: 0.7, ent: CAULDRON_ENT },
"rzb_dcs:fa08": { y: 0.4, ent: SPARKLE_ENT }, // crystal ball — always-on twinkling sparkle ring around the orb (no lit state)
// open grill = smoke; the CLOSED grill ids aren't here, so the sweep removes the emitter when it's shut (no lit state)
"rzb_dcs:su19_black": { y: 0.9, ent: GRILL_ENT },
"rzb_dcs:su19_blue": { y: 0.9, ent: GRILL_ENT },
"rzb_dcs:su19_red": { y: 0.9, ent: GRILL_ENT },
"rzb_dcs:su19_silver": { y: 0.9, ent: GRILL_ENT },
};
export const emberPos = (b) => ({ x: b.x + 0.5, y: b.y + (EMBER_BLOCKS[b.typeId]?.y ?? 0.35), z: b.z + 0.5 });
export function clearEmberAt(dim, at) { for (const t of EMBER_ENTS) { try { for (const e of dim.getEntities({ type: t, location: at, maxDistance: 0.6 })) e.remove(); } catch {} } }
export const isLit = (b) => { try { return (b.permutation.getState("rzb_dcs:lit") ?? 1) === 1; } catch { return true; } };
export function spawnEmberFor(b) { const c = EMBER_BLOCKS[b.typeId]; if (!c) return; try { b.dimension.spawnEntity(c.ent, emberPos(b)); } catch {} }
// fa37 cauldrons are always on but their emitter can be pushed/removed (e.g. a block placed above). Track positions so the
// sweep can REHEAL a missing emitter. Repopulated on load from survivors.
const CAULDRONS = new Map(); // "dimId,x,y,z" -> { dim, x, y, z }
const cKey = (d, x, y, z) => d.id + "," + x + "," + y + "," + z;
onPlace((ev) => {
const id = ev.block?.typeId; if (!(id in EMBER_BLOCKS)) return;
const b = ev.block; clearEmberAt(b.dimension, emberPos(b));
if (isLit(b)) spawnEmberFor(b); // only a LIT ember block emits
if (id === "rzb_dcs:fa37") CAULDRONS.set(cKey(b.dimension, b.x, b.y, b.z), { dim: b.dimension, x: b.x, y: b.y, z: b.z });
});
onBreak((ev) => {
const id = ev.brokenBlockPermutation?.type?.id; if (!(id in EMBER_BLOCKS)) return;
const b = ev.block; clearEmberAt(b.dimension, emberPos({ x: b.x, y: b.y, z: b.z, typeId: id }));
if (id === "rzb_dcs:fa37") CAULDRONS.delete(cKey(b.dimension, b.x, b.y, b.z));
});
// Sweep orphaned emitters: no ember block beneath (removed by explosion/piston), or an ember block toggled OFF whose
// emitter lingered (e.g. across a reload).
system.runInterval(() => {
for (const name of ["overworld", "nether", "the_end"]) {
let dim; try { dim = world.getDimension(name); } catch { continue; }
for (const t of EMBER_ENTS) {
let ents; try { ents = dim.getEntities({ type: t }); } catch { continue; }
for (const e of ents) { const l = e.location; const b = safeGetBlock(dim, { x: Math.floor(l.x), y: Math.floor(l.y), z: Math.floor(l.z) }); if (b && (!(b.typeId in EMBER_BLOCKS) || !isLit(b))) { try { e.remove(); } catch {} } }
}
}
// HEAL: respawn a cauldron emitter that went missing.
for (const [key, c] of CAULDRONS) {
const b = safeGetBlock(c.dim, { x: c.x, y: c.y, z: c.z });
if (!b) continue; // chunk unloaded -> keep tracked, try later
if (b.typeId !== "rzb_dcs:fa37") { CAULDRONS.delete(key); continue; } // no longer a cauldron
const at = emberPos({ x: c.x, y: c.y, z: c.z, typeId: "rzb_dcs:fa37" });
let has = true; try { has = c.dim.getEntities({ type: CAULDRON_ENT, location: at, maxDistance: 0.6 }).length > 0; } catch {}
if (!has) { try { c.dim.spawnEntity(CAULDRON_ENT, at); } catch {} }
}
}, 40);
// On (re)load, readopt cauldron positions from surviving emitters so the heal knows about preexisting cauldrons.
system.run(() => {
for (const name of ["overworld", "nether", "the_end"]) {
let dim; try { dim = world.getDimension(name); } catch { continue; }
let ents; try { ents = dim.getEntities({ type: CAULDRON_ENT }); } catch { continue; }
for (const e of ents) { const l = e.location, x = Math.floor(l.x), y = Math.floor(l.y), z = Math.floor(l.z); CAULDRONS.set(cKey(dim, x, y, z), { dim, x, y, z }); }
}
});
// Standing in/on a LIT firepit catches the player on fire; the OFF state / bamboo torches do not
system.runInterval(() => {
for (const p of world.getAllPlayers()) {
if (isCreative(p)) continue;
const l = p.location;
let b; try { b = safeGetBlock(p.dimension, { x: Math.floor(l.x), y: Math.floor(l.y), z: Math.floor(l.z) }); } catch { continue; }
if (b && b.typeId === "rzb_dcs:su26" && isLit(b)) { try { p.setOnFire(3, true); } catch {} }
}
}, 10);
+117
View File
@@ -0,0 +1,117 @@
// features/mb_grid.js — grid & canopy multiblocks, defined as data.
import { defineGridMultiblock } from "../core/multiblock.js";
export const FLOWER_CART = defineGridMultiblock({
prefix: "rzb_dcs:fc_", anchorKey: "3_0_0", name: "flower cart",
actionBar: (placed, total) => "§aFlower cart v3: placed " + placed + "/" + total + " cells",
cells: {
"0_0_0": [3, 0, 0], "0_0_1": [3, 0, 1], "1_0_0": [2, 0, 0], "1_0_1": [2, 0, 1], "1_1_0": [2, 1, 0],
"1_1_1": [2, 1, 1], "1_2_0": [2, 2, 0], "1_2_1": [2, 2, 1], "2_0_0": [1, 0, 0], "2_0_1": [1, 0, 1],
"2_1_0": [1, 1, 0], "2_1_1": [1, 1, 1], "2_2_0": [1, 2, 0], "2_2_1": [1, 2, 1], "3_0_0": [0, 0, 0],
"3_0_1": [0, 0, 1], "3_1_0": [0, 1, 0], "4_0_0": [-1, 0, 0], "4_0_1": [-1, 0, 1], "4_1_0": [-1, 1, 0],
"4_1_1": [-1, 1, 1], "5_0_0": [-2, 0, 0], "5_1_0": [-2, 1, 0],
},
});
export const ICE_CREAM_CART = defineGridMultiblock({
prefix: "rzb_dcs:ic_", anchorKey: "1_0_0", name: "ice cream cart",
actionBar: (placed, total) => "§aIce cream cart: placed " + placed + "/" + total + " cells",
cells: { "0_0_0": [-1, 0, 0], "0_1_0": [-1, 1, 0], "1_0_0": [0, 0, 0], "1_1_0": [0, 1, 0], "1_2_0": [0, 2, 0], "2_0_0": [1, 0, 0], "2_1_0": [1, 1, 0], "2_2_0": [1, 2, 0] },
});
export const ANTIQUE_TREE = defineGridMultiblock({
prefix: "rzb_dcs:", anchorKey: "at_1_1_0_1", name: "antique christmas tree", undefinedBlocks: true,
cells: { "at_1_0_0_0": [-1, 0, -1], "at_1_0_0_1": [-1, 0, 0], "at_1_0_0_2": [-1, 0, 1], "at_1_0_1_0": [-1, 1, -1], "at_1_0_1_1": [-1, 1, 0], "at_1_0_1_2": [-1, 1, 1], "at_1_0_2_0": [-1, 2, -1], "at_1_0_2_1": [-1, 2, 0], "at_1_0_2_2": [-1, 2, 1], "at_1_0_3_0": [-1, 3, -1], "at_1_0_3_1": [-1, 3, 0], "at_1_0_3_2": [-1, 3, 1], "at_1_1_0_0": [0, 0, -1], "at_1_1_0_1": [0, 0, 0], "at_1_1_0_2": [0, 0, 1], "at_1_1_1_0": [0, 1, -1], "at_1_1_1_1": [0, 1, 0], "at_1_1_1_2": [0, 1, 1], "at_1_1_2_0": [0, 2, -1], "at_1_1_2_1": [0, 2, 0], "at_1_1_2_2": [0, 2, 1], "at_1_1_3_0": [0, 3, -1], "at_1_1_3_1": [0, 3, 0], "at_1_1_3_2": [0, 3, 1], "at_1_2_0_0": [1, 0, -1], "at_1_2_0_1": [1, 0, 0], "at_1_2_0_2": [1, 0, 1], "at_1_2_1_0": [1, 1, -1], "at_1_2_1_1": [1, 1, 0], "at_1_2_1_2": [1, 1, 1], "at_1_2_2_0": [1, 2, -1], "at_1_2_2_1": [1, 2, 0], "at_1_2_2_2": [1, 2, 1], "at_1_2_3_0": [1, 3, -1], "at_1_2_3_1": [1, 3, 0], "at_1_2_3_2": [1, 3, 1] },
});
export const TENT = defineGridMultiblock({
prefix: "rzb_dcs:tent_", anchorKey: "fr_l", name: "tent", undefinedBlocks: true,
cells: { fr_l: [0, 0, 0], fr_r: [-1, 0, 0], bl: [0, 0, 1], br: [-1, 0, 1] },
});
const lgOK = (nb) => nb && (nb.typeId === "minecraft:air" || nb.typeId === "minecraft:water" || nb.typeId === "minecraft:flowing_water");
export const LOUNGER_COLORS = ["rainbow", "red", "orange", "yellow", "green", "blue", "purple", "pink"]; // index = entity rzb_dcs:color
export const LOUNGER = defineGridMultiblock({
prefix: "rzb_dcs:su03_", anchorKey: "0_0_0", name: "pool lounger", canReplace: lgOK, undefinedBlocks: true, color: true,
cells: { "0_0_0": [0, 0, 0], "0_0_1": [0, 0, 1], "0_0_2": [0, 0, 2] },
});
export const DIVING_BOARD = defineGridMultiblock({
prefix: "rzb_dcs:dv_", anchorKey: "front", name: "diving board", canReplace: lgOK, undefinedBlocks: true,
cells: { front: [0, 0, 0], mid: [0, 0, 1], end: [0, 0, 2] },
});
export const UMBRELLA_BAMBOO = defineGridMultiblock({
prefix: "rzb_dcs:um_", anchorKey: "3_0_3", name: "umbrella", rotating: false, color: true,
cells: {
"0_2_3": [-3, 2, 0], "1_2_1": [-2, 2, -2], "1_2_2": [-2, 2, -1], "1_2_3": [-2, 2, 0], "1_2_4": [-2, 2, 1],
"1_2_5": [-2, 2, 2], "2_2_1": [-1, 2, -2], "2_2_2": [-1, 2, -1], "2_2_3": [-1, 2, 0], "2_2_4": [-1, 2, 1],
"2_2_5": [-1, 2, 2], "3_0_3": [0, 0, 0], "3_1_3": [0, 1, 0], "3_2_0": [0, 2, -3], "3_2_1": [0, 2, -2],
"3_2_2": [0, 2, -1], "3_2_3": [0, 2, 0], "3_2_4": [0, 2, 1], "3_2_5": [0, 2, 2], "3_2_6": [0, 2, 3],
"4_2_1": [1, 2, -2], "4_2_2": [1, 2, -1], "4_2_3": [1, 2, 0], "4_2_4": [1, 2, 1], "4_2_5": [1, 2, 2],
"5_2_1": [2, 2, -2], "5_2_2": [2, 2, -1], "5_2_3": [2, 2, 0], "5_2_4": [2, 2, 1], "5_2_5": [2, 2, 2],
"6_2_3": [3, 2, 0],
},
});
export const UMBRELLA_BEACH = defineGridMultiblock({
prefix: "rzb_dcs:bu_", anchorKey: "3_0_3", name: "umbrella", rotating: false, color: true,
cells: {
"1_2_2": [-2, 2, -1], "1_2_3": [-2, 2, 0], "1_2_4": [-2, 2, 1], "2_2_1": [-1, 2, -2], "2_2_2": [-1, 2, -1],
"2_2_3": [-1, 2, 0], "2_2_4": [-1, 2, 1], "2_2_5": [-1, 2, 2], "3_0_3": [0, 0, 0], "3_1_3": [0, 1, 0],
"3_2_1": [0, 2, -2], "3_2_2": [0, 2, -1], "3_2_3": [0, 2, 0], "3_2_4": [0, 2, 1], "3_2_5": [0, 2, 2],
"4_2_1": [1, 2, -2], "4_2_2": [1, 2, -1], "4_2_3": [1, 2, 0], "4_2_4": [1, 2, 1], "4_2_5": [1, 2, 2],
"5_2_2": [2, 2, -1], "5_2_3": [2, 2, 0], "5_2_4": [2, 2, 1],
},
});
export const BAMBOO_TABLE = defineGridMultiblock({
prefix: "rzb_dcs:bt_", anchorKey: "0_0_0", name: "table", rotating: false, color: true,
cells: { "0_0_0": [0, 0, 0] },
});
export const BT_COLORS = ["yellow", "green"];
export const MANTEL = defineGridMultiblock({
prefix: "rzb_dcs:", anchorKey: "mn_1_1_0_0", name: "christmas mantel", rotating: true, undefinedBlocks: true,
cells: { "mn_1_0_0_0": [-1, 0, 0], "mn_1_0_1_0": [-1, 1, 0], "mn_1_1_0_0": [0, 0, 0], "mn_1_1_1_0": [0, 1, 0], "mn_1_2_0_0": [1, 0, 0], "mn_1_2_1_0": [1, 1, 0], "mn_1_2_2_0": [1, 2, 0] },
});
export const PICNIC = defineGridMultiblock({
prefix: "rzb_dcs:sp38_", anchorKey: "fr_l", name: "picnic table", rotating: true, undefinedBlocks: true,
cells: {
fr_l: [0, 0, 0], fr_r: [-1, 0, 0],
bl: [0, 0, -1], br: [-1, 0, -1],
},
});
export const TRAMPOLINE = defineGridMultiblock({
prefix: "rzb_dcs:tr_", anchorKey: "fl", name: "trampoline", rotating: false, undefinedBlocks: true,
cells: { fl: [0, 0, 0], fr: [-1, 0, 0], bl: [0, 0, 1], br: [-1, 0, 1] },
});
export const KOINO_CHILDREN = defineGridMultiblock({
prefix: "rzb_dcs:sp15_", anchorKey: "pole", name: "koinobori children", rotating: true, undefinedBlocks: true,
cells: { pole: [0, 0, 0], fish: [-1, 0, 0] },
});
export const KOINO_CHILDREN_2 = defineGridMultiblock({ // colour variant 2
prefix: "rzb_dcs:sp15_2_", anchorKey: "pole", name: "koinobori children 2", rotating: true, undefinedBlocks: true,
cells: { pole: [0, 0, 0], fish: [-1, 0, 0] },
});
export const KOINO_PARENTS = defineGridMultiblock({
prefix: "rzb_dcs:sp16_", anchorKey: "pole", name: "koinobori parents", rotating: true, undefinedBlocks: true,
cells: { pole: [0, 0, 0], fish: [-1, 0, 0] },
});
export const KOINO_TOP = defineGridMultiblock({
prefix: "rzb_dcs:sp18_", anchorKey: "pole", name: "koinobori top", rotating: true, undefinedBlocks: true,
cells: { pole: [0, 0, 0], fish: [-1, 0, 0] },
});
export const JACUZZI = defineGridMultiblock({
prefix: "rzb_dcs:jc_", anchorKey: "fr_mid", name: "jacuzzi", rotating: true, undefinedBlocks: true,
cells: {
fr_l: [1, 0, 0], fr_mid: [0, 0, 0], fr_r: [-1, 0, 0],
mid_l: [1, 0, 1], center: [0, 0, 1], mid_r: [-1, 0, 1],
b_l: [1, 0, 2], b_mid: [0, 0, 2], b_r: [-1, 0, 2],
},
});
+335
View File
@@ -0,0 +1,335 @@
// 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 {} } }
}
});
+72
View File
@@ -0,0 +1,72 @@
// features/mb_trees.js — christmas / light / potted trees as multiblocks.
import { BlockPermutation, ItemStack, isCreative, getDir, safeGetBlock } from "../core/mc.js";
import { grid, fcCanReplace } from "../core/grid.js";
import { undoPlace, breakAir } from "../core/fx.js";
import { onPlace, onBreak } from "../core/events.js";
// Rainbow and white are INDEPENDENT block sets (white id = rainbow id + "_w"), each registered as a plain multiblock — no
// rzb_dcs:tc state. Pick-block therefore returns the correct item (and name) natively; brush.js converts between the two.
// --- grid trees (christmas / light): a 3D cell grid placed from the anchor cell ---
function registerGridTree(cfg) { // cfg: ANCHOR, ANCHOR_KEY, CELLS, cellWorld, anchorFrom, parse, label, actionBar?
onPlace((ev) => {
const b = ev.block; if (b?.typeId !== cfg.ANCHOR) return;
const dim = b.dimension, dir = getDir(b.permutation);
for (const c of Object.keys(cfg.CELLS)) { if (c === cfg.ANCHOR_KEY) continue; const nb = safeGetBlock(dim, cfg.cellWorld(c, b.x, b.y, b.z, dir)); if (!fcCanReplace(nb)) { undoPlace(b, ev.player, cfg.ANCHOR, cfg.label); return; } }
let placed = 0;
for (const c of Object.keys(cfg.CELLS)) { if (c === cfg.ANCHOR_KEY) continue; const nb = dim.getBlock(cfg.cellWorld(c, b.x, b.y, b.z, dir)); if (fcCanReplace(nb)) { try { nb.setPermutation(BlockPermutation.resolve("rzb_dcs:" + c, { "minecraft:cardinal_direction": dir })); placed++; } catch {} } }
if (cfg.actionBar) { try { ev.player.onScreenDisplay.setActionBar(cfg.actionBar(placed, Object.keys(cfg.CELLS).length - 1)); } catch {} }
});
onBreak((ev) => {
const cell = cfg.parse(ev.brokenBlockPermutation?.type?.id || ""); if (!cell) return;
const b = ev.block, dir = getDir(ev.brokenBlockPermutation), a = cfg.anchorFrom(cell, b.x, b.y, b.z, dir);
for (const c of Object.keys(cfg.CELLS)) { if (c === cell) continue; const nb = safeGetBlock(b.dimension, cfg.cellWorld(c, a.x, a.y, a.z, dir)); if (nb && cfg.parse(nb.typeId) === c) { try { breakAir(nb); } catch {} } }
if (ev.player && !isCreative(ev.player)) { try { b.dimension.spawnItem(new ItemStack(cfg.ANCHOR, 1), { x: b.x + 0.5, y: b.y + 0.5, z: b.z + 0.5 }); } catch {} }
});
}
// register a grid tree family as BOTH a rainbow set and a "_w" white set, returning the parse/grid helpers of each variant.
function gridTreeFamily(CELLS, anchorKey, label, actionBar) {
const parseIn = (M) => (id) => { if (!id || !id.startsWith("rzb_dcs:")) return null; const c = id.slice(8); return M[c] ? c : null; };
const gR = grid(CELLS, true), parseR = parseIn(CELLS);
registerGridTree({ ANCHOR: "rzb_dcs:" + anchorKey, ANCHOR_KEY: anchorKey, CELLS, cellWorld: gR.cellWorld, anchorFrom: gR.anchorFrom, parse: parseR, label, actionBar });
const CELLS_W = {}; for (const k in CELLS) CELLS_W[k + "_w"] = CELLS[k];
const gW = grid(CELLS_W, true), parseW = parseIn(CELLS_W);
registerGridTree({ ANCHOR: "rzb_dcs:" + anchorKey + "_w", ANCHOR_KEY: anchorKey + "_w", CELLS: CELLS_W, cellWorld: gW.cellWorld, anchorFrom: gW.anchorFrom, parse: parseW, label: label + " (white)", actionBar });
return { CELLS, parse: parseR, cellWorld: gR.cellWorld, anchorFrom: gR.anchorFrom, CELLS_W, parseW, cellWorldW: gW.cellWorld, anchorFromW: gW.anchorFrom };
}
export const CT_CELLS = { "ct_1_1_0_1": [0, 0, 0], "ct_1_1_0_0": [0, 0, -1], "ct_1_0_0_1": [1, 0, 0], "ct_1_1_0_2": [0, 0, 1], "ct_1_2_0_1": [-1, 0, 0], "ct_1_1_1_1": [0, 1, 0], "ct_1_1_2_1": [0, 2, 0], "ct_1_1_3_1": [0, 3, 0], "ct_1_1_1_0": [0, 1, -1], "ct_1_2_1_1": [-1, 1, 0], "ct_1_0_1_1": [1, 1, 0], "ct_1_1_1_2": [0, 1, 1], "ct_1_1_2_0": [0, 2, -1], "ct_1_0_2_1": [1, 2, 0], "ct_1_1_2_2": [0, 2, 1], "ct_1_2_0_0": [-1, 0, -1], "ct_1_0_0_0": [1, 0, -1], "ct_1_0_0_2": [1, 0, 1], "ct_1_2_0_2": [-1, 0, 1], "ct_1_2_2_1": [-1, 2, 0], "ct_1_1_3_2": [0, 3, 1], "ct_1_1_3_0": [0, 3, -1], "ct_1_0_3_1": [1, 3, 0], "ct_1_2_2_2": [-1, 2, 1], "ct_1_2_3_2": [-1, 3, 1], "ct_1_2_2_0": [-1, 2, -1], "ct_1_0_2_2": [1, 2, 1], "ct_1_2_3_0": [-1, 3, -1], "ct_1_0_3_2": [1, 3, 1], "ct_1_2_3_1": [-1, 3, 0], "ct_1_0_2_0": [1, 2, -1], "ct_1_0_3_0": [1, 3, -1], "ct_1_2_1_0": [-1, 1, -1], "ct_1_0_1_2": [1, 1, 1], "ct_1_2_1_2": [-1, 1, 1], "ct_1_0_1_0": [1, 1, -1] };
const CT = gridTreeFamily(CT_CELLS, "ct_1_1_0_1", "christmas tree", (n, tot) => "§aChristmas tree: placed " + n + "/" + tot + " cells");
export const ctParse = CT.parse, ctCellWorld = CT.cellWorld, ctAnchorFrom = CT.anchorFrom;
export const CT_CELLS_W = CT.CELLS_W, ctParseW = CT.parseW, ctCellWorldW = CT.cellWorldW, ctAnchorFromW = CT.anchorFromW;
export const LT_CELLS = { "lt_1_1_2_1": [0, 2, 0], "lt_1_1_1_1": [0, 1, 0], "lt_1_1_0_1": [0, 0, 0], "lt_1_2_0_0": [-1, 0, -1], "lt_1_0_0_2": [1, 0, 1], "lt_1_0_0_0": [1, 0, -1], "lt_1_2_0_2": [-1, 0, 1], "lt_1_0_0_1": [1, 0, 0], "lt_1_2_0_1": [-1, 0, 0], "lt_1_1_0_2": [0, 0, 1], "lt_1_1_0_0": [0, 0, -1] };
const LT = gridTreeFamily(LT_CELLS, "lt_1_1_0_1", "light tree");
export const ltParse = LT.parse, ltCellWorld = LT.cellWorld, ltAnchorFrom = LT.anchorFrom;
export const LT_CELLS_W = LT.CELLS_W, ltParseW = LT.parseW, ltCellWorldW = LT.cellWorldW, ltAnchorFromW = LT.anchorFromW;
// --- potted christmas tree: 2-cell vertical multiblock (pct_b pot/anchor at Y, pct_t tree at Y+1) ---
function registerVerticalTree(anchorId, topId, label) {
onPlace((ev) => {
const b = ev.block; if (b?.typeId !== anchorId) return;
const dim = b.dimension, dir = getDir(b.permutation);
const top = safeGetBlock(dim, { x: b.x, y: b.y + 1, z: b.z });
if (!fcCanReplace(top)) { undoPlace(b, ev.player, anchorId, label); return; }
try { top.setPermutation(BlockPermutation.resolve(topId, { "minecraft:cardinal_direction": dir })); } catch {}
});
onBreak((ev) => {
const id = ev.brokenBlockPermutation?.type?.id || ""; if (id !== anchorId && id !== topId) return;
const b = ev.block;
const sibY = id === anchorId ? b.y + 1 : b.y - 1, want = id === anchorId ? topId : anchorId;
const nb = safeGetBlock(b.dimension, { x: b.x, y: sibY, z: b.z });
if (nb && nb.typeId === want) { try { breakAir(nb); } catch {} }
if (ev.player && !isCreative(ev.player)) { try { b.dimension.spawnItem(new ItemStack(anchorId, 1), { x: b.x + 0.5, y: b.y + 0.5, z: b.z + 0.5 }); } catch {} }
});
}
export const PCT_ANCHOR = "rzb_dcs:pct_b", PCT_TOP = "rzb_dcs:pct_t", PCT_ANCHOR_W = "rzb_dcs:pct_b_w", PCT_TOP_W = "rzb_dcs:pct_t_w";
export const pctParse = (id) => (id === PCT_ANCHOR || id === PCT_TOP) ? id.slice(8) : null;
export const pctParseW = (id) => (id === PCT_ANCHOR_W || id === PCT_TOP_W) ? id.slice(8) : null;
registerVerticalTree(PCT_ANCHOR, PCT_TOP, "potted christmas tree");
registerVerticalTree(PCT_ANCHOR_W, PCT_TOP_W, "potted christmas tree (white)");
+490
View File
@@ -0,0 +1,490 @@
// features/rideables.js — block <-> rideable-entity vehicles (tube, flamingo, lounger, bike, surfboard, basket bike, sled).
// Parks as a block; a ride_* interact spawns a steerable entity; riderless it reverts (color + heading kept).
// Water craft steer via applyImpulse + no-Y setRotation (engine keeps float); land bikes velocity-drive with gravity.
import { world, system, BlockPermutation, ItemStack, isCreative, getDir, safeGetBlock } from "../core/mc.js";
import { RIGHT, FWD, TUBE_DIR_YAW, tubeYawToDir, tubeWaterTop, tubeOverWater, loungerSupportY, fcCanReplace } from "../core/grid.js";
import { BLOCK_TOP, BLOCK_PASSABLE } from "../core/block_heights.js";
import { undoPlace, breakAir } from "../core/fx.js";
import { registerVariants } from "../core/variants.js";
import { onPlace, onBreak } from "../core/events.js";
import { LOUNGER, LOUNGER_COLORS } from "./mb_grid.js";
const R = (dir) => RIGHT[dir] || RIGHT.south, F = (dir) => FWD[dir] || FWD.south;
// ---------- shared 2-cell "grid vehicle" scaffolding (surfboard-lay, basket bike, sled; lounger reuses placeGridBlocks) ----------
// cellWorld/anchorFrom rotate a cell offset by facing; parse maps a block id -> {cell, color} via the PREFIX map.
function makeCellHelpers(CELLS, PREFIX) {
const cellWorld = (cell, ax, ay, az, dir) => { const g = CELLS[cell], r = R(dir), f = F(dir); return { x: ax + g[0] * r[0] + g[2] * f[0], y: ay + g[1], z: az + g[0] * r[2] + g[2] * f[2] }; };
const anchorFrom = (cell, x, y, z, dir) => { const g = CELLS[cell], r = R(dir), f = F(dir); return { x: x - g[0] * r[0] - g[2] * f[0], y: y - g[1], z: z - g[0] * r[2] - g[2] * f[2] }; };
const parse = (id) => { if (!id) return null; for (const cell in PREFIX) { const px = PREFIX[cell]; if (id.startsWith(px)) return { cell, color: id.slice(px.length) }; } return null; };
return { cellWorld, anchorFrom, parse };
}
// Place a rig's cells iff every target block is replaceable (surfboard also requires water directly below each). blockFor(cell)->id.
function placeGridBlocks(dim, ax, ay, az, dir, CELLS, cellWorld, blockFor, requireWaterBelow = false) {
const cells = Object.keys(CELLS).map((c) => ({ c, p: cellWorld(c, ax, ay, az, dir) }));
for (const { p } of cells) {
const b = safeGetBlock(dim, p); if (!b || !(b.isAir || b.isLiquid)) return false;
if (requireWaterBelow) { const below = safeGetBlock(dim, { x: p.x, y: p.y - 1, z: p.z }); if (!below || !(below.typeId || "").endsWith("water")) return false; }
}
for (const { c, p } of cells) { const b = safeGetBlock(dim, p); try { b.setPermutation(BlockPermutation.resolve(blockFor(c), { "minecraft:cardinal_direction": dir })); } catch {} }
return true;
}
// Anchor + one-forward rig (basket bike, sled): placing the anchor auto-builds the front cell; breaking either removes the pair.
function registerFrontPairRig(BASE, FRONT_PREFIX, CELLS, cellWorld, anchorFrom, parse, label) {
onPlace((ev) => {
const b = ev.block; if (!b.typeId.startsWith(BASE)) return;
const color = b.typeId.slice(BASE.length), dim = b.dimension, dir = getDir(b.permutation), fb = safeGetBlock(dim, cellWorld("0_0_1", b.x, b.y, b.z, dir));
if (!fb || !fcCanReplace(fb)) { undoPlace(b, ev.player, BASE + color, label); return; }
try { fb.setPermutation(BlockPermutation.resolve(FRONT_PREFIX + color, { "minecraft:cardinal_direction": dir })); } catch {}
});
onBreak((ev) => {
const sp = parse(ev.brokenBlockPermutation?.type?.id || ""); if (!sp) return;
const b = ev.block, dim = b.dimension, dir = getDir(ev.brokenBlockPermutation), a = anchorFrom(sp.cell, b.x, b.y, b.z, dir);
for (const c of Object.keys(CELLS)) { if (c === sp.cell) continue; const p = cellWorld(c, a.x, a.y, a.z, dir); const nb = safeGetBlock(dim, p); if (nb && parse(nb.typeId)?.cell === c) { try { breakAir(nb); } catch {} } }
});
}
// Board a 2-cell rig: air its cells, spawn its entity at the rig midpoint (+half a cell forward), seat the rider. cfg.spawnYaw
// (dir, player) gives the spawn heading (per vehicle); cfg.waterGate requires water (surfboard). Fallback: re-place the blocks.
function board2Cell(player, block, cfg) {
const p = cfg.parse(block.typeId); if (!p) return false;
if (cfg.waterGate && !tubeOverWater(block)) return false;
const dim = block.dimension, dir = getDir(block.permutation), color = p.color, a = cfg.anchorFrom(p.cell, block.x, block.y, block.z, dir);
for (const c of Object.keys(cfg.cells)) { const cp = cfg.cellWorld(c, a.x, a.y, a.z, dir); const bb = safeGetBlock(dim, cp); if (bb && cfg.parse(bb.typeId)) { try { bb.setType("minecraft:air"); } catch {} } }
const f = F(dir), loc = { x: a.x + 0.5 + f[0] * 0.5, y: a.y, z: a.z + 0.5 + f[2] * 0.5 };
let ent; try { ent = dim.spawnEntity(cfg.entity, loc); } catch {}
if (!ent) { cfg.place(dim, a.x, a.y, a.z, dir, color); return true; }
try { ent.setProperty("rzb_dcs:color", Math.max(0, cfg.colors.indexOf(color))); } catch {}
try { ent.setDynamicProperty("spawn", system.currentTick); } catch {}
try { ent.teleport(loc, { dimension: dim, rotation: { x: 0, y: cfg.spawnYaw(dir, player) } }); } catch {}
try { ent.getComponent("minecraft:rideable")?.addRider(player); } catch {}
return true;
}
// Riderless revert of a 2-cell rig: re-place the parked blocks (or drop the anchor item). cfg.revertDir(yaw) = block facing;
// cfg.surfaceSnap floats the parked blocks to the water surface (surfboard), else they revert at the entity's floor.
function revert2Cell(t, dim, cfg) {
let yaw = 0; try { yaw = t.getRotation().y; } catch {}
const dir = cfg.revertDir(yaw), f = F(dir);
let ci = 0; try { ci = t.getProperty("rzb_dcs:color") ?? 0; } catch {}
const color = cfg.colors[ci] ?? cfg.colors[0];
const l = t.location, ax = Math.floor(l.x - 0.5 * f[0]), az = Math.floor(l.z - 0.5 * f[2]);
let my = Math.floor(l.y);
if (cfg.surfaceSnap) { const topW = tubeWaterTop(dim, Math.floor(l.x), Math.floor(l.z), l.y); my = topW !== null ? topW + 1 : Math.floor(l.y); }
try { t.remove(); } catch {}
if (!cfg.place(dim, ax, my, az, dir, color)) { try { dim.spawnItem(new ItemStack(cfg.base + color, 1), { x: l.x, y: my + 1, z: l.z }); } catch {} }
}
const TUBE_ENTITY = "rzb_dcs:pool_tube";
// Special (non-colour) pool tubes: each has its OWN model/entity, NO colour property, and a facing offset (the directional
// models render 180 off the round tube). Add one here plus its assets and it's fully wired.
const TUBE_SPECIAL = { flamingo: { entity: "rzb_dcs:pool_tube_flamingo", yawOff: 180 }, unicorn: { entity: "rzb_dcs:pool_tube_unicorn", yawOff: 180 } };
const TUBE_SPECIAL_BY_ENT = {}; for (const name in TUBE_SPECIAL) TUBE_SPECIAL_BY_ENT[TUBE_SPECIAL[name].entity] = { name, ...TUBE_SPECIAL[name] };
const TUBE_COLORS = ["black", "blue", "cyan", "green", "light_blue", "lime", "magenta", "orange", "pink", "purple", "red", "yellow"];
const TUBE_SPEED = 0.18;
registerVariants(["rzb_dcs:su02_black", "rzb_dcs:su02_orange", "rzb_dcs:su02_magenta", "rzb_dcs:su02_light_blue", "rzb_dcs:su02_yellow", "rzb_dcs:su02_lime", "rzb_dcs:su02_pink", "rzb_dcs:su02_cyan", "rzb_dcs:su02_purple", "rzb_dcs:su02_blue", "rzb_dcs:su02_green", "rzb_dcs:su02_red"]);
function boardTube(player, block) {
const m = block.typeId.match(/^rzb_dcs:su02_(.+)$/); if (!m) return false;
const special = TUBE_SPECIAL[m[1]];
const ci = special ? -1 : TUBE_COLORS.indexOf(m[1]); if (!special && ci < 0) return false;
if (!tubeOverWater(block)) return false; // on land the tube stays a plain block
const dim = block.dimension, dir = getDir(block.permutation), loc = { x: block.x + 0.5, y: block.y, z: block.z + 0.5 };
try { block.setType("minecraft:air"); } catch {}
let ent; try { ent = dim.spawnEntity(special ? special.entity : TUBE_ENTITY, loc); } catch {}
if (!ent) { try { block.setPermutation(BlockPermutation.resolve("rzb_dcs:su02_" + m[1], { "minecraft:cardinal_direction": dir })); } catch {} return true; }
if (!special) { try { ent.setProperty("rzb_dcs:color", ci); } catch {} }
try { ent.setDynamicProperty("spawn", system.currentTick); } catch {}
try { ent.teleport(loc, { dimension: dim, rotation: { x: 0, y: (TUBE_DIR_YAW[dir] ?? 0) + (special ? special.yawOff : 0) } }); } catch {}
try { ent.getComponent("minecraft:rideable")?.addRider(player); } catch {}
return true;
}
function revertTube(t, dim) { // handles the coloured tube AND the special tubes (flamingo/unicorn: no colour property)
const sp = TUBE_SPECIAL_BY_ENT[t.typeId];
let color = sp ? sp.name : "blue", yawOff = sp ? sp.yawOff : 0;
if (!sp) { let ci = 0; try { ci = t.getProperty("rzb_dcs:color") ?? 0; } catch {} color = TUBE_COLORS[ci] ?? "blue"; }
let yaw = 0; try { yaw = t.getRotation().y; } catch {}
const l = t.location, wx = Math.floor(l.x), wz = Math.floor(l.z);
const topW = tubeWaterTop(dim, wx, wz, l.y);
const pos = { x: wx, y: topW !== null ? topW + 1 : Math.floor(l.y), z: wz };
try { t.remove(); } catch {}
const perm = () => BlockPermutation.resolve("rzb_dcs:su02_" + color, { "minecraft:cardinal_direction": tubeYawToDir(yaw + yawOff) });
const b = safeGetBlock(dim, pos);
if (b && (b.isAir || b.isLiquid)) { try { b.setPermutation(perm()); } catch {} }
else if (b) {
const up = safeGetBlock(dim, { x: pos.x, y: pos.y + 1, z: pos.z });
if (up && (up.isAir || up.isLiquid)) { try { up.setPermutation(perm()); } catch {} }
else { try { dim.spawnItem(new ItemStack("rzb_dcs:su02_" + color, 1), { x: pos.x + 0.5, y: pos.y + 1, z: pos.z + 0.5 }); } catch {} }
}
}
const LOUNGER_ENTITY = "rzb_dcs:pool_lounger";
const LOUNGER_SPEED = 0.16;
const LG_BASE = LOUNGER.prefix, LG_ANCHOR = LOUNGER.anchorKey;
const placeLoungerBlocks = (dim, ax, ay, az, dir, color) => placeGridBlocks(dim, ax, ay, az, dir, LOUNGER.cells, LOUNGER.cellWorld, (c) => LOUNGER.idFor(c, color));
// Shared steering basis for every vehicle. Travel dir = flattened look; near-vertical look jitters (divide by ~0), so
// require fl > 0.2 else HOLD heading from entity yaw minus yawOff (caller's setRotation re-adds it). `look` gates re-aiming.
function steerForward(p, ent, yawOff = 0) {
const v = p.getViewDirection(), fl = Math.hypot(v.x, v.z), look = fl > 0.2;
if (look) return { fx: v.x / fl, fz: v.z / fl, look: true };
let y0 = 0; try { y0 = ent.getRotation().y; } catch {}
const r = (y0 - yawOff) * Math.PI / 180;
return { fx: -Math.sin(r), fz: Math.cos(r), look: false };
}
// Every buoyant water craft (tube, flamingo, lounger, surfboard) steers identically: impulse horizontal velocity toward
// look+WASD * speed (coast when idle), face the travel heading. Only the pose anim, speed, yaw offset, and turn style
// vary — smoothTurn>0 lerps the body toward heading (the round tube), 0 snaps instantly (directional lounger/surfboard).
function driveWaterCraft(p, boat, anim, speed, yawOff = 0, smoothTurn = 0) {
try { p.playAnimation(anim, { blendOutTime: 0, stopExpression: "!q.is_riding" }); } catch {}
let cur; try { cur = boat.getVelocity(); } catch { cur = { x: 0, y: 0, z: 0 }; }
let mv; try { mv = p.inputInfo.getMovementVector(); } catch { mv = null; }
const { fx, fz, look } = steerForward(p, boat, yawOff);
const rx = fz, rz = -fx;
if (mv && (mv.x !== 0 || mv.y !== 0)) { const dx = (fx * mv.y + rx * mv.x) * speed, dz = (fz * mv.y + rz * mv.x) * speed; try { boat.applyImpulse({ x: dx - cur.x, y: 0, z: dz - cur.z }); } catch {} }
else { try { boat.applyImpulse({ x: -cur.x * 0.25, y: 0, z: -cur.z * 0.25 }); } catch {} }
if (look) { const target = -Math.atan2(fx, fz) * 180 / Math.PI + yawOff;
try { if (smoothTurn > 0) { let cy = 0; try { cy = boat.getRotation().y; } catch {} const d = ((target - cy + 540) % 360) - 180; boat.setRotation({ x: 0, y: cy + d * smoothTurn }); } else boat.setRotation({ x: 0, y: target }); } catch {} }
}
function driveLounger(p, boat) { driveWaterCraft(p, boat, "animation.rzb_dcs.ride_lounger", LOUNGER_SPEED); }
function driveTube(p, boat) { driveWaterCraft(p, boat, "animation.rzb_dcs.ride_pool_tube", TUBE_SPEED, 0, 0.12); }
function boardLounger(player, block) {
const pr = LOUNGER.parse(block.typeId); if (!pr) return false; const cell = pr.cell, color = pr.color;
if (!tubeOverWater(block)) return false; // on land the lounger stays furniture (generic sit handles it)
const dim = block.dimension, dir = getDir(block.permutation), a = LOUNGER.anchorFrom(cell, block.x, block.y, block.z, dir);
for (const c of Object.keys(LOUNGER.cells)) { const p = LOUNGER.cellWorld(c, a.x, a.y, a.z, dir); const b = safeGetBlock(dim, p); if (b && LOUNGER.parse(b.typeId)) { try { b.setType("minecraft:air"); } catch {} } }
const mid = LOUNGER.cellWorld("0_0_1", a.x, a.y, a.z, dir), loc = { x: mid.x + 0.5, y: mid.y, z: mid.z + 0.5 };
let ent; try { ent = dim.spawnEntity(LOUNGER_ENTITY, loc); } catch {}
if (!ent) { placeLoungerBlocks(dim, a.x, a.y, a.z, dir, color); return true; }
try { ent.setProperty("rzb_dcs:color", Math.max(0, LOUNGER_COLORS.indexOf(color))); } catch {} // carry the colour onto the entity
try { ent.setDynamicProperty("spawn", system.currentTick); } catch {}
try { ent.teleport(loc, { dimension: dim, rotation: { x: 0, y: TUBE_DIR_YAW[dir] ?? 0 } }); } catch {}
try { ent.getComponent("minecraft:rideable")?.addRider(player); } catch {}
return true;
}
function revertLounger(t, dim) {
let yaw = 0; try { yaw = t.getRotation().y; } catch {}
let ci = 0; try { ci = t.getProperty("rzb_dcs:color") ?? 0; } catch {} const color = LOUNGER_COLORS[ci] || "rainbow";
const dir = tubeYawToDir(yaw + 180);
const l = t.location, wx = Math.floor(l.x), wz = Math.floor(l.z), topW = tubeWaterTop(dim, wx, wz, l.y);
const my = topW !== null ? topW + 1 : Math.floor(l.y), a = LOUNGER.anchorFrom("0_0_1", wx, my, wz, dir);
try { t.remove(); } catch {}
if (!placeLoungerBlocks(dim, a.x, a.y, a.z, dir, color)) { try { dim.spawnItem(new ItemStack(LOUNGER.idFor(LG_ANCHOR, color), 1), { x: wx + 0.5, y: my + 1, z: wz + 0.5 }); } catch {} }
}
const BIKE_ENTITY = "rzb_dcs:mountain_bike";
const BIKE_COLORS = ["black", "blue", "green", "purple", "red", "silver"]; // entity color-index map (tied to rzb_dcs:color); do NOT reorder
registerVariants(["black", "purple", "blue", "green", "red", "silver"].map((c) => "rzb_dcs:su29_" + c)); // brush cycle order (canonical), decoupled from the index map
const BIKE_MAX = 0.42, BIKE_ACCEL = 0.02, BIKE_DECEL = 0.03, BIKE_STRAFE = 0.14, BIKE_ROLL = 0.12, BIKE_STEP = 1.05, BIKE_WHEEL = 0.62;
const BIKE_HOP = 0.38, BIKE_IMPULSE_MAX = 1.6; // hop impulse to climb a 1-block step; per-tick impulse clamp
const BIKE_SNOW_FACTOR = 0.5; // bikes are sluggish on snow/ice (opposite of the sled)
function boardBike(player, block) {
const m = block.typeId.match(/^rzb_dcs:su29_(.+)$/); if (!m) return false;
const ci = BIKE_COLORS.indexOf(m[1]); if (ci < 0) return false;
const dim = block.dimension, dir = getDir(block.permutation), loc = { x: block.x + 0.5, y: block.y, z: block.z + 0.5 };
try { block.setType("minecraft:air"); } catch {}
let ent; try { ent = dim.spawnEntity(BIKE_ENTITY, loc); } catch {}
if (!ent) { try { block.setPermutation(BlockPermutation.resolve("rzb_dcs:su29_" + m[1], { "minecraft:cardinal_direction": dir })); } catch {} return true; }
try { ent.setProperty("rzb_dcs:color", ci); } catch {}
try { ent.setDynamicProperty("spawn", system.currentTick); } catch {}
let pyaw = TUBE_DIR_YAW[dir] ?? 0; try { pyaw = player.getRotation().y; } catch {}
try { ent.teleport(loc, { dimension: dim, rotation: { x: 0, y: pyaw } }); } catch {}
try { ent.getComponent("minecraft:rideable")?.addRider(player); } catch {}
return true;
}
function breakBikeInWater(bike, dim, at) {
let ci = 0; try { ci = bike.getProperty("rzb_dcs:color") ?? 0; } catch {}
let item = null;
if (bike.typeId === BIKE_ENTITY) item = "rzb_dcs:su29_" + (BIKE_COLORS[ci] || BIKE_COLORS[0]);
else if (bike.typeId === BB_ENTITY) item = "rzb_dcs:su31_b_" + (BB_COLORS[ci] || BB_COLORS[0]);
if (item) { try { dim.spawnItem(new ItemStack(item, 1), at); } catch {} }
try { bike.remove(); } catch {}
}
function bikeInWater(dim, x, y, z) {
const isW = (b) => { try { return !!b && b.typeId.endsWith("water"); } catch { return false; } };
return isW(safeGetBlock(dim, { x: Math.floor(x), y: Math.floor(y), z: Math.floor(z) })) || isW(safeGetBlock(dim, { x: Math.floor(x), y: Math.floor(y) + 1, z: Math.floor(z) }));
}
function blockCollisionTop(b) {
try {
const id = b.typeId || "";
if (id.includes("slab") && !id.includes("double")) { let h = ""; try { h = b.permutation.getState("minecraft:vertical_half"); } catch {} return h === "top" ? 1 : 0.5; }
if (id.endsWith("snow_layer")) { let n = 0; try { n = b.permutation.getState("height"); } catch {} return (typeof n === "number" ? n + 1 : 1) * 0.125; }
if (id.endsWith("carpet")) return 0.0625;
} catch {}
return 1;
}
function groundSupportY(dim, x, z, fromY) {
const bx = Math.floor(x), bz = Math.floor(z);
for (let y = Math.floor(fromY) + 1; y >= Math.floor(fromY) - 48; y--) {
let b = null; try { b = dim.getBlock({ x: bx, y, z: bz }); } catch {}
if (!b) continue;
const id = b.typeId || "";
if (/water|lava/.test(id)) continue;
let air = id === "minecraft:air"; try { air = b.isAir; } catch {}
if (air || BLOCK_PASSABLE.has(id)) continue; // air or a no-collision decoration -> ride through
const top = BLOCK_TOP[id]; // pack partial-collision block -> baked real top
return y + (top !== undefined ? top : blockCollisionTop(b));
}
return null;
}
function solidCol(dim, x, y, z) {
let b = null; try { b = dim.getBlock({ x: Math.floor(x), y: Math.floor(y), z: Math.floor(z) }); } catch {}
if (!b) return false;
const id = b.typeId || "";
if (/water|lava/.test(id)) return false;
let air = id === "minecraft:air"; try { air = b.isAir; } catch {}
return !air && !BLOCK_PASSABLE.has(id);
}
function frontStep(dim, loc, dx, dz) {
const ax = loc.x + dx * 0.6, az = loc.z + dz * 0.6;
return solidCol(dim, ax, loc.y + 0.1, az) && !solidCol(dim, ax, loc.y + 1.1, az);
}
// Shared land-vehicle velocity step (bike + sled are byte-identical here bar the strafe const and the water-break fn):
// steer horizontal velocity toward (fx,fz)*spd + strafe, hop a 1-block step, break if it entered water. Returns the
// pre-impulse velocity facts the caller's tail needs (nearGround, gspd) + broke (caller returns early when true).
function landImpulse(veh, dim, loc, fx, fz, rx, rz, ix, spd, strafe, breakFn) {
let vel = { x: 0, y: 0, z: 0 }; try { vel = veh.getVelocity(); } catch {}
const nearGround = vel.y > -0.3 && vel.y < 0.12; // ~resting/rolling
const desX = fx * spd + rx * ix * strafe, desZ = fz * spd + rz * ix * strafe;
let jx = desX - vel.x, jz = desZ - vel.z; // impulse to reach target horizontal velocity (compensates ground drag)
const jm = Math.hypot(jx, jz); if (jm > BIKE_IMPULSE_MAX) { jx = jx / jm * BIKE_IMPULSE_MAX; jz = jz / jm * BIKE_IMPULSE_MAX; }
const sg = spd < 0 ? -1 : 1;
const hop = (nearGround && Math.abs(spd) > 0.05 && frontStep(dim, loc, fx * sg, fz * sg)) ? BIKE_HOP : 0;
try { veh.applyImpulse({ x: jx, y: hop, z: jz }); } catch {}
let broke = false;
if (bikeInWater(dim, loc.x, loc.y, loc.z)) { breakFn(veh, dim, { x: loc.x, y: loc.y + 0.5, z: loc.z }); broke = true; }
return { nearGround, gspd: Math.hypot(vel.x, vel.z) * sg, broke };
}
// VELOCITY DRIVE: engine owns Y (gravity), collision, and carries the rider. We steer only horizontal velocity via
// applyImpulse + face with setRotation — NO teleport (teleport pins position and kills gravity).
function driveBike(p, bike) {
const dim = bike.dimension, loc = bike.location;
let spd0 = 0; try { spd0 = bike.getDynamicProperty("spd") ?? 0; } catch {}
const bikeBase = bike.typeId === BB_ENTITY ? "animation.rzb_dcs.ride_basket_bike" : "animation.rzb_dcs.ride_bike";
const bikeAnim = Math.abs(spd0) > 0.02 ? bikeBase + "_pedal" : bikeBase;
try { p.playAnimation(bikeAnim, { blendOutTime: 0, stopExpression: "!q.is_riding" }); } catch {}
const { fx, fz, look: hasLook } = steerForward(p, bike); // hasLook gates the steering ramp below
const rx = fz, rz = -fx;
let mv; try { mv = p.inputInfo.getMovementVector(); } catch { mv = null; }
const ix = mv ? mv.x : 0, iy = mv ? mv.y : 0;
let spd = 0; try { spd = bike.getDynamicProperty("spd") ?? 0; } catch {}
const below = safeGetBlock(dim, { x: Math.floor(loc.x), y: Math.floor(loc.y) - 1, z: Math.floor(loc.z) });
const slow = isSnow(below?.typeId) || isSand(below?.typeId); // snow/ice + sand/gravel = loose/slick
const bmax = slow ? BIKE_MAX * BIKE_SNOW_FACTOR : BIKE_MAX;
if (iy > 0.1) spd = Math.min(spd + BIKE_ACCEL, bmax);
else if (iy < -0.1) spd = Math.max(spd - BIKE_ACCEL * 1.5, -bmax * 0.35);
else spd = spd > 0 ? Math.max(spd - BIKE_DECEL, 0) : Math.min(spd + BIKE_DECEL, 0);
if (spd > bmax) spd = Math.max(spd - BIKE_DECEL, bmax); // ease down when crossing onto slow ground at speed
try { bike.setDynamicProperty("spd", spd); } catch {}
const { nearGround, gspd, broke } = landImpulse(bike, dim, loc, fx, fz, rx, rz, ix, spd, BIKE_STRAFE, breakBikeInWater);
if (broke) return;
let roll = 0; try { roll = bike.getProperty("rzb_dcs:roll") ?? 0; } catch {}
roll = (roll + gspd * BIKE_ROLL) % 1; if (roll < 0) roll += 1;
try { bike.setProperty("rzb_dcs:roll", roll); } catch {}
let yaw = 0; try { yaw = -Math.atan2(fx, fz) * 180 / Math.PI; } catch {}
{ let cur = 0; try { cur = bike.getRotation().y; } catch {} const d = hasLook ? ((yaw - cur + 540) % 360) - 180 : 0; const target = Math.max(-1, Math.min(1, d * 0.16));
let st = 0; try { st = bike.getDynamicProperty("str") ?? 0; } catch {} st += (target - st) * 0.18; if (Math.abs(st) < 0.015) st = 0;
try { bike.setDynamicProperty("str", st); } catch {} try { bike.setProperty("rzb_dcs:steer", st); } catch {} }
try { bike.setRotation({ x: 0, y: yaw }); } catch {}
if (nearGround && Math.abs(spd) >= bmax - 0.05 && Math.abs(gspd) > 0.03 && (system.currentTick & 1) === 0) { // spd = commanded top speed (reliable); gspd>0.03 = actually rolling (no dust vs a wall)
const gy = loc.y + 0.02;
try { dim.spawnParticle("rzb_dcs:bike_dust", { x: loc.x + fx * BIKE_WHEEL, y: gy, z: loc.z + fz * BIKE_WHEEL }); } catch {}
try { dim.spawnParticle("rzb_dcs:bike_dust", { x: loc.x - fx * BIKE_WHEEL, y: gy, z: loc.z - fz * BIKE_WHEEL }); } catch {}
}
}
function revertBike(t, dim) {
let ci = 0; try { ci = t.getProperty("rzb_dcs:color") ?? 0; } catch {}
const color = BIKE_COLORS[ci] ?? "black";
let yaw = 0; try { yaw = t.getRotation().y; } catch {}
const dir = tubeYawToDir(yaw + 180);
const l = t.location, wx = Math.floor(l.x), wy = Math.floor(l.y), wz = Math.floor(l.z);
try { t.remove(); } catch {}
const perm = () => BlockPermutation.resolve("rzb_dcs:su29_" + color, { "minecraft:cardinal_direction": dir });
const b = safeGetBlock(dim, { x: wx, y: wy, z: wz });
if (b && (b.isAir || b.isLiquid)) { try { b.setPermutation(perm()); } catch {} }
else { const up = safeGetBlock(dim, { x: wx, y: wy + 1, z: wz }); if (up && (up.isAir || up.isLiquid)) { try { up.setPermutation(perm()); } catch {} } else { try { dim.spawnItem(new ItemStack("rzb_dcs:su29_" + color, 1), { x: wx + 0.5, y: wy + 1, z: wz + 0.5 }); } catch {} } }
}
export const SB_BASE = "rzb_dcs:su30_b_", SB_TOP = "rzb_dcs:su30_t_", SB_ENTITY = "rzb_dcs:surfboard";
export const SB_LB = "rzb_dcs:su30_lb_", SB_LF = "rzb_dcs:su30_lf_";
export const SB_COLORS = ["blue", "cyan", "green", "light_blue", "lime", "magenta", "orange", "pink", "purple", "red", "yellow"]; // entity color-index map; do NOT reorder
export const SB_ORDER = ["orange", "magenta", "light_blue", "yellow", "lime", "pink", "cyan", "purple", "blue", "green", "red"]; // brush cycle order (canonical), decoupled from the index map
const SURF_SPEED = 0.17, SURF_YAW_OFF = 0;
registerVariants([...SB_COLORS.map((c) => SB_BASE + c), ...SB_COLORS.map((c) => SB_TOP + c)]);
registerVariants([...SB_COLORS.map((c) => SB_LB + c), ...SB_COLORS.map((c) => SB_LF + c)]);
export const SB_LAY_CELLS = { "0_0_0": [0, 0, 0], "0_0_1": [0, 0, 1] };
export const SB_LAY_PREFIX = { "0_0_0": SB_LB, "0_0_1": SB_LF };
const _sbH = makeCellHelpers(SB_LAY_CELLS, SB_LAY_PREFIX);
export const sbLayCellWorld = _sbH.cellWorld, sbLayAnchorFrom = _sbH.anchorFrom, sbLayParse = _sbH.parse;
const placeSurfboardBlocks = (dim, ax, ay, az, dir, color) => placeGridBlocks(dim, ax, ay, az, dir, SB_LAY_CELLS, sbLayCellWorld, (c) => SB_LAY_PREFIX[c] + color, true);
const _surfRecent = new Set();
function markSurf(pid) { if (pid) { _surfRecent.add(pid); system.runTimeout(() => _surfRecent.delete(pid), 4); } }
onPlace((ev) => {
const b = ev.block; if (!b.typeId.startsWith(SB_BASE)) return;
const color = b.typeId.slice(SB_BASE.length), dim = b.dimension, dir = getDir(b.permutation);
if (tubeOverWater(b)) {
const wx = b.x, wz = b.z, f = F(dir), fx = wx + f[0], fz = wz + f[2];
const isWater = (x, y, z) => (safeGetBlock(dim, { x, y, z })?.typeId || "").endsWith("water");
const clear = (bl) => bl && (bl.isAir || bl.isLiquid);
let ay = b.y;
for (let i = 0; i < 8 && isWater(fx, ay, fz); i++) ay++;
const c1 = ay === b.y ? b : safeGetBlock(dim, { x: wx, y: ay, z: wz });
const c2 = safeGetBlock(dim, { x: fx, y: ay, z: fz });
if (c1 && (c1 === b || clear(c1)) && clear(c2)) {
if (ay !== b.y) { try { b.setType(b.y < ay ? "minecraft:water" : "minecraft:air"); } catch {} }
try { c2.setPermutation(BlockPermutation.resolve(SB_LF + color, { "minecraft:cardinal_direction": dir })); } catch {}
try { c1.setPermutation(BlockPermutation.resolve(SB_LB + color, { "minecraft:cardinal_direction": dir })); } catch {}
markSurf(ev.player?.id);
} else {
undoPlace(b, ev.player, SB_BASE + color, "surfboard");
}
return;
}
const ab = safeGetBlock(dim, { x: b.x, y: b.y + 1, z: b.z });
if (!ab || !fcCanReplace(ab)) { undoPlace(b, ev.player, SB_BASE + color, "surfboard"); return; }
try { ab.setPermutation(BlockPermutation.resolve(SB_TOP + color, { "minecraft:cardinal_direction": dir })); } catch {}
});
world.afterEvents.itemUse?.subscribe((ev) => {
const p = ev.source, item = ev.itemStack; if (!item?.typeId?.startsWith(SB_BASE) || !p) return;
let hit; try { hit = p.getBlockFromViewDirection({ includeLiquidBlocks: true, maxDistance: 6 }); } catch {}
if (!hit?.block || !/water/.test(hit.block.typeId)) return;
const color = item.typeId.slice(SB_BASE.length), dim = p.dimension, wx = hit.block.x, wz = hit.block.z, pid = p.id;
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; }
let yaw = 0; try { yaw = p.getRotation().y; } catch {}
const dir = tubeYawToDir(yaw);
system.run(() => {
if (_surfRecent.has(pid)) return;
markSurf(pid);
if (placeSurfboardBlocks(dim, wx, wy + 1, wz, dir, color)) {
try { if (!isCreative(p)) { const inv = p.getComponent("minecraft:inventory")?.container, i = p.selectedSlotIndex, it = inv?.getItem(i); if (it && it.typeId?.startsWith(SB_BASE)) { if (it.amount > 1) { it.amount -= 1; inv.setItem(i, it); } else inv.setItem(i, undefined); } } } catch {}
}
});
});
onBreak((ev) => {
const id = ev.brokenBlockPermutation?.type?.id || "", b = ev.block, dim = b.dimension;
if (id.startsWith(SB_BASE)) { const nb = dim.getBlock({ x: b.x, y: b.y + 1, z: b.z }); if (nb && nb.typeId === SB_TOP + id.slice(SB_BASE.length)) { try { breakAir(nb); } catch {} } }
else if (id.startsWith(SB_TOP)) { const nb = dim.getBlock({ x: b.x, y: b.y - 1, z: b.z }); if (nb && nb.typeId === SB_BASE + id.slice(SB_TOP.length)) { try { breakAir(nb); } catch {} } }
else { const lp = sbLayParse(id); if (lp) { const dir = getDir(ev.brokenBlockPermutation), a = sbLayAnchorFrom(lp.cell, b.x, b.y, b.z, dir); for (const c of Object.keys(SB_LAY_CELLS)) { if (c === lp.cell) continue; const p = sbLayCellWorld(c, a.x, a.y, a.z, dir); const nb = safeGetBlock(dim, p); if (nb && sbLayParse(nb.typeId)?.cell === c) { try { breakAir(nb); } catch {} } } } }
});
const SURF_RIG = { parse: sbLayParse, cells: SB_LAY_CELLS, cellWorld: sbLayCellWorld, anchorFrom: sbLayAnchorFrom, entity: SB_ENTITY, colors: SB_COLORS, base: SB_BASE, place: placeSurfboardBlocks, waterGate: true, surfaceSnap: true, spawnYaw: (dir) => (TUBE_DIR_YAW[dir] ?? 0) + SURF_YAW_OFF, revertDir: (yaw) => tubeYawToDir(yaw - SURF_YAW_OFF) };
const boardSurfboard = (player, block) => board2Cell(player, block, SURF_RIG);
const revertSurfboard = (t, dim) => revert2Cell(t, dim, SURF_RIG);
function driveSurfboard(p, boat) { driveWaterCraft(p, boat, "animation.rzb_dcs.ride_surfboard", SURF_SPEED, SURF_YAW_OFF); }
const BB_ENTITY = "rzb_dcs:basket_bike";
export const BB_B = "rzb_dcs:su31_b_", BB_FR = "rzb_dcs:su31_fr_";
export const BB_COLORS = ["cyan", "green", "lavender", "light_blue", "mint", "orange", "pink", "red", "white", "yellow"]; // entity color-index map; do NOT reorder
export const BB_ORDER = ["white", "orange", "light_blue", "yellow", "pink", "cyan", "green", "red", "lavender", "mint"]; // brush cycle order (canonical); decoupled from the index map
const BB_YAW_OFF = 180;
registerVariants([...BB_COLORS.map((c) => BB_B + c), ...BB_COLORS.map((c) => BB_FR + c)]);
export const BB_CELLS = { "0_0_0": [0, 0, 0], "0_0_1": [0, 0, -1] };
export const BB_PREFIX = { "0_0_0": BB_B, "0_0_1": BB_FR };
const _bbH = makeCellHelpers(BB_CELLS, BB_PREFIX);
export const bbCellWorld = _bbH.cellWorld, bbAnchorFrom = _bbH.anchorFrom, bbParse = _bbH.parse;
const placeBasketBikeBlocks = (dim, ax, ay, az, dir, color) => placeGridBlocks(dim, ax, ay, az, dir, BB_CELLS, bbCellWorld, (c) => BB_PREFIX[c] + color);
registerFrontPairRig(BB_B, BB_FR, BB_CELLS, bbCellWorld, bbAnchorFrom, bbParse, "basket bike");
const BB_RIG = { parse: bbParse, cells: BB_CELLS, cellWorld: bbCellWorld, anchorFrom: bbAnchorFrom, entity: BB_ENTITY, colors: BB_COLORS, base: BB_B, place: placeBasketBikeBlocks, spawnYaw: (dir, player) => { let y = (TUBE_DIR_YAW[dir] ?? 0) + BB_YAW_OFF; try { y = player.getRotation().y; } catch {} return y; }, revertDir: (yaw) => tubeYawToDir(yaw - BB_YAW_OFF + 180) };
const boardBasketBike = (player, block) => board2Cell(player, block, BB_RIG);
const revertBasketBike = (t, dim) => revert2Cell(t, dim, BB_RIG);
const SLED_ENTITY = "rzb_dcs:sled";
export const SLED_B = "rzb_dcs:wi41_b_", SLED_FR = "rzb_dcs:wi41_fr_";
const SLED_COLORS = ["red", "green", "blue", "yellow"]; // entity color index — do NOT reorder
export const SLED_ORDER = ["yellow", "blue", "green", "red"]; // brush cycle order (canonical); decoupled from the index map
export const SLED_CELLS = { "0_0_0": [0, 0, 0], "0_0_1": [0, 0, 1] }; // anchor(back) + front, one block forward (+FWD)
export const SLED_PREFIX = { "0_0_0": SLED_B, "0_0_1": SLED_FR };
const SLED_MAX_SNOW = 0.5, SLED_MAX_SLOW = 0.16, SLED_ACCEL = 0.03, SLED_DECEL = 0.03, SLED_STRAFE = 0.1, SLED_STEP = 1.05;
const SLED_YAW_OFF = 180; // sled drives "backwards" -> flip the model 180 so its front points the way of travel
const isSnow = (id) => !!id && /snow|ice|powder_snow/.test(id);
const isSand = (id) => !!id && /sand|gravel/.test(id); // loose ground — bikes bog down like on snow
registerVariants([...SLED_COLORS.map((c) => SLED_B + c), ...SLED_COLORS.map((c) => SLED_FR + c)]); // brush gate; cycling uses SLED_ORDER
const _slH = makeCellHelpers(SLED_CELLS, SLED_PREFIX);
export const sledCellWorld = _slH.cellWorld, sledAnchorFrom = _slH.anchorFrom, sledParse = _slH.parse;
const placeSledBlocks = (dim, ax, ay, az, dir, color) => placeGridBlocks(dim, ax, ay, az, dir, SLED_CELLS, sledCellWorld, (c) => SLED_PREFIX[c] + color);
registerFrontPairRig(SLED_B, SLED_FR, SLED_CELLS, sledCellWorld, sledAnchorFrom, sledParse, "sled");
const SLED_RIG = { parse: sledParse, cells: SLED_CELLS, cellWorld: sledCellWorld, anchorFrom: sledAnchorFrom, entity: SLED_ENTITY, colors: SLED_COLORS, base: SLED_B, place: placeSledBlocks, spawnYaw: (dir, player) => { let y = (TUBE_DIR_YAW[dir] ?? 0) + SLED_YAW_OFF; try { y = player.getRotation().y + SLED_YAW_OFF; } catch {} return y; }, revertDir: (yaw) => tubeYawToDir(yaw + 180) };
const boardSled = (player, block) => board2Cell(player, block, SLED_RIG);
const revertSled = (t, dim) => revert2Cell(t, dim, SLED_RIG);
function breakSledInWater(sled, dim, at) {
let ci = 0; try { ci = sled.getProperty("rzb_dcs:color") ?? 0; } catch {}
try { dim.spawnItem(new ItemStack(SLED_B + (SLED_COLORS[ci] || SLED_COLORS[0]), 1), at); } catch {}
try { sled.remove(); } catch {}
}
// Land drive like the bike, but max speed depends on the block under the sled: fast on snow/ice, sluggish elsewhere.
function driveSled(p, sled) {
const dim = sled.dimension, loc = sled.location;
try { p.playAnimation("animation.rzb_dcs.ride_sled", { blendOutTime: 0, stopExpression: "!q.is_riding" }); } catch {}
const { fx, fz } = steerForward(p, sled, SLED_YAW_OFF); // holds heading (minus the 180 offset) when look is near-vertical
const rx = fz, rz = -fx;
let mv; try { mv = p.inputInfo.getMovementVector(); } catch { mv = null; }
const ix = mv ? mv.x : 0, iy = mv ? mv.y : 0;
const below = safeGetBlock(dim, { x: Math.floor(loc.x), y: Math.floor(loc.y) - 1, z: Math.floor(loc.z) });
const onSnow = isSnow(below?.typeId), maxSpd = onSnow ? SLED_MAX_SNOW : SLED_MAX_SLOW;
let spd = 0; try { spd = sled.getDynamicProperty("spd") ?? 0; } catch {}
if (iy > 0.1) spd = Math.min(spd + SLED_ACCEL, maxSpd);
else if (iy < -0.1) spd = Math.max(spd - SLED_ACCEL * 1.5, -maxSpd * 0.35);
else spd = spd > 0 ? Math.max(spd - SLED_DECEL, 0) : Math.min(spd + SLED_DECEL, 0);
if (spd > maxSpd) spd = maxSpd; else if (spd < -maxSpd * 0.35) spd = -maxSpd * 0.35; // clamp when crossing onto slow terrain at speed
try { sled.setDynamicProperty("spd", spd); } catch {}
const { nearGround, gspd, broke } = landImpulse(sled, dim, loc, fx, fz, rx, rz, ix, spd, SLED_STRAFE, breakSledInWater);
if (broke) return;
let yaw = SLED_YAW_OFF; try { yaw = -Math.atan2(fx, fz) * 180 / Math.PI + SLED_YAW_OFF; } catch {}
try { sled.setRotation({ x: 0, y: yaw }); } catch {}
if (nearGround && Math.abs(spd) >= maxSpd - 0.03 && Math.abs(gspd) > 0.03 && onSnow && (system.currentTick & 1) === 0) { try { dim.spawnParticle("rzb_dcs:sled_snow", { x: loc.x, y: loc.y + 0.02, z: loc.z }); } catch {} } // snow spray at top speed (spd = commanded, reliable)
}
// Consolidated riderless-revert loop. Scans only dimensions that currently have players (a vehicle only exists where
// someone rode it). Each type's body is its revertX() fn; the rider + spawn-age gate is shared.
const REVERTERS = [
[TUBE_ENTITY, revertTube], ...Object.values(TUBE_SPECIAL).map((s) => [s.entity, revertTube]), [LOUNGER_ENTITY, revertLounger], [BIKE_ENTITY, revertBike],
[SB_ENTITY, revertSurfboard], [BB_ENTITY, revertBasketBike], [SLED_ENTITY, revertSled],
];
system.runInterval(() => {
const dims = new Map();
for (const p of world.getAllPlayers()) { try { dims.set(p.dimension.id, p.dimension); } catch {} }
for (const dim of dims.values()) {
for (const [type, revert] of REVERTERS) {
let ents; try { ents = dim.getEntities({ type }); } catch { continue; }
for (const t of ents) {
let riders; try { riders = t.getComponent("minecraft:rideable")?.getRiders() ?? []; } catch { riders = []; }
if (riders.length > 0) continue;
let spawn = 0; try { spawn = t.getDynamicProperty("spawn") ?? 0; } catch {}
if (system.currentTick - spawn < 20) continue;
try { revert(t, dim); } catch {}
}
}
}
}, 10);
// Master drive loop dispatch pertick steering by the entity a player is riding.
system.runInterval(() => {
for (const p of world.getAllPlayers()) {
let boat; try { boat = p.getComponent("minecraft:riding")?.entityRidingOn; } catch {}
if (!boat) continue;
if (boat.typeId === LOUNGER_ENTITY) { driveLounger(p, boat); continue; }
if (boat.typeId === BIKE_ENTITY) { driveBike(p, boat); continue; }
if (boat.typeId === SB_ENTITY) { driveSurfboard(p, boat); continue; }
if (boat.typeId === BB_ENTITY) { driveBike(p, boat); continue; }
if (boat.typeId === SLED_ENTITY) { driveSled(p, boat); continue; }
if (boat.typeId === TUBE_ENTITY || TUBE_SPECIAL_BY_ENT[boat.typeId]) { driveTube(p, boat); continue; }
}
}, 1);
system.beforeEvents.startup.subscribe((init) => {
const reg = init.blockComponentRegistry;
reg.registerCustomComponent("rzb_dcs:ride_tube", { onPlayerInteract(e) { boardTube(e.player, e.block); } });
reg.registerCustomComponent("rzb_dcs:ride_lounger", { onPlayerInteract(e) { boardLounger(e.player, e.block); } });
reg.registerCustomComponent("rzb_dcs:ride_bike", { onPlayerInteract(e) { boardBike(e.player, e.block); } });
reg.registerCustomComponent("rzb_dcs:ride_surfboard", { onPlayerInteract(e) { boardSurfboard(e.player, e.block); } });
reg.registerCustomComponent("rzb_dcs:ride_basket_bike", { onPlayerInteract(e) { boardBasketBike(e.player, e.block); } });
reg.registerCustomComponent("rzb_dcs:ride_sled", { onPlayerInteract(e) { boardSled(e.player, e.block); } });
});
+213
View File
@@ -0,0 +1,213 @@
// features/seating.js — the sit system (seat entity + per-furniture config).
import { world, system, isCreative, getDir, safeGetBlock } from "../core/mc.js";
import { DIR_YAW, DIR_TROT, FWD } from "../core/grid.js";
import { FLOWER_CART, LOUNGER } from "./mb_grid.js";
import { BR_FRONT, BR_BACK, BR_BACK_OFF, LO_FRONT, LO_BACK, LO_BACK_OFF } from "./mb_pairs.js";
import { onBreak } from "../core/events.js";
const SEAT = "rzb_dcs:seat";
const LAY_ANIM = "animation.rzb_dcs.lay_towel", LAY_TAG = "rzb_dcs:laying"; // lay-down pose for the towel front cell
const LOUNGE_ANIM = "animation.rzb_dcs.lounge_sit", LOUNGE_TAG = "rzb_dcs:lounging"; // reclined-25deg pose for the lounge chair
const TENT_LAY_ANIM = "animation.rzb_dcs.lay_tent", TENT_LAY_TAG = "rzb_dcs:laying_tent"; // COPY of lay_towel, kept SEPARATE so it can be customized without touching the towel
const TENT_SIT_ANIM = "animation.rzb_dcs.sit_tent", TENT_SIT_TAG = "rzb_dcs:sitting_tent"; // root scale only (0.82), composed ON TOP of the engine sit so the player still sits at 82% size
const CASKET_ANIM = "animation.rzb_dcs.casket_lay", CASKET_TAG = "rzb_dcs:laying_casket"; // laying casket pose (custom lay-down + player scale, authored in casket_lay.json)
const SITTABLE = {
};
//jacuzzi sit inset
const JC_SEAT = {
"rzb_dcs:jc_b_mid": { y: 0.325, fwd: -0.5, side: 0, face: 180 },
"rzb_dcs:jc_fr_mid": { y: 0.325, fwd: 0.5, side: 0, face: 0 },
"rzb_dcs:jc_mid_l": { y: 0.325, fwd: 0, side: -0.5, face: 90 },
"rzb_dcs:jc_mid_r": { y: 0.325, fwd: 0, side: 0.5, face: -90 },
"rzb_dcs:jc_b_l": { y: 0.325, fwd: -0.35, side: -0.35, face: 135 },
"rzb_dcs:jc_b_r": { y: 0.325, fwd: -0.35, side: 0.35, face: -135 },
"rzb_dcs:jc_fr_l": { y: 0.325, fwd: 0.35, side: -0.35, face: 45 },
"rzb_dcs:jc_fr_r": { y: 0.325, fwd: 0.35, side: 0.35, face: -45 },
};
// Picnic table
const PICNIC_SEAT = {
"rzb_dcs:sp38_fr_l": { y: 0.3125, fwd: 0.5125, side: 0.3, face: 90 }, // renders sp38_bl geo (down 3 + back 5; in toward center)
"rzb_dcs:sp38_bl": { y: 0.3125, fwd: -0.5125, side: 0.3, face: 90 }, // renders sp38_fr_l geo
"rzb_dcs:sp38_fr_r": { y: 0.3125, fwd: 0.5125, side: -0.3, face: -90 }, // renders sp38_br geo
"rzb_dcs:sp38_br": { y: 0.3125, fwd: -0.5125, side: -0.3, face: -90 }, // renders sp38_fr_r geo
};
function seatCfg(typeId) {
if (SITTABLE[typeId]) return SITTABLE[typeId];
if (JC_SEAT[typeId]) return JC_SEAT[typeId];
if (PICNIC_SEAT[typeId]) return PICNIC_SEAT[typeId];
if (typeId.startsWith("rzb_dcs:sp31_bl_") || typeId.startsWith("rzb_dcs:sp31_br_")) return { y: 0.3, fwd: 0, side: 0 };
if (typeId.startsWith("rzb_dcs:sp32_b_")) return { y: 0.2375, fwd: 0.125, side: 0 }; // rose rope swing seat
if (typeId === "rzb_dcs:sp30") return { y: -0.1, fwd: 0, side: 0 }; // picnic blanket (sit on the ground)
if (typeId.startsWith("rzb_dcs:tw_fr_")) return { y: -0.1, fwd: -0.8, side: 0, lay: true }; // beach towel FRONT cell -> lay down (shifted back)
if (typeId === "rzb_dcs:tent_fr_l") return { y: -0.0875, fwd: 0.125, side: -0.5, pose: { anim: TENT_SIT_ANIM, tag: TENT_SIT_TAG } }; // tent FRONT-left cell -> SIT scaled 82%
if (typeId === "rzb_dcs:tent_bl") return { y: -0.1625, fwd: -0.625, side: -0.3125, lay: true, sleep: true, layAnim: TENT_LAY_ANIM, layTag: TENT_LAY_TAG }; // tent BACK-left cell -> lay/sleep (acts as a BED at night)
if (typeId === "rzb_dcs:fa48") return { y: 0.1, fwd: 0.2, side: 0, lay: true, layAnim: CASKET_ANIM, layTag: CASKET_TAG }; // casket ground HEAD -> lay (centre -0.3 forward)
if (typeId === "rzb_dcs:fa48_b") return { y: 0.1, fwd: -0.8, side: 0, lay: true, layAnim: CASKET_ANIM, layTag: CASKET_TAG }; // casket ground FOOT -> lay (centre -0.3 forward)
if (typeId.startsWith("rzb_dcs:tw_b_")) return { y: -0.1625, fwd: 0, side: 0 }; // beach towel BACK cell -> sit (down 1px)
if (typeId.startsWith("rzb_dcs:lo_fr_")) return { y: 0.3, fwd: -0.25, side: 0, lounge: true }; // lounge chair FRONT cell (dead fallback; resolves to back)
if (typeId.startsWith("rzb_dcs:lo_b_")) return { y: 0.3, fwd: 0.29167, side: 0, lounge: true }; // lounge chair BACK cell (live seat; +fwd = back)
if (typeId.startsWith("rzb_dcs:lc_b_")) return { y: 0.875, fwd: 0, side: 0 }; // lifeguard chair base (elevated seat)
if (typeId.startsWith("rzb_dcs:lc_a_")) return { y: -0.125, fwd: 0, side: 0 }; // lifeguard awning: sit at the SAME spot as the base
if (typeId === "rzb_dcs:su03_0_0_1") return { y: 0.0, fwd: 0, side: 0 }; // pool lounger seat (middle cell)
if (typeId.startsWith("rzb_dcs:su02_")) return { y: 0.0, fwd: 0, side: 0 }; // pool tube: sit on LAND (in water it boards a boat)
if (typeId.startsWith("rzb_dcs:su08_")) return { y: 0.3375, fwd: 0, side: 0 }; // bamboo armchair
if (typeId.startsWith("rzb_dcs:su17_")) return { y: 0.4, fwd: 0, side: 0 }; // bamboo chair
if (typeId.startsWith("rzb_dcs:su23_")) return { y: 0.35, fwd: 0, side: 0 }; // camping chair
if (typeId.startsWith("rzb_dcs:su35_")) return { y: 0.3, fwd: 0, side: 0 }; // beach chair
if (typeId.startsWith("rzb_dcs:su22_")) return { y: 0.3, fwd: 0, side: 0 }; // beach stool
if (typeId.startsWith("rzb_dcs:br_b_")) return { y: 0.65, fwd: -0.78, side: 0, face: 0, bikeRack: true }; // bike rack: 2 saddles
if (typeId === "rzb_dcs:su20") return { y: -0.1, fwd: 0, side: 0 }; // inflatable pool
if (typeId.startsWith("rzb_dcs:su09_") || typeId.startsWith("rzb_dcs:su10_") || typeId.startsWith("rzb_dcs:su11_")) return { y: 0.3375, fwd: 0, side: 0 }; // bamboo couch
if (/^rzb_dcs:sp(39|40|41|42)_/.test(typeId)) return { y: 0.275, fwd: 0, side: 0 }; // spring couch set (armchair + couch L/M/R); lowered 1px from bamboo's 0.3375
if (/^rzb_dcs:fa(41|42|43|44)_/.test(typeId)) return { y: 0.275, fwd: 0, side: 0 }; // fall couch set (armchair + couch L/M/R), same seat as spring
if (/^rzb_dcs:wi(45|46|47|48)_/.test(typeId)) return { y: 0.275, fwd: 0, side: 0 }; // winter couch set (armchair + couch L/M/R)
if (typeId === "rzb_dcs:fc_3_0_0") return { y: 0.8375, fwd: 0.46, side: -0.2, face: 90 }; // flower cart bike saddle
return undefined;
}
// guard so the two interaction paths don't spawn two seats for one click
const recent = new Set();
// Active lay/lounge seats (seatId -> { seat, anim }); the per-tick pose loop re-fires only these instead of scanning entities.
const poseSeats = new Map();
// Tent SIT seats (seatId -> { seat, pid, dim, fx,fy,fz }); on dismount the rider is placed at a safe spot in FRONT of the tent.
const tentSit = new Map();
function safeFront(dim, x, y, z) {
for (const dy of [0, 1, -1, 2]) {
const feet = safeGetBlock(dim, { x, y: y + dy, z }), head = safeGetBlock(dim, { x, y: y + dy + 1, z });
if (feet && head && (feet.isAir || feet.isLiquid) && (head.isAir || head.isLiquid)) return { x: x + 0.5, y: y + dy, z: z + 0.5 };
}
return null;
}
export function sit(player, block) {
if (!player || !block) return;
let dir = getDir(block.permutation);
let typeId = block.typeId, bx = block.x, by = block.y, bz = block.z;
const fcCell = FLOWER_CART.parse(typeId);
if (fcCell) { const a = FLOWER_CART.anchorFrom(fcCell, bx, by, bz, dir); bx = a.x; by = a.y; bz = a.z; typeId = "rzb_dcs:fc_3_0_0"; }
const lgCell = LOUNGER.parse(typeId);
if (lgCell) { const a = LOUNGER.anchorFrom(lgCell, bx, by, bz, dir); const s = LOUNGER.cellWorld("0_0_1", a.x, a.y, a.z, dir); bx = s.x; by = s.y; bz = s.z; typeId = "rzb_dcs:su03_0_0_1"; }
if (typeId.startsWith(BR_FRONT)) { const off = BR_BACK_OFF[dir] || [0, 0, 1]; bx += off[0]; by += off[1]; bz += off[2]; typeId = BR_BACK + typeId.slice(BR_FRONT.length); }
if (typeId.startsWith(LO_FRONT)) { const off = LO_BACK_OFF[dir] || [0, 0, -1]; bx += off[0]; by += off[1]; bz += off[2]; typeId = LO_BACK + typeId.slice(LO_FRONT.length); }
const cfg = seatCfg(typeId);
if (cfg === undefined) return;
if (cfg.bikeRack) { // two saddles: pick left (-X) or right (+X) bike by which side the player is on
const t = (DIR_TROT[dir] ?? 0) * Math.PI / 180, c = Math.cos(t), s = Math.sin(t);
const wx = player.location.x - (bx + 0.5), wz = player.location.z - (bz + 0.5);
cfg.side = (wx * c - wz * s) < 0 ? -5 / 16 : 4 / 16;
}
if (player.isSneaking) return;
if (recent.has(player.id)) return;
recent.add(player.id);
system.runTimeout(() => recent.delete(player.id), 5);
const yaw = (cfg.face !== undefined) ? (cfg.face - (DIR_TROT[dir] ?? 0)) : (DIR_YAW[dir] ?? 0);
const trot = (DIR_TROT[dir] ?? 0) * Math.PI / 180, ct = Math.cos(trot), st = Math.sin(trot);
const fwd = cfg.fwd ?? 0, side = cfg.side ?? 0;
const dx = side * ct + fwd * st, dz = -side * st + fwd * ct;
const dim = block.dimension;
const seatPos = { x: bx + 0.5 + dx, y: by + (cfg.y ?? 0.4), z: bz + 0.5 + dz };
const near = dim.getEntities({ type: SEAT, location: seatPos, maxDistance: 0.6 });
for (const s of near) { const rd = s.getComponent("minecraft:rideable"); if (rd && rd.getRiders().length > 0) return; s.remove(); }
const seat = dim.spawnEntity(SEAT, seatPos);
try { seat.setRotation({ x: 0, y: yaw }); } catch {}
const rideable = seat.getComponent("minecraft:rideable");
if (!rideable) { seat.remove(); return; }
rideable.addRider(player);
if (cfg.lay) { const tag = cfg.layTag || LAY_TAG, anim = cfg.layAnim || LAY_ANIM; try { seat.addTag(tag); } catch {} try { player.playAnimation(anim, { blendOutTime: 0, stopExpression: "!q.is_riding" }); } catch {} try { poseSeats.set(seat.id, { seat, anim }); } catch {} }
if (cfg.lounge) { try { seat.addTag(LOUNGE_TAG); } catch {} try { player.playAnimation(LOUNGE_ANIM, { blendOutTime: 0, stopExpression: "!q.is_riding" }); } catch {} try { poseSeats.set(seat.id, { seat, anim: LOUNGE_ANIM }); } catch {} }
if (cfg.pose) { try { seat.addTag(cfg.pose.tag); } catch {} try { player.playAnimation(cfg.pose.anim, { blendOutTime: 0, stopExpression: "!q.is_riding" }); } catch {} try { poseSeats.set(seat.id, { seat, anim: cfg.pose.anim }); } catch {} } // generic custom pose (e.g. tent scaled sit)
if (cfg.sleep) {
try { const f = FWD[dir] || FWD.south, sp = safeFront(dim, bx - 2 * f[0], by, bz - 2 * f[2]) || { x: bx - 2 * f[0] + 0.5, y: by, z: bz - 2 * f[2] + 0.5 }; player.setSpawnPoint({ dimension: dim, x: sp.x, y: sp.y, z: sp.z }); } catch {}
sleepSkip(seat, player, dim, bx, by, bz, dir);
}
if (typeId === "rzb_dcs:tent_fr_l") { const f = FWD[dir] || FWD.south; tentSit.set(seat.id, { seat, pid: player.id, dim, fx: bx - f[0], fy: by, fz: bz - f[2] }); } // sit dismount -> teleport to a safe spot in FRONT of the tent
}
system.runInterval(() => {
for (const [sid, info] of tentSit) {
let riders; try { riders = info.seat.getComponent("minecraft:rideable")?.getRiders(); } catch { tentSit.delete(sid); continue; } // seat already gone
if (riders && riders.length) continue; // still seated
const p = world.getAllPlayers().find((pl) => pl.id === info.pid);
if (p) { const pos = safeFront(info.dim, info.fx, info.fy, info.fz); if (pos) { try { p.teleport(pos, { dimension: info.dim }); } catch {} } }
try { info.seat.remove(); } catch {}
tentSit.delete(sid); poseSeats.delete(sid);
}
}, 1);
function sleepSkip(seat, player, dim, bx, by, bz, dir) {
system.runTimeout(() => {
try {
const riders = seat.getComponent("minecraft:rideable")?.getRiders() || [];
if (!riders.some((r) => r?.id === player.id)) return; // got up before falling asleep
const t = world.getTimeOfDay();
if (t < 12542 || t > 23458) return; // daytime -> don't skip, keep lying there
try { world.setTimeOfDay(0); } catch {}
try { seat.remove(); } catch {} // end the lay pose
const f = FWD[dir] || FWD.south, sitB = safeGetBlock(dim, { x: bx - f[0], y: by, z: bz - f[2] }); // the paired sit cell
// sit cell FREE -> sit up there; OCCUPIED -> stand up OFF the tent (behind the bed).
let occupied = false;
try { for (const e of dim.getEntities({ type: SEAT, location: { x: bx - f[0] + 0.5, y: by + 0.5, z: bz - f[2] + 0.5 }, maxDistance: 1.4 })) { if ((e.getComponent("minecraft:rideable")?.getRiders() || []).length) { occupied = true; break; } } } catch {}
if (sitB && sitB.typeId === "rzb_dcs:tent_fr_l" && !occupied) system.runTimeout(() => { try { sit(player, sitB); } catch {} }, 2); // sit up (delay so the lay seat is gone)
else system.runTimeout(() => { const pos = safeFront(dim, bx - 2 * f[0], by, bz - 2 * f[2]); if (pos) { try { player.teleport(pos, { dimension: dim }); } catch {} } }, 2); // sit cell taken -> stand up in FRONT of the tent
} catch {}
}, 60);
}
system.beforeEvents.startup.subscribe((init) => {
init.blockComponentRegistry.registerCustomComponent("rzb_dcs:sit", { onPlayerInteract(e) { sit(e.player, e.block); } });
});
onBreak((ev) => {
const id = ev.brokenBlockPermutation?.type?.id || "";
if (!/^rzb_dcs:tent_(fr_l|fr_r|bl|br)$/.test(id)) return;
const b = ev.block;
for (const p of world.getAllPlayers()) {
try {
const sp = p.getSpawnPoint();
if (sp && Math.abs(Math.floor(sp.x) - b.x) <= 3 && Math.abs(Math.floor(sp.z) - b.z) <= 3 && Math.abs(Math.floor(sp.y) - b.y) <= 2) { try { p.setSpawnPoint(undefined); } catch {} } // spawn sits ~1-3 blocks in FRONT of the tent -> wide footprint tolerance
} catch {}
}
});
// Despawn empty seats (player stood up, logged off, etc.).
system.runInterval(() => {
for (const name of ["overworld", "nether", "the_end"]) {
let dim; try { dim = world.getDimension(name); } catch { continue; }
for (const seat of dim.getEntities({ type: SEAT })) {
const rideable = seat.getComponent("minecraft:rideable");
const riders = rideable ? rideable.getRiders() : [];
if (!riders || riders.length === 0) seat.remove();
}
}
}, 20);
const SEAT_POSES = [{ tag: LAY_TAG, anim: LAY_ANIM }, { tag: LOUNGE_TAG, anim: LOUNGE_ANIM }, { tag: TENT_LAY_TAG, anim: TENT_LAY_ANIM }, { tag: TENT_SIT_TAG, anim: TENT_SIT_ANIM }, { tag: CASKET_TAG, anim: CASKET_ANIM }];
// Re-register any tagged pose-seat that isn't tracked (reload, or a transient 0-rider tick dropped it from poseSeats).
// Runs on load AND periodically, so a lost pose self-heals instead of falling back to the vanilla sit forever.
function syncPoseSeats() {
for (const name of ["overworld", "nether", "the_end"]) {
let dim; try { dim = world.getDimension(name); } catch { continue; }
for (const pose of SEAT_POSES) {
let seats; try { seats = dim.getEntities({ type: SEAT, tags: [pose.tag] }); } catch { continue; }
for (const seat of seats) { if (!poseSeats.has(seat.id)) { try { poseSeats.set(seat.id, { seat, anim: pose.anim }); } catch {} } }
}
}
}
system.run(syncPoseSeats);
system.runInterval(syncPoseSeats, 20);
system.runInterval(() => {
if (poseSeats.size === 0) return;
for (const [id, e] of poseSeats) {
if (!e.seat || !e.seat.isValid) { poseSeats.delete(id); continue; } // only stop tracking once the seat is actually removed (despawn loop)
let riders; try { riders = e.seat.getComponent("minecraft:rideable")?.getRiders() ?? []; } catch { continue; } // transient read hiccup: keep the pose, retry next tick (don't drop -> that was the 1-tick vanilla-sit flash)
for (const r of riders) { try { r.playAnimation(e.anim, { blendOutTime: 0, stopExpression: "!q.is_riding" }); } catch {} }
}
}, 1);
+240
View File
@@ -0,0 +1,240 @@
// features/toggles.js; block <-> animated-entity toggles (buoy, sprinkler, spinner birds).
import { world, system, BlockPermutation, ItemStack, isCreative, getDir, safeGetBlock } from "../core/mc.js";
import { DISPLAY_DIR_YAW, displayYawToDir, TUBE_DIR_YAW, tubeYawToDir, fcCanReplace } from "../core/grid.js";
import { summonDisplay, breakFx, toggleDebounced, brushFx, particleFor } from "../core/fx.js";
import { BO_BOTTOM, BO_TOP, boCanReplace } from "./mb_pairs.js";
import { registerVariants } from "../core/variants.js";
import { onPlace, onBreak } from "../core/events.js";
import { KOINO_CHILDREN, KOINO_CHILDREN_2, KOINO_PARENTS, KOINO_TOP } from "./mb_grid.js";
const heldItem = (e) => { try { return e.getComponent("minecraft:inventory")?.container?.getItem(e.selectedSlotIndex)?.typeId; } catch { return undefined; } };
const BRUSH_ENT_SEQ = {}; // (legacy) brushable toggle ENTITY id -> its ordered variant list
function brushEntRecolor(t, player) { // swap animated entity to next variant, same spot/yaw
const seq = BRUSH_ENT_SEQ[t.typeId]; if (!seq) return false;
const next = seq[(seq.indexOf(t.typeId) + 1) % seq.length];
const dim = t.dimension, l = t.location; let yaw = 0; try { yaw = t.getRotation().y; } catch {}
try { t.remove(); } catch {}
summonDisplay(dim, next, l.x, l.y, l.z, yaw);
try { brushFx(dim, { x: Math.floor(l.x), y: Math.floor(l.y), z: Math.floor(l.z) }, particleFor(next), player); } catch {}
return true;
}
const BUOY_ENTITY = "rzb_dcs:buoy";
function buoyBlockToEntity(block) {
if (block.typeId !== BO_BOTTOM && block.typeId !== BO_TOP) return false;
const dim = block.dimension, bx = block.x, bz = block.z, by = (block.typeId === BO_TOP) ? block.y - 1 : block.y;
if (toggleDebounced(bx, by, bz)) return true;
const bottom = safeGetBlock(dim, { x: bx, y: by, z: bz }), top = safeGetBlock(dim, { x: bx, y: by + 1, z: bz });
try { if (bottom && bottom.typeId === BO_BOTTOM) bottom.setType("minecraft:air"); } catch {}
try { if (top && top.typeId === BO_TOP) top.setType("minecraft:air"); } catch {}
try { dim.spawnEntity(BUOY_ENTITY, { x: bx + 0.5, y: by, z: bz + 0.5 }); } catch {}
return true;
}
function buoyEntityToBlock(ent) {
const dim = ent.dimension, l = ent.location, bx = Math.floor(l.x), by = Math.floor(l.y), bz = Math.floor(l.z);
if (toggleDebounced(bx, by, bz)) return;
const bb = safeGetBlock(dim, { x: bx, y: by, z: bz }), tt = safeGetBlock(dim, { x: bx, y: by + 1, z: bz });
if (boCanReplace(bb) && boCanReplace(tt)) { try { bb.setPermutation(BlockPermutation.resolve(BO_BOTTOM)); tt.setPermutation(BlockPermutation.resolve(BO_TOP)); ent.remove(); } catch {} }
else { try { ent.remove(); } catch {} try { dim.spawnItem(new ItemStack(BO_BOTTOM, 1), { x: bx + 0.5, y: by + 0.5, z: bz + 0.5 }); } catch {} }
}
const BOBBING_BLOCK = "rzb_dcs:fa38", BOBBING_ENT = "rzb_dcs:bobbing_apples";
function bobbingBlockToEntity(block) {
if (block?.typeId !== BOBBING_BLOCK) return false;
const dim = block.dimension, x = block.x, y = block.y, z = block.z;
if (toggleDebounced(x, y, z)) return true;
try { block.setType("minecraft:air"); } catch {}
try { dim.spawnEntity(BOBBING_ENT, { x: x + 0.5, y, z: z + 0.5 }); } catch {}
return true;
}
function bobbingEntityToBlock(ent) {
const dim = ent.dimension, l = ent.location, bx = Math.floor(l.x), by = Math.floor(l.y), bz = Math.floor(l.z);
if (toggleDebounced(bx, by, bz)) return;
const nb = safeGetBlock(dim, { x: bx, y: by, z: bz });
if (nb && (nb.isAir || nb.isLiquid || fcCanReplace(nb))) { try { nb.setPermutation(BlockPermutation.resolve(BOBBING_BLOCK)); ent.remove(); } catch {} } // setPermutation -> onPlace does NOT fire -> stays static
else { try { ent.remove(); } catch {} try { dim.spawnItem(new ItemStack(BOBBING_BLOCK, 1), { x: bx + 0.5, y: by + 0.5, z: bz + 0.5 }); } catch {} }
}
onPlace((ev) => { if (ev.block?.typeId === BOBBING_BLOCK) bobbingBlockToEntity(ev.block); }); // item place -> animated entity (base)
// Wind Chime 2 (sp45 / sp45_rainbow): same animated-base toggle as the bobbing apples, but 2 colours carried by an int
// `color` property on ONE entity (0 regular / 1 rainbow) so toggle + brush-recolour preserve it. NON-directional.
const WINDCHIME_ENT = "rzb_dcs:windchime", WINDCHIME_BLOCKS = ["rzb_dcs:sp45", "rzb_dcs:sp45_rainbow"]; // index = colour
function windchimeBlockToEntity(block) {
const ci = WINDCHIME_BLOCKS.indexOf(block?.typeId); if (ci < 0) return false;
const dim = block.dimension, x = block.x, y = block.y, z = block.z, dir = getDir(block.permutation);
if (toggleDebounced(x, y, z)) return true;
try { block.setType("minecraft:air"); } catch {}
let ent; try { ent = dim.spawnEntity(WINDCHIME_ENT, { x: x + 0.5, y, z: z + 0.5 }); } catch {}
if (ent) { try { ent.setProperty("rzb_dcs:color", ci); } catch {} try { ent.setRotation({ x: 0, y: TUBE_DIR_YAW[dir] ?? 0 }); } catch {} } // entity yaw matches the block facing
return true;
}
function windchimeEntityToBlock(ent) {
const dim = ent.dimension, l = ent.location, bx = Math.floor(l.x), by = Math.floor(l.y), bz = Math.floor(l.z);
if (toggleDebounced(bx, by, bz)) return;
let ci = 0; try { ci = ent.getProperty("rzb_dcs:color") ?? 0; } catch {}
let yaw = 0; try { yaw = ent.getRotation().y; } catch {}
const blockId = WINDCHIME_BLOCKS[ci] || WINDCHIME_BLOCKS[0];
const nb = safeGetBlock(dim, { x: bx, y: by, z: bz });
if (nb && (nb.isAir || nb.isLiquid || fcCanReplace(nb))) { try { nb.setPermutation(BlockPermutation.resolve(blockId, { "minecraft:cardinal_direction": tubeYawToDir(yaw) })); ent.remove(); } catch {} } // setPermutation -> onPlace does NOT fire -> stays static
else { try { ent.remove(); } catch {} try { dim.spawnItem(new ItemStack(blockId, 1), { x: bx + 0.5, y: by + 0.5, z: bz + 0.5 }); } catch {} }
}
function windchimeRecolor(ent, player) { // brush the animated chime -> flip colour property in place
let ci = 0; try { ci = ent.getProperty("rzb_dcs:color") ?? 0; } catch {}
const next = (ci + 1) % WINDCHIME_BLOCKS.length;
try { ent.setProperty("rzb_dcs:color", next); } catch {}
try { const l = ent.location; brushFx(ent.dimension, { x: Math.floor(l.x), y: Math.floor(l.y), z: Math.floor(l.z) }, next === 1 ? "rainbow" : "white", player); } catch {}
}
onPlace((ev) => { if (WINDCHIME_BLOCKS.includes(ev.block?.typeId)) windchimeBlockToEntity(ev.block); }); // item place -> animated entity (base)
const SPRINK_ID = "rzb_dcs:su34", SPRINK_ENT = "rzb_dcs:sprinkler";
const sprinkPos = (b) => ({ x: b.x + 0.5, y: b.y, z: b.z + 0.5 }); // entity origin = block floor centre
function sprinklerBlockToEntity(block) {
if (block.typeId !== SPRINK_ID) return false;
if (toggleDebounced(block.x, block.y, block.z)) return true;
const dim = block.dimension, at = sprinkPos(block), dir = getDir(block.permutation);
summonDisplay(dim, SPRINK_ENT, at.x, at.y, at.z, TUBE_DIR_YAW[dir] ?? 0);
try { block.setType("minecraft:air"); } catch {}
return true;
}
function sprinklerEntityToBlock(ent) {
const dim = ent.dimension, l = ent.location, bx = Math.floor(l.x), by = Math.floor(l.y), bz = Math.floor(l.z);
if (toggleDebounced(bx, by, bz)) return;
let yaw = 0; try { yaw = ent.getRotation().y; } catch {}
const nb = safeGetBlock(dim, { x: bx, y: by, z: bz });
if (nb && (nb.isAir || nb.isLiquid || fcCanReplace(nb))) { try { nb.setPermutation(BlockPermutation.resolve(SPRINK_ID, { "minecraft:cardinal_direction": tubeYawToDir(yaw) })); ent.remove(); } catch {} }
else { try { ent.remove(); } catch {} try { dim.spawnItem(new ItemStack(SPRINK_ID, 1), { x: bx + 0.5, y: by + 0.5, z: bz + 0.5 }); } catch {} }
}
const SPINNER_PAIRS = {
"rzb_dcs:sp35_robin": "rzb_dcs:sp35_robin_e", "rzb_dcs:sp35_blue_j": "rzb_dcs:sp35_blue_j_e",
"rzb_dcs:su01_1": "rzb_dcs:su01_1_e", "rzb_dcs:su01_2": "rzb_dcs:su01_2_e", "rzb_dcs:su01_3": "rzb_dcs:su01_3_e",
"rzb_dcs:su01_4": "rzb_dcs:su01_4_e", "rzb_dcs:su01_5": "rzb_dcs:su01_5_e", "rzb_dcs:su01_6": "rzb_dcs:su01_6_e",
"rzb_dcs:su01_7": "rzb_dcs:su01_7_e", "rzb_dcs:su01_rainbow": "rzb_dcs:su01_rainbow_e",
};
const SPINNER_ENT_TO_BLOCK = {}; for (const [b, e] of Object.entries(SPINNER_PAIRS)) SPINNER_ENT_TO_BLOCK[e] = b;
export const spinnerIsSpin = (perm) => { try { return !!perm.getState("rzb_dcs:spin"); } catch { return false; } };
export function spinnerSpawnAt(dim, blockId, x, y, z, yaw) { const ent = SPINNER_PAIRS[blockId]; if (ent) try { summonDisplay(dim, ent, x + 0.5, y, z + 0.5, yaw); } catch {} }
export function spinnerDespawnAt(dim, blockId, x, y, z) { const ent = SPINNER_PAIRS[blockId]; if (!ent) return; try { for (const e of dim.getEntities({ type: ent, location: { x: x + 0.5, y, z: z + 0.5 }, maxDistance: 0.6 })) e.remove(); } catch {} }
function spinnerToggle(block) {
if (!SPINNER_PAIRS[block.typeId]) return;
const spin = spinnerIsSpin(block.permutation), dim = block.dimension, x = block.x, y = block.y, z = block.z;
const yaw = DISPLAY_DIR_YAW[getDir(block.permutation)] ?? 0;
try { block.setPermutation(block.permutation.withState("rzb_dcs:spin", !spin)); } catch {}
if (!spin) spinnerSpawnAt(dim, block.typeId, x, y, z, yaw); else spinnerDespawnAt(dim, block.typeId, x, y, z);
}
onBreak((ev) => {
const id = ev.brokenBlockPermutation?.type?.id; if (!SPINNER_PAIRS[id]) return;
if (spinnerIsSpin(ev.brokenBlockPermutation)) { const b = ev.block; spinnerDespawnAt(b.dimension, id, b.x, b.y, b.z); }
});
function toggleEntityToBlock(ent) {
const t = ent?.typeId; if (t === BUOY_ENTITY) buoyEntityToBlock(ent); else if (t === SPRINK_ENT) sprinklerEntityToBlock(ent); else if (t === BOBBING_ENT) bobbingEntityToBlock(ent); else if (t === WINDCHIME_ENT) windchimeEntityToBlock(ent);
}
world.afterEvents.dataDrivenEntityTrigger?.subscribe((ev) => {
if (ev.id === "rzb_dcs:to_block") toggleEntityToBlock(ev.entity);
else if (ev.id === "rzb_dcs:to_recolor") brushEntRecolor(ev.entity);
});
world.afterEvents.playerInteractWithEntity?.subscribe((ev) => {
const t = ev.target, id = t?.typeId;
if (BRUSH_ENT_SEQ[id] && heldItem(ev.player) === "rzb_dcs:brush_color") return;
if (id === WINDCHIME_ENT && heldItem(ev.player) === "rzb_dcs:brush_color") return;
if (id === BUOY_ENTITY || id === SPRINK_ENT || id === BOBBING_ENT || id === WINDCHIME_ENT || SPINNER_ENT_TO_BLOCK[id]) toggleEntityToBlock(t);
});
world.afterEvents.entityHitEntity?.subscribe((ev) => {
const t = ev.hitEntity, id = t?.typeId; if (!id) return;
if (BRUSH_ENT_SEQ[id] && heldItem(ev.damagingEntity) === "rzb_dcs:brush_color") { brushEntRecolor(t, ev.damagingEntity); return; }
if (id === WINDCHIME_ENT && heldItem(ev.damagingEntity) === "rzb_dcs:brush_color") { windchimeRecolor(t, ev.damagingEntity); return; }
let wcc = 0; if (id === WINDCHIME_ENT) { try { wcc = t.getProperty("rzb_dcs:color") ?? 0; } catch {} }
const item = (id === BUOY_ENTITY) ? BO_BOTTOM : (id === SPRINK_ENT) ? SPRINK_ID : (id === BOBBING_ENT) ? BOBBING_BLOCK : (id === WINDCHIME_ENT) ? WINDCHIME_BLOCKS[wcc] : SPINNER_ENT_TO_BLOCK[id];
if (!item) return;
const dim = t.dimension, l = t.location, dropIt = ev.damagingEntity && !isCreative(ev.damagingEntity);
try { t.remove(); } catch {}
breakFx(dim, l.x, l.y + 0.4, l.z);
if (dropIt) { try { dim.spawnItem(new ItemStack(item, 1), { x: Math.floor(l.x) + 0.5, y: Math.floor(l.y) + 0.5, z: Math.floor(l.z) + 0.5 }); } catch {} }
});
const KOINO = {
"rzb_dcs:sp15": { mb: KOINO_CHILDREN, ent: "rzb_dcs:sp15_e" },
"rzb_dcs:sp15_2": { mb: KOINO_CHILDREN_2, ent: "rzb_dcs:sp15_2_e" }, // children colour variant 2
"rzb_dcs:sp16": { mb: KOINO_PARENTS, ent: "rzb_dcs:sp16_e" },
"rzb_dcs:sp18": { mb: KOINO_TOP, ent: "rzb_dcs:sp18_e" },
};
const KOINO_CELL = {}, KOINO_ANCHOR = {};
for (const [main, c] of Object.entries(KOINO)) { KOINO_ANCHOR[main] = { main, mb: c.mb, ent: c.ent }; for (const cell in c.mb.cells) KOINO_CELL[c.mb.prefix + cell] = { main, mb: c.mb, ent: c.ent, cell }; }
const KOINO_COLOR_SEQ = ["rzb_dcs:sp15", "rzb_dcs:sp15_2"];
export const koinoAnchorBrushable = (id) => KOINO_COLOR_SEQ.includes(id);
const koinoSpawn = (dim, ent, ax, ay, az, yaw = 0) => { try { summonDisplay(dim, ent, ax + 0.5, ay, az + 0.5, yaw); } catch {} };
const koinoDespawn = (dim, ent, ax, ay, az) => { try { for (const e of dim.getEntities({ type: ent, location: { x: ax + 0.5, y: ay, z: az + 0.5 }, maxDistance: 0.6 })) e.remove(); } catch {} };
onPlace((ev) => {
const b = ev.block, info = KOINO_ANCHOR[b?.typeId]; if (!info) return;
let yaw = 0; try { yaw = DISPLAY_DIR_YAW[b.permutation.getState("minecraft:cardinal_direction")] ?? 0; } catch {}
koinoSpawn(b.dimension, info.ent, b.x, b.y, b.z, yaw);
});
// mine the invisible anchor -> despawn entity (loot drops the main item)
onBreak((ev) => {
const id = ev.brokenBlockPermutation?.type?.id, info = KOINO_ANCHOR[id]; if (!info) return;
const b = ev.block; koinoDespawn(b.dimension, info.ent, b.x, b.y, b.z);
});
function koinoAnchorToBlock(block) {
const info = KOINO_ANCHOR[block?.typeId]; if (!info) return;
const mb = info.mb, dim = block.dimension, ax = block.x, ay = block.y, az = block.z;
let dir = "south"; try { dir = block.permutation.getState("minecraft:cardinal_direction") ?? "south"; } catch {}
koinoDespawn(dim, info.ent, ax, ay, az);
try { for (const cell in mb.cells) { const p = mb.cellWorld(cell, ax, ay, az, dir); const nb = safeGetBlock(dim, p); if (nb) nb.setPermutation(BlockPermutation.resolve(mb.prefix + cell, { "minecraft:cardinal_direction": dir })); } } catch {}
}
function koinoBlockToEntity(block) {
const info = KOINO_CELL[block?.typeId]; if (!info) return;
const mb = info.mb, dim = block.dimension;
let dir = "south"; try { dir = block.permutation.getState("minecraft:cardinal_direction") ?? "south"; } catch {}
const a = mb.anchorFrom(info.cell, block.x, block.y, block.z, dir);
try {
for (const cell in mb.cells) { const p = mb.cellWorld(cell, a.x, a.y, a.z, dir); const nb = safeGetBlock(dim, p); if (nb && mb.cellOf(nb.typeId) === cell) nb.setType("minecraft:air"); }
const anchorBlk = safeGetBlock(dim, { x: a.x, y: a.y, z: a.z }); // pole cell becomes the invisible anchor again
if (anchorBlk) anchorBlk.setPermutation(BlockPermutation.resolve(info.main, { "minecraft:cardinal_direction": dir }));
koinoSpawn(dim, info.ent, a.x, a.y, a.z, DISPLAY_DIR_YAW[dir] ?? 0);
} catch {}
}
export function koinoAnchorRecolor(block) {
const cur = block?.typeId, i = KOINO_COLOR_SEQ.indexOf(cur); if (i < 0) return false;
const nextMain = KOINO_COLOR_SEQ[(i + 1) % KOINO_COLOR_SEQ.length], info = KOINO_ANCHOR[cur], nextEnt = KOINO[nextMain].ent;
const dim = block.dimension, ax = block.x, ay = block.y, az = block.z;
let dir = "south"; try { dir = block.permutation.getState("minecraft:cardinal_direction") ?? "south"; } catch {}
try { block.setPermutation(BlockPermutation.resolve(nextMain, { "minecraft:cardinal_direction": dir })); } catch {}
koinoDespawn(dim, info.ent, ax, ay, az);
koinoSpawn(dim, nextEnt, ax, ay, az, DISPLAY_DIR_YAW[dir] ?? 0);
return true;
}
const KOINO_FACE_OFF = { Up: [0, 1, 0], Down: [0, -1, 0], North: [0, 0, -1], South: [0, 0, 1], East: [1, 0, 0], West: [-1, 0, 0] };
const koinoPlayerDir = (p) => { try { const v = p.getViewDirection(); return Math.abs(v.x) > Math.abs(v.z) ? (v.x > 0 ? "east" : "west") : (v.z > 0 ? "south" : "north"); } catch { return "south"; } };
function koinoPlaceHeld(e, player, inv, slot, item) {
const off = KOINO_FACE_OFF["" + e.face]; if (!off) return;
const b = e.block, dim = e.dimension ?? b.dimension, pos = { x: b.x + off[0], y: b.y + off[1], z: b.z + off[2] };
const target = safeGetBlock(dim, pos); if (!target) return;
try { if (!target.isAir) return; } catch { return; } // only place into empty space, never overwrite
const id = item.typeId;
let perm; try { perm = BlockPermutation.resolve(id); } catch { return; } // held item isn't a placeable block -> ignore
try { perm = BlockPermutation.resolve(id, { "minecraft:cardinal_direction": koinoPlayerDir(player) }); } catch {} // face like a normal placement, if supported
try { target.setPermutation(perm); } catch { return; }
const k = KOINO_ANCHOR[id]; // a koinobori placed by script -> spawn its entity (script placement doesn't fire onPlace)
if (k) { let yaw = 0; try { yaw = DISPLAY_DIR_YAW[target.permutation.getState("minecraft:cardinal_direction")] ?? 0; } catch {} koinoSpawn(dim, k.ent, pos.x, pos.y, pos.z, yaw); }
try { if (!isCreative(player)) { if (item.amount > 1) { item.amount -= 1; inv.setItem(slot, item); } else inv.setItem(slot, undefined); } } catch {} // consume one in survival
}
system.beforeEvents.startup.subscribe((init) => {
const reg = init.blockComponentRegistry;
reg.registerCustomComponent("rzb_dcs:buoy_toggle", { onPlayerInteract(e) { buoyBlockToEntity(e.block); } });
reg.registerCustomComponent("rzb_dcs:bobbing_toggle", { onPlayerInteract(e) { bobbingBlockToEntity(e.block); } });
reg.registerCustomComponent("rzb_dcs:windchime_toggle", { onPlayerInteract(e) { if (heldItem(e.player) === "rzb_dcs:brush_color") return; windchimeBlockToEntity(e.block); } }); // brush in hand -> let brush.js recolour
reg.registerCustomComponent("rzb_dcs:sprinkler_toggle", { onPlayerInteract(e) { sprinklerBlockToEntity(e.block); } });
reg.registerCustomComponent("rzb_dcs:spinner_toggle", { onPlayerInteract(e) { if (heldItem(e.player) === "rzb_dcs:brush_color") return; spinnerToggle(e.block); } }); // brush in hand -> let brush.js recolour, don't toggle
// Koinobori: EMPTY hand -> toggle (anchor->multiblock, cell->anchor+entity); ITEM in hand -> place it at the clicked face.
reg.registerCustomComponent("rzb_dcs:koino_toggle", { onPlayerInteract(e) {
const player = e.player, inv = player?.getComponent("minecraft:inventory")?.container, slot = player?.selectedSlotIndex ?? 0, item = inv?.getItem(slot);
if (!item) { const id = e.block?.typeId; if (KOINO_ANCHOR[id]) koinoAnchorToBlock(e.block); else if (KOINO_CELL[id]) koinoBlockToEntity(e.block); return; } // empty hand -> toggle
koinoPlaceHeld(e, player, inv, slot, item); // item in hand -> place against the koinobori
} });
});