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 3–6 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."); });