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.

154 lines
13 KiB
JavaScript

// 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);