All checks were successful
Deploy Addons / deploy (push) Successful in 16s
- New postal-service-addon: per-player mailboxes + post-office send block (ActionForm recipient picker, offline notification queue, chunk-load retry via tickingarea) - Commit previously untracked private-chest, home-sign, keep-inventory addons and their docker-compose mounts - Deploy workflow: add postal + previously unwired addons to path filter and checkout list; drop easter-egg from deployment - enabled_packs.json: register postal UUIDs for Lyla + Mya Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
159 lines
5.5 KiB
JavaScript
159 lines
5.5 KiB
JavaScript
import { world, system, ItemStack } from "@minecraft/server";
|
|
|
|
// ─── Constants ──────────────────────────────────────────────
|
|
const PRIVATE_BLOCK = "silverlabs:private_chest";
|
|
const VANILLA_CHEST = "minecraft:chest";
|
|
const PROP_KEY = "private_chests_v1";
|
|
|
|
// ─── State ──────────────────────────────────────────────────
|
|
// In-memory mirror of dynamic property: { "x,y,z,dim": { ownerId, ownerName } }
|
|
let chests = {};
|
|
|
|
function loadState() {
|
|
try {
|
|
const raw = world.getDynamicProperty(PROP_KEY);
|
|
if (raw && typeof raw === "string") {
|
|
chests = JSON.parse(raw);
|
|
}
|
|
} catch (e) {
|
|
world.sendMessage(`§c[Private Chest] Failed to load state: ${e.message}`);
|
|
chests = {};
|
|
}
|
|
}
|
|
|
|
function saveState() {
|
|
try {
|
|
world.setDynamicProperty(PROP_KEY, JSON.stringify(chests));
|
|
} catch (e) {
|
|
world.sendMessage(`§c[Private Chest] Failed to save state: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
function keyOf(loc, dimensionId) {
|
|
return `${Math.floor(loc.x)},${Math.floor(loc.y)},${Math.floor(loc.z)},${dimensionId}`;
|
|
}
|
|
|
|
function claim(loc, dimensionId, player) {
|
|
chests[keyOf(loc, dimensionId)] = { ownerId: player.id, ownerName: player.name };
|
|
saveState();
|
|
}
|
|
|
|
function release(loc, dimensionId) {
|
|
delete chests[keyOf(loc, dimensionId)];
|
|
saveState();
|
|
}
|
|
|
|
function getOwner(loc, dimensionId) {
|
|
return chests[keyOf(loc, dimensionId)] || null;
|
|
}
|
|
|
|
// ─── Chest facing from player rotation ──────────────────────
|
|
// Bedrock yaw: 0=south, 90=west, 180=north, -90=east. Chest front faces the player.
|
|
function chestFacing(yaw) {
|
|
let y = yaw;
|
|
while (y > 180) y -= 360;
|
|
while (y < -180) y += 360;
|
|
if (y >= -45 && y < 45) return "north"; // player facing south
|
|
if (y >= 45 && y < 135) return "east"; // player facing west
|
|
if (y >= -135 && y < -45) return "west"; // player facing east
|
|
return "south"; // player facing north
|
|
}
|
|
|
|
// ─── Placement: swap custom block → vanilla chest + claim ──
|
|
world.afterEvents.playerPlaceBlock.subscribe((event) => {
|
|
const block = event.block;
|
|
if (block.typeId !== PRIVATE_BLOCK) return;
|
|
|
|
const player = event.player;
|
|
const loc = block.location;
|
|
const dim = block.dimension;
|
|
const facing = chestFacing(player.getRotation().y);
|
|
|
|
try {
|
|
dim.runCommand(
|
|
`setblock ${loc.x} ${loc.y} ${loc.z} chest ["minecraft:cardinal_direction":"${facing}"]`
|
|
);
|
|
} catch (e) {
|
|
// Older block-state syntax fallback (rare)
|
|
try {
|
|
dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} chest`);
|
|
} catch (_) {}
|
|
}
|
|
|
|
claim(loc, dim.id, player);
|
|
player.sendMessage(`§6[Private Chest] §7Locked to you. Only you can open or break it.`);
|
|
});
|
|
|
|
// ─── Interact: block opening for non-owners ─────────────────
|
|
try {
|
|
world.beforeEvents.playerInteractWithBlock.subscribe((event) => {
|
|
const block = event.block;
|
|
if (block.typeId !== VANILLA_CHEST) return;
|
|
const owner = getOwner(block.location, block.dimension.id);
|
|
if (!owner) return;
|
|
if (owner.ownerId === event.player.id) return;
|
|
event.cancel = true;
|
|
const playerRef = event.player;
|
|
system.run(() =>
|
|
playerRef.sendMessage(`§c[Private Chest] §7This chest belongs to §f${owner.ownerName}§7.`)
|
|
);
|
|
});
|
|
} catch (e) {
|
|
console.warn(`[Private Chest] beforeEvents.playerInteractWithBlock unavailable: ${e}`);
|
|
}
|
|
|
|
// ─── Break: protect for non-owners; owner break drops the custom item back ──
|
|
try {
|
|
world.beforeEvents.playerBreakBlock.subscribe((event) => {
|
|
const block = event.block;
|
|
if (block.typeId !== VANILLA_CHEST) return;
|
|
const owner = getOwner(block.location, block.dimension.id);
|
|
if (!owner) return;
|
|
|
|
const player = event.player;
|
|
|
|
if (owner.ownerId !== player.id) {
|
|
event.cancel = true;
|
|
system.run(() =>
|
|
player.sendMessage(`§c[Private Chest] §7This chest belongs to §f${owner.ownerName}§7. You can't break it.`)
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Owner breaking — cancel default drop, manually eject contents + return the custom item.
|
|
event.cancel = true;
|
|
const loc = { x: block.location.x, y: block.location.y, z: block.location.z };
|
|
const dim = block.dimension;
|
|
|
|
system.run(() => {
|
|
try {
|
|
const inv = dim.getBlock(loc)?.getComponent("inventory");
|
|
const container = inv?.container;
|
|
const dropPos = { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 };
|
|
if (container) {
|
|
for (let i = 0; i < container.size; i++) {
|
|
const item = container.getItem(i);
|
|
if (item) {
|
|
dim.spawnItem(item, dropPos);
|
|
container.setItem(i, undefined);
|
|
}
|
|
}
|
|
}
|
|
dim.spawnItem(new ItemStack(PRIVATE_BLOCK, 1), dropPos);
|
|
dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`);
|
|
} catch (e) {
|
|
player.sendMessage(`§c[Private Chest] Error during break: ${e.message}`);
|
|
}
|
|
release(loc, dim.id);
|
|
});
|
|
});
|
|
} catch (e) {
|
|
console.warn(`[Private Chest] beforeEvents.playerBreakBlock unavailable: ${e}`);
|
|
}
|
|
|
|
// ─── Boot ──────────────────────────────────────────────────
|
|
system.run(() => {
|
|
loadState();
|
|
world.sendMessage("§6[Private Chest] §7Owner-locked chest system loaded.");
|
|
});
|