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

210 lines
13 KiB
JavaScript

// 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"];
// 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 };
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") });
// 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) {
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;
}
// 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)));
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
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 {}
}
// 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);
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);
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 = takeFromHand(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 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) {
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);
}
// 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
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"),
"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); });