Files
minecraft-aiworld/camping-supplies-addon/camping_supplies_BP/scripts/main.js
SysAdmin b9e3380f6c feat(camping): three-tier ore detector with private-chest faraday cage
New basic/improved/advanced detectors (8/16/32 block range). Aim and
right-click to ping the nearest ore on the view ray; pitch-coded sound
and action-bar text show distance and ore type. Any ore within 4 blocks
of a silverlabs:private_chest is hidden — chests act as faraday cages
so claimed bases stay private from neighbours' detectors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 21:59:27 +01:00

821 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { world, system, ItemStack } from "@minecraft/server";
// ─── Constants ──────────────────────────────────────────────
const TENT_ITEM = "silverlabs:tent";
const HAMMOCK_ITEM = "silverlabs:hammock";
const TENT_BLOCK = "silverlabs:tent_canvas"; // legacy cube — kept so old worlds load cleanly
const TENT_PANEL_L = "silverlabs:tent_panel_l";
const TENT_PANEL_R = "silverlabs:tent_panel_r";
const TENT_BLOCK_IDS = [TENT_BLOCK, TENT_PANEL_L, TENT_PANEL_R];
const HAMMOCK_BLOCK = "silverlabs:hammock_cloth";
const STATE_PROP = "camping_state_v1";
const HAMMOCK_TAG = "camping_hammock";
const TENT_REST_PROP = "camping_tent_rest"; // per-player: "{x,y,z,dim,startTick}"
const SLEEP_TICK_INTERVAL = 20; // run sleep loop every 1s
const NIGHT_START = 12500;
const NIGHT_END = 23500;
// ─── State ──────────────────────────────────────────────────
// tents: key = "ox,oy,oz,dim" -> { ownerId, ownerName, cells: [[x,y,z]...] }
// hammocks: key = "ax,ay,az->bx,by,bz,dim" -> { ownerId, ownerName, anchorA, anchorB, cells }
let state = { tents: {}, hammocks: {} };
function loadState() {
try {
const raw = world.getDynamicProperty(STATE_PROP);
if (raw && typeof raw === "string") {
const parsed = JSON.parse(raw);
state = {
tents: parsed.tents || {},
hammocks: parsed.hammocks || {},
};
}
} catch (e) {
world.sendMessage(`§c[Camping] state load failed: ${e.message}`);
}
}
function saveState() {
try {
world.setDynamicProperty(STATE_PROP, JSON.stringify(state));
} catch (e) {
world.sendMessage(`§c[Camping] state save failed: ${e.message}`);
}
}
function keyOf(x, y, z, dimId) {
return `${x},${y},${z},${dimId}`;
}
// ─── Orientation helpers ────────────────────────────────────
function cardinalFacing(yaw) {
let y = yaw;
while (y > 180) y -= 360;
while (y < -180) y += 360;
if (y >= -45 && y < 45) return "south";
if (y >= 45 && y < 135) return "west";
if (y >= -135 && y < -45) return "east";
return "north";
}
// Map our placement facing → block-state cardinal_direction the panel rotates to.
// The geometry's "default" (cardinal_direction = "north") has the slope's outer
// edge on -X and inner apex on +X with the depth running along Z. When the
// player faces north, that aligns with the world. When they face elsewhere,
// the placement_direction trait + permutations rotate the model to match.
function blockFacingFor(playerFacing) {
return playerFacing; // 1:1 — placement_direction handles the rotation
}
function vecsForFacing(facing) {
switch (facing) {
case "north": return { fx: 0, fz: -1, rx: 1, rz: 0 };
case "south": return { fx: 0, fz: 1, rx: -1, rz: 0 };
case "east": return { fx: 1, fz: 0, rx: 0, rz: -1 };
case "west": return { fx: -1, fz: 0, rx: 0, rz: 1 };
}
return { fx: 0, fz: 1, rx: -1, rz: 0 };
}
// ─── Inventory helpers ──────────────────────────────────────
function consumeOneOfType(player, typeId) {
const inv = player.getComponent("inventory")?.container;
if (!inv) return false;
const slot = player.selectedSlotIndex;
const item = inv.getItem(slot);
if (item && item.typeId === typeId) {
if (item.amount > 1) {
item.amount -= 1;
inv.setItem(slot, item);
} else {
inv.setItem(slot, undefined);
}
return true;
}
for (let i = 0; i < inv.size; i++) {
const it = inv.getItem(i);
if (it && it.typeId === typeId) {
if (it.amount > 1) {
it.amount -= 1;
inv.setItem(i, it);
} else {
inv.setItem(i, undefined);
}
return true;
}
}
return false;
}
// ─── Tent placement (2×3 footprint, ridge-tunnel shape) ─────
function tryPlaceTent(player) {
const dim = player.dimension;
const facing = cardinalFacing(player.getRotation().y);
const { fx, fz, rx, rz } = vecsForFacing(facing);
// Use precise player position; floor X/Z but scan Y downward to find the actual
// standing surface. player.location.y may be fractionally above the block you're
// on (e.g. 87.01), so floor() alone is reliable, but if the player is in the
// air (jumping / on a slab / flying) we want to project them down to solid ground
// so the tent doesn't try to sit on empty space.
const feetX = Math.floor(player.location.x);
const feetZ = Math.floor(player.location.z);
let feetY = Math.floor(player.location.y);
// If the block at feet level is solid (player inside a block, e.g. standing in
// tall grass that rounded up), step up one.
const feetBlock = dim.getBlock({ x: feetX, y: feetY, z: feetZ });
if (feetBlock && !feetBlock.isAir && !feetBlock.isLiquid) feetY += 1;
// If the block below is air (mid-jump / airborne), project down to ground.
for (let probe = 0; probe < 4; probe++) {
const below = dim.getBlock({ x: feetX, y: feetY - 1, z: feetZ });
if (below && !below.isAir && !below.isLiquid) break;
feetY -= 1;
}
const ox = feetX + fx;
const oy = feetY;
const oz = feetZ + fz;
const groundCells = [];
const clearCells = [];
for (let l = 0; l < 3; l++) {
for (let w = 0; w < 2; w++) {
const cx = ox + l * fx + w * rx;
const cz = oz + l * fz + w * rz;
groundCells.push({ x: cx, y: oy - 1, z: cz });
for (let h = 0; h <= 1; h++) clearCells.push({ x: cx, y: oy + h, z: cz });
}
}
for (const g of groundCells) {
const b = dim.getBlock(g);
if (!b || b.isAir || b.isLiquid) {
const seen = b ? b.typeId : "unloaded";
player.sendMessage(`§c[Camping] §7Ground at §f${g.x},${g.y},${g.z}§7 is §f${seen}§7 — need solid ground there.`);
return false;
}
}
for (const c of clearCells) {
const b = dim.getBlock(c);
if (!b) {
player.sendMessage(`§c[Camping] §7Can't reach §f${c.x},${c.y},${c.z}§7 (chunk unloaded).`);
return false;
}
if (!b.isAir && !b.isLiquid) {
player.sendMessage(`§c[Camping] §7Space at §f${c.x},${c.y},${c.z}§7 is blocked by §f${b.typeId}§7.`);
return false;
}
}
// A-frame layout: 3 long × 2 wide, single block tall. Each cross-section is a
// pair of slope panels meeting at the apex on the seam between the two columns.
// Bedrock renders the panel geometry mirrored across the block's local X axis
// relative to a literal read of the .geo.json, so panel_r goes in the player's
// column and panel_l goes one step right to land /\ instead of \/.
const blockFacing = blockFacingFor(facing);
const canvasCells = [];
for (let l = 0; l < 3; l++) {
canvasCells.push({
x: ox + l * fx,
y: oy,
z: oz + l * fz,
block: TENT_PANEL_R,
});
canvasCells.push({
x: ox + l * fx + rx,
y: oy,
z: oz + l * fz + rz,
block: TENT_PANEL_L,
});
}
for (const c of canvasCells) {
try {
dim.runCommand(
`setblock ${c.x} ${c.y} ${c.z} ${c.block} ["minecraft:cardinal_direction"="${blockFacing}"]`
);
} catch (_) {}
}
const key = keyOf(ox, oy, oz, dim.id);
state.tents[key] = {
ownerId: player.id,
ownerName: player.name,
facing,
cells: canvasCells.map((c) => [c.x, c.y, c.z]),
};
saveState();
return true;
}
// ─── Hammock placement ──────────────────────────────────────
function isPostBlock(block) {
if (!block) return false;
const id = block.typeId;
return (
id.endsWith("_fence") ||
id.endsWith("_log") ||
id.endsWith("_wood") ||
id.includes("stripped_") ||
id.endsWith("_wall")
);
}
function findPartnerPost(dim, anchor) {
const candidates = [];
for (let dx = -6; dx <= 6; dx++) {
for (let dz = -6; dz <= 6; dz++) {
if (dx === 0 && dz === 0) continue;
const aligned = dx === 0 || dz === 0 || Math.abs(dx) === Math.abs(dz);
if (!aligned) continue;
const dist = Math.max(Math.abs(dx), Math.abs(dz));
if (dist < 3 || dist > 6) continue;
for (let dy = -1; dy <= 1; dy++) {
const pos = { x: anchor.x + dx, y: anchor.y + dy, z: anchor.z + dz };
let blk;
try { blk = dim.getBlock(pos); } catch (_) { continue; }
if (!blk || !isPostBlock(blk)) continue;
candidates.push({ pos, dist, dy });
}
}
}
if (candidates.length === 0) return null;
candidates.sort((a, b) => (Math.abs(a.dy) - Math.abs(b.dy)) || (a.dist - b.dist));
return candidates[0].pos;
}
function computeHammockCells(a, b) {
const dx = b.x - a.x;
const dy = b.y - a.y;
const dz = b.z - a.z;
const steps = Math.max(Math.abs(dx), Math.abs(dz));
const cells = [];
for (let t = 1; t < steps; t++) {
const cx = a.x + Math.round((dx * t) / steps);
const cz = a.z + Math.round((dz * t) / steps);
let cy = a.y + Math.round((dy * t) / steps);
const rel = t / steps;
if (steps >= 4 && rel > 0.25 && rel < 0.75) cy -= 1;
cells.push({ x: cx, y: cy, z: cz });
}
return cells;
}
function tryPlaceHammock(player, anchorBlock) {
const dim = player.dimension;
const a = {
x: anchorBlock.location.x,
y: anchorBlock.location.y,
z: anchorBlock.location.z,
};
const b = findPartnerPost(dim, a);
if (!b) {
player.sendMessage("§c[Camping] §7Need a second post 36 blocks away (straight line or diagonal, ±1 block in height).");
return false;
}
const cells = computeHammockCells(a, b);
for (const c of cells) {
let blk;
try { blk = dim.getBlock(c); } catch (_) { return false; }
if (!blk || (!blk.isAir && !blk.isLiquid)) {
player.sendMessage("§c[Camping] §7The space between the posts isn't clear.");
return false;
}
}
for (const c of cells) {
try { dim.runCommand(`setblock ${c.x} ${c.y} ${c.z} ${HAMMOCK_BLOCK}`); } catch (_) {}
}
const key = `${a.x},${a.y},${a.z}->${b.x},${b.y},${b.z},${dim.id}`;
state.hammocks[key] = {
ownerId: player.id,
ownerName: player.name,
anchorA: [a.x, a.y, a.z],
anchorB: [b.x, b.y, b.z],
cells: cells.map((c) => [c.x, c.y, c.z]),
};
saveState();
return true;
}
// ─── Item use handler ───────────────────────────────────────
world.afterEvents.itemUse.subscribe((event) => {
const player = event.source;
const stack = event.itemStack;
if (!stack || !player) return;
if (stack.typeId === TENT_ITEM) {
system.run(() => {
if (tryPlaceTent(player)) {
consumeOneOfType(player, TENT_ITEM);
player.sendMessage("§a[Camping] §7Tent pitched. Right-click the canvas to rest until dawn.");
}
});
} else if (stack.typeId === HAMMOCK_ITEM) {
system.run(() => {
const looking = player.getBlockFromViewDirection({ maxDistance: 6 });
const block = looking?.block;
if (!block || !isPostBlock(block)) {
player.sendMessage("§c[Camping] §7Aim at a fence, log, or wooden post to anchor the hammock.");
return;
}
if (tryPlaceHammock(player, block)) {
consumeOneOfType(player, HAMMOCK_ITEM);
player.sendMessage("§a[Camping] §7Hammock strung. Right-click the cloth to climb in.");
}
});
}
});
// ─── Interact: tent rest + hammock toggle ───────────────────
try {
world.beforeEvents.playerInteractWithBlock.subscribe((event) => {
const block = event.block;
if (!block) return;
if (TENT_BLOCK_IDS.includes(block.typeId)) {
event.cancel = true;
const player = event.player;
const loc = { x: block.location.x, y: block.location.y, z: block.location.z };
const dimId = block.dimension.id;
system.run(() => enterTentRest(player, loc, dimId));
} else if (block.typeId === HAMMOCK_BLOCK) {
event.cancel = true;
const player = event.player;
const loc = block.location;
system.run(() => toggleHammock(player, loc));
}
});
} catch (e) {
console.warn(`[Camping] playerInteractWithBlock unavailable: ${e}`);
}
// ─── Tent rest: vote-skip with mixed bed + tent sleepers ────────
// Tracks which players are currently "resting" in a tent. A player counts as a
// tent sleeper as long as they stay near the panel they interacted with and
// don't sneak/move/disconnect/take damage. Vanilla bed sleepers are detected
// via player.isSleeping (true while a player is in a real bed). We compare the
// combined count against the world's playersSleepingPercentage gamerule and
// skip the night when the threshold is crossed.
const tentRest = new Map(); // playerId → { x, y, z, dimId, startTick }
function isNight(tod) {
return tod >= NIGHT_START && tod <= NIGHT_END;
}
function enterTentRest(player, loc, dimId) {
const tod = world.getTimeOfDay();
if (!isNight(tod)) {
player.sendMessage("§e[Camping] §7It's still daylight — nothing to sleep off.");
return;
}
if (tentRest.has(player.id)) {
leaveTentRest(player, "§7[Camping] You stop resting.");
return;
}
tentRest.set(player.id, {
x: loc.x,
y: loc.y,
z: loc.z,
dimId,
startTick: system.currentTick,
});
// Cinematic fade so it reads like sleep instead of a status message.
try {
player.runCommand("camera @s fade time 0.4 1.5 0.6 color 0 0 0");
} catch (_) {}
try {
player.onScreenDisplay.setTitle("§7Resting…", {
fadeInDuration: 8,
stayDuration: 60,
fadeOutDuration: 12,
subtitle: "§8Move, sneak, or take damage to wake.",
});
} catch (_) {}
reportSleepProgress(player, /*onEnter*/ true);
}
function leaveTentRest(player, msg) {
if (!tentRest.delete(player.id)) return;
if (msg && player) {
try { player.sendMessage(msg); } catch (_) {}
}
}
function countSleepers() {
let bed = 0;
let tent = 0;
let online = 0;
for (const p of world.getAllPlayers()) {
online++;
// Vanilla bed sleep: Player.isSleeping is true while they're in a bed.
// Available since @minecraft/server 1.10+; guarded for safety.
let sleeping = false;
try { sleeping = !!p.isSleeping; } catch (_) {}
if (sleeping) bed++;
else if (tentRest.has(p.id)) tent++;
}
return { bed, tent, online, resting: bed + tent };
}
function getSleepThreshold() {
// playersSleepingPercentage is a percentage 0-100. 0 means any one player
// can skip night (vanilla quirk); 100 means everyone must sleep.
let pct = 100;
try {
const v = world.gameRules?.playersSleepingPercentage;
if (typeof v === "number") pct = v;
} catch (_) {}
// 0 in vanilla means "one is enough" — preserve that intent.
if (pct <= 0) return 1;
return pct;
}
function requiredSleepers(online, pct) {
// Standard vanilla rounding: ceil(online * pct / 100), min 1.
return Math.max(1, Math.ceil((online * pct) / 100));
}
function reportSleepProgress(targetPlayer, onEnter = false) {
const { bed, tent, online, resting } = countSleepers();
const pct = getSleepThreshold();
const need = requiredSleepers(online, pct);
const remaining = Math.max(0, need - resting);
const msg = onEnter
? `§a[Camping] §7You settle in. §f${resting}§7/§f${need}§7 resting (§f${tent}§7 tent + §f${bed}§7 bed)${remaining ? `. Need §f${remaining}§7 more.` : `.`}`
: `§7[Sleep] §f${resting}§7/§f${need}§7 resting (§f${tent}§7 tent + §f${bed}§7 bed)`;
if (onEnter && targetPlayer) {
try { targetPlayer.sendMessage(msg); } catch (_) {}
} else {
// Broadcast a subtle update to everyone currently resting.
for (const p of world.getAllPlayers()) {
if (tentRest.has(p.id) || (() => { try { return !!p.isSleeping; } catch (_) { return false; } })()) {
try { p.sendMessage(msg); } catch (_) {}
}
}
}
}
function awardRestEffects(player) {
try {
player.addEffect("regeneration", 200, { amplifier: 1, showParticles: false });
player.addEffect("saturation", 40, { amplifier: 0, showParticles: false });
} catch (_) {}
}
function executeNightSkip() {
// Snapshot tent sleepers before clearing, so we can give them rest perks.
const tentIds = [...tentRest.keys()];
tentRest.clear();
try { world.setTimeOfDay(0); } catch (_) {}
for (const p of world.getAllPlayers()) {
if (tentIds.includes(p.id) || p.isSleeping) {
awardRestEffects(p);
}
try { p.runCommand("camera @s clear"); } catch (_) {}
}
world.sendMessage("§6[Sleep] §7The camp rests. Dawn breaks.");
}
// ─── Sleep loop: validate tent sleepers, check threshold ────────
system.runInterval(() => {
if (tentRest.size === 0) return;
// Validate each tent sleeper still meets the criteria.
for (const [pid, rest] of [...tentRest.entries()]) {
const player = world.getAllPlayers().find((p) => p.id === pid);
if (!player) {
tentRest.delete(pid);
continue;
}
if (player.dimension.id !== rest.dimId) {
leaveTentRest(player, "§7[Camping] You wandered out of camp.");
continue;
}
const dx = player.location.x - (rest.x + 0.5);
const dy = player.location.y - rest.y;
const dz = player.location.z - (rest.z + 0.5);
if (dx * dx + dz * dz > 4 || Math.abs(dy) > 2) {
leaveTentRest(player, "§7[Camping] You wandered out of camp.");
continue;
}
if (player.isSneaking) {
leaveTentRest(player, "§7[Camping] You climb out of the tent.");
continue;
}
// Sleepers don't get scared off by mobs but a hit cancels rest:
// (handled implicitly — damage breaks the camera fade and the player is
// expected to sneak out; we don't have a public hurt event hook here)
awardRestEffects(player);
}
if (tentRest.size === 0) return;
const tod = world.getTimeOfDay();
if (!isNight(tod)) {
// Sun came up some other way — clear resters quietly.
tentRest.clear();
return;
}
const { online, resting } = countSleepers();
const pct = getSleepThreshold();
const need = requiredSleepers(online, pct);
if (resting >= need) {
executeNightSkip();
}
}, SLEEP_TICK_INTERVAL);
const HAMMOCK_ANCHOR_PROP = "camping_hammock_anchor";
function toggleHammock(player, loc) {
if (player.hasTag(HAMMOCK_TAG)) {
exitHammock(player);
return;
}
player.addTag(HAMMOCK_TAG);
const anchor = { x: loc.x + 0.5, y: loc.y + 0.1, z: loc.z + 0.5 };
try {
player.setDynamicProperty(HAMMOCK_ANCHOR_PROP, JSON.stringify(anchor));
} catch (_) {}
try {
player.teleport(anchor, { dimension: player.dimension });
} catch (_) {}
// Slowness 255 + weakness + mining_fatigue make the player effectively immobile while
// still conscious; saturation + regen are the "rest" payoff.
try {
player.addEffect("slowness", 100000, { amplifier: 255, showParticles: false });
player.addEffect("weakness", 100000, { amplifier: 255, showParticles: false });
player.addEffect("mining_fatigue", 100000, { amplifier: 255, showParticles: false });
player.addEffect("saturation", 40, { amplifier: 0, showParticles: false });
player.addEffect("regeneration", 200, { amplifier: 1, showParticles: false });
} catch (_) {}
// Pull the camera back into a cinematic third-person view so the player can see
// themselves lying in the hammock. Ease in smoothly.
try {
player.runCommand("camera @s set minecraft:third_person ease 0.8 out_sine");
} catch (_) {}
player.sendMessage("§a[Camping] §7You settle into the hammock. Wild creatures don't notice you. §8(Sneak to climb out.)");
}
function exitHammock(player) {
player.removeTag(HAMMOCK_TAG);
try {
player.removeEffect("slowness");
player.removeEffect("weakness");
player.removeEffect("mining_fatigue");
} catch (_) {}
// Nudge player one block off the hammock so the next tick doesn't re-teleport them
// back into the cradle.
try {
const raw = player.getDynamicProperty(HAMMOCK_ANCHOR_PROP);
if (raw && typeof raw === "string") {
const a = JSON.parse(raw);
const yaw = player.getRotation().y;
const rad = (yaw * Math.PI) / 180;
const dx = -Math.sin(rad);
const dz = Math.cos(rad);
player.teleport(
{ x: a.x + dx * 1.2, y: a.y + 0.3, z: a.z + dz * 1.2 },
{ dimension: player.dimension }
);
}
} catch (_) {}
try { player.setDynamicProperty(HAMMOCK_ANCHOR_PROP, undefined); } catch (_) {}
try { player.runCommand("camera @s clear"); } catch (_) {}
player.sendMessage("§7[Camping] You climb out of the hammock.");
}
// ─── Hammock upkeep loop: position lock + mob repulsion + sneak-exit ────────
system.runInterval(() => {
for (const player of world.getAllPlayers()) {
if (!player.hasTag(HAMMOCK_TAG)) continue;
if (player.isSneaking) {
exitHammock(player);
continue;
}
// Pin the player to the hammock anchor so they can't drift off even with slowness
try {
const raw = player.getDynamicProperty(HAMMOCK_ANCHOR_PROP);
if (raw && typeof raw === "string") {
const a = JSON.parse(raw);
const dx = player.location.x - a.x;
const dy = player.location.y - a.y;
const dz = player.location.z - a.z;
if (dx * dx + dy * dy + dz * dz > 0.25) {
player.teleport(a, { dimension: player.dimension });
}
}
} catch (_) {}
let hostiles = [];
try {
hostiles = player.dimension.getEntities({
families: ["monster"],
location: player.location,
maxDistance: 14,
});
} catch (_) {}
for (const m of hostiles) {
const dx = m.location.x - player.location.x;
const dy = m.location.y - player.location.y;
const dz = m.location.z - player.location.z;
const d = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (d > 0.01 && d < 6) {
const scale = 9 / d;
const target = {
x: player.location.x + dx * scale,
y: m.location.y,
z: player.location.z + dz * scale,
};
try { m.tryTeleport(target, { checkForBlocks: true }); } catch (_) {}
}
}
}
}, 10);
// ─── Break cleanup: break one = pack up the whole structure ─
try {
world.beforeEvents.playerBreakBlock.subscribe((event) => {
const block = event.block;
if (!block) return;
const id = block.typeId;
const isTent = TENT_BLOCK_IDS.includes(id);
if (!isTent && id !== HAMMOCK_BLOCK) return;
event.cancel = true;
const loc = { x: block.location.x, y: block.location.y, z: block.location.z };
const dimId = block.dimension.id;
const player = event.player;
if (isTent) {
system.run(() => dismantleTentAt(loc, dimId, player));
} else {
system.run(() => dismantleHammockAt(loc, dimId, player));
}
});
} catch (e) {
console.warn(`[Camping] playerBreakBlock unavailable: ${e}`);
}
function dismantleTentAt(loc, dimId, player) {
const dim = world.getDimension(dimId);
let matchedKey = null;
for (const [k, tent] of Object.entries(state.tents)) {
const parts = k.split(",");
if (parts[parts.length - 1] !== dimId) continue;
if (tent.cells.some(([x, y, z]) => x === loc.x && y === loc.y && z === loc.z)) {
matchedKey = k;
break;
}
}
if (!matchedKey) {
try { dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`); } catch (_) {}
return;
}
const tent = state.tents[matchedKey];
for (const [x, y, z] of tent.cells) {
try { dim.runCommand(`setblock ${x} ${y} ${z} air`); } catch (_) {}
}
delete state.tents[matchedKey];
saveState();
try {
dim.spawnItem(new ItemStack(TENT_ITEM, 1), { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 });
} catch (_) {}
if (player) player.sendMessage("§7[Camping] Tent packed up.");
}
function dismantleHammockAt(loc, dimId, player) {
const dim = world.getDimension(dimId);
let matchedKey = null;
for (const [k, h] of Object.entries(state.hammocks)) {
if (!k.endsWith("," + dimId)) continue;
if (h.cells.some(([x, y, z]) => x === loc.x && y === loc.y && z === loc.z)) {
matchedKey = k;
break;
}
}
if (!matchedKey) {
try { dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`); } catch (_) {}
return;
}
const h = state.hammocks[matchedKey];
for (const [x, y, z] of h.cells) {
try { dim.runCommand(`setblock ${x} ${y} ${z} air`); } catch (_) {}
}
delete state.hammocks[matchedKey];
saveState();
try {
dim.spawnItem(new ItemStack(HAMMOCK_ITEM, 1), { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 });
} catch (_) {}
if (player) player.sendMessage("§7[Camping] Hammock taken down.");
}
// ─── Ore Detector ───────────────────────────────────────────
// Three tiered items share one scan handler. Tier → max scan range.
// Faraday: any ore within 4 blocks of a silverlabs:private_chest is hidden
// (mirrors the source DetectOre faraday-cage mechanic, but built into private chests).
const DETECTOR_RANGES = {
"silverlabs:ore_detector_basic": 8,
"silverlabs:ore_detector_improved": 16,
"silverlabs:ore_detector_advanced": 32,
};
const PRIVATE_CHEST_ID = "silverlabs:private_chest";
const FARADAY_RADIUS = 4; // 9³ cube around each ore candidate
const ORE_IDS = new Set([
"minecraft:coal_ore", "minecraft:deepslate_coal_ore",
"minecraft:iron_ore", "minecraft:deepslate_iron_ore",
"minecraft:copper_ore", "minecraft:deepslate_copper_ore",
"minecraft:gold_ore", "minecraft:deepslate_gold_ore", "minecraft:nether_gold_ore",
"minecraft:redstone_ore", "minecraft:deepslate_redstone_ore",
"minecraft:lit_redstone_ore", "minecraft:lit_deepslate_redstone_ore",
"minecraft:lapis_ore", "minecraft:deepslate_lapis_ore",
"minecraft:emerald_ore", "minecraft:deepslate_emerald_ore",
"minecraft:diamond_ore", "minecraft:deepslate_diamond_ore",
"minecraft:nether_quartz_ore",
"minecraft:ancient_debris",
]);
function prettyOreName(typeId) {
const bare = typeId.replace(/^minecraft:/, "")
.replace(/^lit_/, "")
.replace(/^deepslate_/, "Deepslate ")
.replace(/^nether_/, "Nether ");
return bare.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
}
function isPrivateChestNearby(dim, cx, cy, cz) {
for (let dx = -FARADAY_RADIUS; dx <= FARADAY_RADIUS; dx++) {
for (let dy = -FARADAY_RADIUS; dy <= FARADAY_RADIUS; dy++) {
for (let dz = -FARADAY_RADIUS; dz <= FARADAY_RADIUS; dz++) {
const b = dim.getBlock({ x: cx + dx, y: cy + dy, z: cz + dz });
if (b && b.typeId === PRIVATE_CHEST_ID) return true;
}
}
}
return false;
}
function runOreScan(player, range) {
const dim = player.dimension;
const head = player.getHeadLocation();
const dir = player.getViewDirection();
// Walk integer-block steps along the view ray. Resolution 0.5 blocks
// catches ores the cardinal-aligned steps would skip on diagonals.
const stepCount = Math.floor(range * 2);
const seen = new Set();
let foundOre = null;
let foundDist = 0;
for (let i = 1; i <= stepCount; i++) {
const t = i * 0.5;
const px = head.x + dir.x * t;
const py = head.y + dir.y * t;
const pz = head.z + dir.z * t;
const bx = Math.floor(px);
const by = Math.floor(py);
const bz = Math.floor(pz);
const key = `${bx},${by},${bz}`;
if (seen.has(key)) continue;
seen.add(key);
let block;
try { block = dim.getBlock({ x: bx, y: by, z: bz }); } catch (_) { continue; }
if (!block) continue;
if (ORE_IDS.has(block.typeId)) {
if (isPrivateChestNearby(dim, bx, by, bz)) continue; // faraday
foundOre = block.typeId;
foundDist = t;
break;
}
}
if (foundOre) {
const distRounded = Math.round(foundDist * 10) / 10;
const pitch = Math.max(0.8, Math.min(2.0, 2.0 - (foundDist / range)));
try {
player.playSound("random.orb", { pitch, volume: 0.7 });
} catch (_) {}
try {
player.onScreenDisplay.setActionBar(`§a● §f${prettyOreName(foundOre)} §7at §b${distRounded}m`);
} catch (_) {}
} else {
try {
player.playSound("note.bass", { pitch: 0.7, volume: 0.5 });
} catch (_) {}
try {
player.onScreenDisplay.setActionBar(`§7○ No ores within §f${range}m`);
} catch (_) {}
}
}
world.afterEvents.itemUse.subscribe((event) => {
const player = event.source;
const stack = event.itemStack;
if (!stack || !player) return;
const range = DETECTOR_RANGES[stack.typeId];
if (!range) return;
system.run(() => runOreScan(player, range));
});
// ─── Boot ───────────────────────────────────────────────────
system.run(() => {
loadState();
world.sendMessage("§6[Camping] §7Camping Supplies loaded.");
});