Files
minecraft-aiworld/camping-supplies-addon/camping_supplies_BP/scripts/main.js
SysAdmin 60603ab74c
All checks were successful
Deploy Addons / deploy (push) Successful in 15s
fix(camping): scan-down now uses air/liquid check + hammock camera pull
The tent's scan-down-for-ground loop still used b.isSolid (undefined
in BDS), so it always fell through the break conditions and
decremented feetY by 3 blocks before checking ground. That's why
clicking grass reported "dirt" and stone reported "dirt/stone from
underneath the surface". Replaced both solid checks with
!isAir && !isLiquid like the main ground check.

Also added a cinematic third-person camera when climbing into the
hammock (ease 0.8s out_sine), cleared on exit. Gives the player a
"lying back, can see myself" view while resting.

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

513 lines
18 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);
// 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;
}
}
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.");
}
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;
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.");
});