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 air (player's mid-jump or location rounded up), // scan downward up to 3 blocks to find the ground. for (let probe = 0; probe < 4; probe++) { const here = dim.getBlock({ x: feetX, y: feetY - 1, z: feetZ }); if (here && here.isSolid) break; const onSolid = dim.getBlock({ x: feetX, y: feetY, z: feetZ }); if (onSolid && onSolid.isSolid) { feetY += 1; 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 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."); } 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 (_) {} 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 (_) {} 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."); });