All checks were successful
Deploy Addons / deploy (push) Successful in 14s
- camping: replace cube tent with A-frame slope panels (tent_panel_l/r) + cardinal_direction permutations; vote-skip sleep that mixes player.isSleeping bed sleepers with tent occupants and respects the playersSleepingPercentage gamerule; new weathered-canvas texture. - lobby: walk-through silverlabs:portal_field block (no collision, translucent swirl, cross-plane geo) auto-placed above each portal frame; invisible silverlabs:portal_label entity floats above each portal with the destination world name; transfer detection now scans down through the field to find the destination frame. - postal: regenerate post_office and mailbox block textures so they fill the full block face (brick + POST plaque, full red panel with slot/latch /flag/rivets) instead of small sprites floating on transparent. - dynamite + tow-boat: ship the addons (volumes wired into all four worlds; enabled_packs registers them into Mya's world). - art: build-textures.py extended; build-art-catalog.py added to project. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
710 lines
25 KiB
JavaScript
710 lines
25 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"; // 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.
|
||
// w=0 is the player's column → LEFT side of the tent (panel_l).
|
||
// w=1 is one step right → RIGHT side of the tent (panel_r).
|
||
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_L,
|
||
});
|
||
canvasCells.push({
|
||
x: ox + l * fx + rx,
|
||
y: oy,
|
||
z: oz + l * fz + rz,
|
||
block: TENT_PANEL_R,
|
||
});
|
||
}
|
||
|
||
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.");
|
||
}
|
||
|
||
// ─── Boot ───────────────────────────────────────────────────
|
||
system.run(() => {
|
||
loadState();
|
||
world.sendMessage("§6[Camping] §7Camping Supplies loaded.");
|
||
});
|