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>
This commit is contained in:
2026-04-27 21:59:27 +01:00
parent 145f5d9beb
commit b9e3380f6c
12 changed files with 264 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
{
"format_version": "1.21.0",
"minecraft:item": {
"description": {
"identifier": "silverlabs:ore_detector_advanced",
"menu_category": {
"category": "equipment",
"group": "itemGroup.name.miscellaneous"
}
},
"components": {
"minecraft:max_stack_size": 1,
"minecraft:icon": "ore_detector_advanced",
"minecraft:hand_equipped": true
}
}
}

View File

@@ -0,0 +1,17 @@
{
"format_version": "1.21.0",
"minecraft:item": {
"description": {
"identifier": "silverlabs:ore_detector_basic",
"menu_category": {
"category": "equipment",
"group": "itemGroup.name.miscellaneous"
}
},
"components": {
"minecraft:max_stack_size": 1,
"minecraft:icon": "ore_detector_basic",
"minecraft:hand_equipped": true
}
}
}

View File

@@ -0,0 +1,17 @@
{
"format_version": "1.21.0",
"minecraft:item": {
"description": {
"identifier": "silverlabs:ore_detector_improved",
"menu_category": {
"category": "equipment",
"group": "itemGroup.name.miscellaneous"
}
},
"components": {
"minecraft:max_stack_size": 1,
"minecraft:icon": "ore_detector_improved",
"minecraft:hand_equipped": true
}
}
}

View File

@@ -0,0 +1,27 @@
{
"format_version": "1.21.0",
"minecraft:recipe_shapeless": {
"description": {
"identifier": "silverlabs:ore_detector_advanced_recipe"
},
"tags": [
"crafting_table"
],
"unlock": [
{
"item": "silverlabs:ore_detector_improved"
}
],
"ingredients": [
{ "item": "silverlabs:ore_detector_improved" },
{ "item": "minecraft:gold_ingot" },
{ "item": "minecraft:gold_ingot" },
{ "item": "minecraft:amethyst_shard" },
{ "item": "minecraft:amethyst_shard" }
],
"result": {
"item": "silverlabs:ore_detector_advanced",
"count": 1
}
}
}

View File

@@ -0,0 +1,36 @@
{
"format_version": "1.21.0",
"minecraft:recipe_shaped": {
"description": {
"identifier": "silverlabs:ore_detector_basic_recipe"
},
"tags": [
"crafting_table"
],
"unlock": [
{
"item": "minecraft:redstone"
}
],
"pattern": [
" R ",
"CSC",
" C "
],
"key": {
"R": {
"item": "minecraft:redstone"
},
"C": {
"item": "minecraft:cobblestone"
},
"S": {
"item": "minecraft:stick"
}
},
"result": {
"item": "silverlabs:ore_detector_basic",
"count": 1
}
}
}

View File

@@ -0,0 +1,28 @@
{
"format_version": "1.21.0",
"minecraft:recipe_shapeless": {
"description": {
"identifier": "silverlabs:ore_detector_improved_recipe"
},
"tags": [
"crafting_table"
],
"unlock": [
{
"item": "silverlabs:ore_detector_basic"
}
],
"ingredients": [
{ "item": "silverlabs:ore_detector_basic" },
{ "item": "minecraft:iron_ingot" },
{ "item": "minecraft:iron_ingot" },
{ "item": "minecraft:iron_ingot" },
{ "item": "minecraft:iron_ingot" },
{ "item": "minecraft:redstone" }
],
"result": {
"item": "silverlabs:ore_detector_improved",
"count": 1
}
}
}

View File

@@ -703,6 +703,116 @@ function dismantleHammockAt(loc, dimId, player) {
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();