added decobench assets added backend logic to best of my ability

This commit is contained in:
MomokoKoigakubo
2026-08-07 23:13:31 -04:00
parent 4a48f0bae9
commit 5d1d0a5c21
35 changed files with 656 additions and 32 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
// features/break_particle.js — custom block break particle
// features/break_particle.js custom block break particle
import { system } from "../core/mc.js";
+1 -1
View File
@@ -1,4 +1,4 @@
// features/brush.js — the Color Brush recolour
// 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";
+6 -6
View File
@@ -1,15 +1,15 @@
// features/decor.js — standalone brush recolorable decoration blocks.
// 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: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: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
@@ -28,7 +28,7 @@ registerVariants(["rzb_dcs:su37_yellow", "rzb_dcs:su37_green"]); // potted bambo
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
// 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
@@ -74,6 +74,6 @@ registerVariants(["rzb_dcs:bl_p_orange", "rzb_dcs:bl_p_yellow", "rzb_dcs:bl_p_li
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
// 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)
+39 -7
View File
@@ -25,6 +25,9 @@ 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"];
// decomposer fuel (DecoCraft): coal only, +4 per coal up to 32, -4 consumed per block-decompose
const FUEL = { obj: "rzb_dcs_decomp_fuel", max: 32, perCoal: 4, perDecomp: 4 };
const FUEL_ITEMS = ["minecraft:coal", "minecraft:charcoal"];
// 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 };
@@ -44,6 +47,10 @@ const get = (station, key) => { try { return objective(key).getScore(station) ??
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") });
// decomposer fuel scoreboard (participant = the decomposer entity)
const fuelObj = () => { let o = world.scoreboard.getObjective(FUEL.obj); if (!o) { try { o = world.scoreboard.addObjective(FUEL.obj, FUEL.obj); } catch { o = world.scoreboard.getObjective(FUEL.obj); } } return o; };
const fuelGet = (st) => { try { return fuelObj().getScore(st) ?? 0; } catch { return 0; } };
const fuelSet = (st, v) => { try { fuelObj().setScore(st, Math.max(0, Math.min(FUEL.max, v))); } catch {} };
// mirror the balance onto the station's meter_* props so the model can show fill
function syncMeters(station) {
@@ -72,6 +79,15 @@ function takeFromPlayer(player, itemId, count) {
}
return count - need;
}
// pull up to `count` of itemId from the player's MAINHAND stack only (DecoCraft consumes the held stack, not the inventory)
function takeFromHand(player, itemId, count) {
const inv = player?.getComponent("minecraft:inventory")?.container; if (!inv) return 0;
const i = player.selectedSlotIndex ?? 0, it = inv.getItem(i);
if (it?.typeId !== itemId) return 0;
const take = Math.min(it.amount, count);
if (it.amount - take <= 0) inv.setItem(i, undefined); else { it.amount -= take; inv.setItem(i, it); }
return take;
}
function givePlayer(player, itemId, count) {
const inv = player?.getComponent("minecraft:inventory")?.container;
const stack = new ItemStack(itemId, Math.max(1, Math.min(64, count)));
@@ -106,11 +122,21 @@ 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
if (d.cost.m) givePlayer(player, FEED_ITEM.m, d.cost.m); // raw_material is 1 unit/item -> give m items
for (const k of ["r", "g", "b"]) { const items = Math.floor((d.cost[k] || 0) / UNITS[k]); if (items) givePlayer(player, FEED_ITEM[k], items); } // dye cost is in UNITS -> convert to items (/8), no 8x inflation
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
// coal -> fuel (DecoCraft: +4 per coal up to 32, from the held stack)
function feedFuel(station) {
const player = nearestPlayer(station); if (!player) return;
const space = FUEL.max - fuelGet(station); if (space < FUEL.perCoal) return;
const maxCoal = Math.floor(space / FUEL.perCoal);
let took = 0; for (const it of FUEL_ITEMS) { took = takeFromHand(player, it, maxCoal); if (took) break; }
if (took) fuelSet(station, fuelGet(station) + took * FUEL.perCoal);
}
// blocks -> raw_material, from the player's held stack (floor by rate, leftover stays). Requires + consumes coal fuel (DecoCraft)
function decompBlock(station) {
if (fuelGet(station) < FUEL.perDecomp) { try { nearestPlayer(station)?.playSound("note.bass", { volume: 0.6 }); } catch {} return; } // out of fuel
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);
@@ -120,13 +146,14 @@ function decompBlock(station) {
if (held.amount - used <= 0) inv.setItem(idx, undefined);
else { held.amount -= used; inv.setItem(idx, held); }
givePlayer(player, FEED_ITEM.m, toraw);
fuelSet(station, fuelGet(station) - FUEL.perDecomp); // remove_fuel per process
}
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;
const took = takeFromHand(player, FEED_ITEM[key], maxItems); if (!took) return;
addUnits(station, key, Math.min(space, took * per));
syncMeters(station);
}
@@ -134,10 +161,9 @@ function feed(station, key) {
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));
const maxItems = Math.min(Math.floor(spaceA / per), Math.floor(spaceB / per)); if (maxItems <= 0) return; // both channels must accept the full 4 (no dye wasted)
const took = takeFromHand(player, itemId, maxItems); if (!took) return;
addUnits(station, kA, took * per); addUnits(station, kB, took * per);
syncMeters(station);
}
function drop(station, key) {
@@ -147,6 +173,11 @@ function drop(station, key) {
dropAt(station.dimension, station.location, FEED_ITEM[key], items);
syncMeters(station);
}
// return ALL stored materials as items (DecoCraft returns these when a bench is broken)
export function dropAll(station) { for (const k of CHANNELS) drop(station, k); }
function dropFuel(station) { const coal = Math.floor(fuelGet(station) / FUEL.perCoal); if (coal > 0) dropAt(station.dimension, station.location, "minecraft:coal", coal); fuelSet(station, 0); }
// on break: the bench returns its stored materials, the decomposer returns its stored fuel as coal
export function dropStored(station) { if (station?.typeId === "rzb_dcs:decomposer") dropFuel(station); else dropAll(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
@@ -154,6 +185,7 @@ 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:feed_fuel": (st) => feedFuel(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"),
+1 -1
View File
@@ -1,5 +1,5 @@
// features/display.js — animated display pieces (place a block -> spawn an entity).
// 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";
+2 -2
View File
@@ -1,5 +1,5 @@
// features/interact.js — right-click lids/toggles + the interact fall-through dispatcher.
// 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";
@@ -110,7 +110,7 @@ world.afterEvents.playerInteractWithBlock?.subscribe((ev) => {
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.
// 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);
+2 -2
View File
@@ -1,4 +1,4 @@
// features/maintenance.js — pick-block anchor redirect + firepit embers.
// 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";
@@ -87,7 +87,7 @@ export const EMBER_BLOCKS = {
"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)
"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 },
+1 -1
View File
@@ -1,4 +1,4 @@
// features/mb_grid.js — grid & canopy multiblocks, defined as data.
// features/mb_grid.js grid & canopy multiblocks, defined as data.
import { defineGridMultiblock } from "../core/multiblock.js";
+1 -1
View File
@@ -1,4 +1,4 @@
// features/mb_pairs.js — partner / vertical / 2x2 multiblocks.
// 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";
+2 -2
View File
@@ -1,11 +1,11 @@
// features/mb_trees.js — christmas / light / potted trees as multiblocks.
// 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
// 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 ---
+5 -5
View File
@@ -1,4 +1,4 @@
// features/rideables.js — block <-> rideable-entity vehicles (tube, flamingo, lounger, bike, surfboard, basket bike, sled).
// 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.
@@ -135,7 +135,7 @@ function steerForward(p, ent, yawOff = 0) {
// 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).
// 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 }; }
@@ -263,7 +263,7 @@ function landImpulse(veh, dim, loc, fx, fz, rx, rz, ix, spd, strafe, breakFn) {
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).
// 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 {}
@@ -398,14 +398,14 @@ 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
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
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;
+1 -1
View File
@@ -1,4 +1,4 @@
// features/seating.js — the sit system (seat entity + per-furniture config).
// 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";
+55
View File
@@ -0,0 +1,55 @@
// features/stations.js crafting stations placement + removal (1-to-1 with DecoCraft).
// Placer item -> minecraft:entity_placer spawns an INVISIBLE _init; on its spawn we grid-align, check clearance + the
// 8-block duplicate guard, then summon the real station PRE-ROTATED to the player's look cardinal (one step, no visible
// rotation) and remove the init. A blocked placement refunds the item + plays the reject sound. The station is non-solid;
// its footprint is invisible full-collision blocks (bench = 2 cells, decomposer = 1). Hitting a station returns the stored
// materials + the item (no item in creative, like DecoCraft) and clears the footprint blocks.
import { world, ItemStack, isCreative, safeGetBlock } from "../core/mc.js";
import { fcCanReplace } from "../core/grid.js";
import { dropStored } from "./decostation.js";
const INIT_TO_STATION = { "rzb_dcs:decobench_init": "rzb_dcs:decobench", "rzb_dcs:decomposer_init": "rzb_dcs:decomposer" };
const STATIONS = new Set(["rzb_dcs:decobench", "rzb_dcs:decomposer"]);
const ITEM_FOR = { "rzb_dcs:decobench": "rzb_dcs:seasons_bench", "rzb_dcs:decomposer": "rzb_dcs:deco_seasons_decomp" };
const FILL = { "rzb_dcs:decobench": "^ ^ ^ ^1 ^ ^", "rzb_dcs:decomposer": "^ ^ ^ ^ ^ ^" }; // 2-cell bench, 1-cell decomposer
const TWO_CELL = new Set(["rzb_dcs:decobench"]);
const leftCell = (yaw) => { const r = yaw * Math.PI / 180; return [Math.round(Math.cos(r)), Math.round(Math.sin(r))]; }; // world offset of the local-left cell (^1)
const replaceable = (dim, cx, cy, cz) => { const b = safeGetBlock(dim, { x: cx, y: cy, z: cz }); return !!b && (b.isAir || fcCanReplace(b)); };
world.afterEvents.entitySpawn?.subscribe((ev) => {
const e = ev.entity;
const station = INIT_TO_STATION[e?.typeId];
if (station) {
const dim = e.dimension, l = e.location;
const bx = Math.floor(l.x), by = Math.floor(l.y), bz = Math.floor(l.z);
let yaw = 180, player; try { player = dim.getPlayers({ location: l, closest: 1, maxDistance: 6 })[0]; if (player) yaw = Math.round(player.getRotation().y / 90) * 90 + 180; } catch {}
const cells = [[bx, by, bz]];
if (TWO_CELL.has(station)) { const [lx, lz] = leftCell(yaw); cells.push([bx + lx, by, bz + lz]); }
const clear = cells.every(([cx, cy, cz]) => replaceable(dim, cx, cy, cz));
let dup = false; try { dup = dim.getEntities({ location: l, type: station, maxDistance: 8 }).length > 0; } catch {}
if (!clear || dup) { // DecoCraft: refuse -> refund the item + reject sound
try { dim.spawnItem(new ItemStack(ITEM_FOR[station], 1), { x: bx + 0.5, y: by + 0.5, z: bz + 0.5 }); } catch {}
try { player?.playSound("note.didgeridoo", { volume: 0.8 }); } catch {}
try { e.remove(); } catch {}
return;
}
const ht = TWO_CELL.has(station) ? ` rzb_dcs:ht_${((yaw % 360) + 360) % 360}` : ""; // facing-oriented 2-cell hit box (bench only)
try { dim.runCommand(`summon ${station} ${bx + 0.5} ${by} ${bz + 0.5} ${yaw} 0${ht}`); } catch {} // pre-rotated
try { e.remove(); } catch {}
return;
}
if (STATIONS.has(e?.typeId) && ev.cause !== "Loaded") { // real station placed -> fill its invisible collision footprint
try { e.runCommand(`fill ${FILL[e.typeId]} rzb_dcs:invisible_block`); } catch {}
}
});
// hit to break -> clear footprint, return stored materials + the item (no item in creative), remove
world.afterEvents.entityHitEntity?.subscribe((ev) => {
const t = ev.hitEntity; if (!STATIONS.has(t?.typeId)) return;
const dmg = ev.damagingEntity; if (dmg?.typeId !== "minecraft:player") return;
try { t.runCommand(`fill ${FILL[t.typeId]} air replace rzb_dcs:invisible_block`); } catch {}
try { dropStored(t); } catch {}
if (!isCreative(dmg)) { try { t.dimension.spawnItem(new ItemStack(ITEM_FOR[t.typeId], 1), t.location); } catch {} }
try { t.remove(); } catch {}
});