Files
minecraft-aiworld/camping-supplies-addon/camping_supplies_BP/scripts/main.js
SysAdmin 77a7524917
All checks were successful
Deploy Addons / deploy (push) Successful in 23s
fix(camping): swap tent panel L/R to render apex /\ not \/
Bedrock mirrors the panel geometry across the block's local X axis
relative to the .geo.json, so the player's column needs panel_r and
the adjacent column needs panel_l for the roof seam to peak correctly.

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

711 lines
25 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.");
}
// ─── Boot ───────────────────────────────────────────────────
system.run(() => {
loadState();
world.sendMessage("§6[Camping] §7Camping Supplies loaded.");
});