Files
minecraft-aiworld/camping-supplies-addon/camping_supplies_BP/scripts/main.js
SysAdmin 8b83e324f0
All checks were successful
Deploy Addons / deploy (push) Successful in 13s
feat(camping): add craftable tent and hammock addon
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>
2026-04-23 23:10:21 +01:00

443 lines
14 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";
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 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 (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.");
});