All checks were successful
Deploy Addons / deploy (push) Successful in 13s
Tent pitches over a 2x3 flat footprint and lets players skip to dawn without touching their spawn point. Hammock strings between two posts 3-6 blocks apart (straight or diagonal, +/-1 block height) and keeps hostile mobs at bay while occupied. Both are craftable (wool/sticks and wool/string) and mounted in all four worlds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
443 lines
14 KiB
JavaScript
443 lines
14 KiB
JavaScript
import { world, system, ItemStack } from "@minecraft/server";
|
||
|
||
// ─── Constants ──────────────────────────────────────────────
|
||
const TENT_ITEM = "silverlabs:tent";
|
||
const HAMMOCK_ITEM = "silverlabs:hammock";
|
||
const TENT_BLOCK = "silverlabs:tent_canvas";
|
||
const HAMMOCK_BLOCK = "silverlabs:hammock_cloth";
|
||
const STATE_PROP = "camping_state_v1";
|
||
const HAMMOCK_TAG = "camping_hammock";
|
||
|
||
// ─── 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";
|
||
}
|
||
|
||
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);
|
||
const feet = {
|
||
x: Math.floor(player.location.x),
|
||
y: Math.floor(player.location.y),
|
||
z: Math.floor(player.location.z),
|
||
};
|
||
const ox = feet.x + fx;
|
||
const oy = feet.y;
|
||
const oz = feet.z + 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 <= 2; h++) clearCells.push({ x: cx, y: oy + h, z: cz });
|
||
}
|
||
}
|
||
|
||
for (const g of groundCells) {
|
||
const b = dim.getBlock(g);
|
||
if (!b || !b.isSolid) {
|
||
player.sendMessage("§c[Camping] §7Need a flat 2×3 patch of solid ground in front of you.");
|
||
return false;
|
||
}
|
||
}
|
||
for (const c of clearCells) {
|
||
const b = dim.getBlock(c);
|
||
if (!b) {
|
||
player.sendMessage("§c[Camping] §7Can't reach that area (chunk unloaded).");
|
||
return false;
|
||
}
|
||
if (!b.isAir && !b.isLiquid) {
|
||
player.sendMessage("§c[Camping] §7The space above the tent footprint isn't clear.");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
const canvasCells = [];
|
||
for (let w = 0; w < 2; w++) {
|
||
// Back wall at l=0 (Y=0 and Y=1)
|
||
canvasCells.push({ x: ox + w * rx, y: oy + 0, z: oz + w * rz });
|
||
canvasCells.push({ x: ox + w * rx, y: oy + 1, z: oz + w * rz });
|
||
// Roof at l=1 and l=2 (Y=1)
|
||
canvasCells.push({ x: ox + 1 * fx + w * rx, y: oy + 1, z: oz + 1 * fz + w * rz });
|
||
canvasCells.push({ x: ox + 2 * fx + w * rx, y: oy + 1, z: oz + 2 * fz + w * rz });
|
||
}
|
||
|
||
for (const c of canvasCells) {
|
||
try {
|
||
dim.runCommand(`setblock ${c.x} ${c.y} ${c.z} ${TENT_BLOCK}`);
|
||
} catch (_) {}
|
||
}
|
||
|
||
const key = keyOf(ox, oy, oz, dim.id);
|
||
state.tents[key] = {
|
||
ownerId: player.id,
|
||
ownerName: player.name,
|
||
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 (block.typeId === TENT_BLOCK) {
|
||
event.cancel = true;
|
||
const player = event.player;
|
||
system.run(() => sleepInTent(player));
|
||
} 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}`);
|
||
}
|
||
|
||
function sleepInTent(player) {
|
||
const tod = world.getTimeOfDay();
|
||
if (tod < 12500 && tod > 500) {
|
||
player.sendMessage("§e[Camping] §7It's still daylight — nothing to sleep off.");
|
||
return;
|
||
}
|
||
player.addEffect("regeneration", 200, { amplifier: 1, showParticles: false });
|
||
player.addEffect("saturation", 40, { amplifier: 0, showParticles: false });
|
||
world.setTimeOfDay(0);
|
||
player.sendMessage("§a[Camping] §7You rest until dawn. §8Spawn point unchanged.");
|
||
}
|
||
|
||
function toggleHammock(player, loc) {
|
||
if (player.hasTag(HAMMOCK_TAG)) {
|
||
player.removeTag(HAMMOCK_TAG);
|
||
player.sendMessage("§7[Camping] You climb out of the hammock.");
|
||
return;
|
||
}
|
||
player.addTag(HAMMOCK_TAG);
|
||
try {
|
||
player.teleport(
|
||
{ x: loc.x + 0.5, y: loc.y + 0.4, z: loc.z + 0.5 },
|
||
{ dimension: player.dimension }
|
||
);
|
||
} catch (_) {}
|
||
player.sendMessage("§a[Camping] §7You settle into the hammock. Wild creatures don't notice you. §8(Sneak to climb out.)");
|
||
}
|
||
|
||
// ─── Hammock upkeep loop: mob repulsion + sneak-exit ────────
|
||
system.runInterval(() => {
|
||
for (const player of world.getAllPlayers()) {
|
||
if (!player.hasTag(HAMMOCK_TAG)) continue;
|
||
if (player.isSneaking) {
|
||
player.removeTag(HAMMOCK_TAG);
|
||
player.sendMessage("§7[Camping] You climb out of the hammock.");
|
||
continue;
|
||
}
|
||
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;
|
||
if (id !== TENT_BLOCK && 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 (id === TENT_BLOCK) {
|
||
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.");
|
||
});
|