feat(hub-return): add lodestone waypoints with compass menu and HUD guidance
All checks were successful
Deploy Addons / deploy (push) Successful in 40s
All checks were successful
Deploy Addons / deploy (push) Successful in 40s
Right-clicking the recovery compass now opens an ActionForm menu with "Return to Hub" plus any lodestone waypoints the player has placed in the current dimension. Placing a lodestone prompts for a label and saves it under the waypoints_v1 world dynamic property (max 10 per player). Selected waypoints drive an on-screen actionbar HUD with distance and an 8-direction arrow, clearing on arrival within 3 blocks. Lodestone breaks are ownership-gated and drop the block back. Bumps pack to 1.0.5 and declares the @minecraft/server-ui dependency required by the new ActionForm/Modal/MessageForm flows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
"name": "Hub Return Transfer",
|
||||
"description": "Transfers players back to lobby when they step on the return portal",
|
||||
"uuid": "b2c3d4e5-1111-2222-3333-fedcba654321",
|
||||
"version": [1, 0, 4],
|
||||
"version": [1, 0, 5],
|
||||
"min_engine_version": [1, 21, 0]
|
||||
},
|
||||
"modules": [
|
||||
@@ -12,7 +12,7 @@
|
||||
"type": "script",
|
||||
"language": "javascript",
|
||||
"uuid": "b2c3d4e5-4444-5555-6666-fedcba987654",
|
||||
"version": [1, 0, 4],
|
||||
"version": [1, 0, 5],
|
||||
"entry": "scripts/main.js"
|
||||
}
|
||||
],
|
||||
@@ -24,6 +24,10 @@
|
||||
{
|
||||
"module_name": "@minecraft/server-admin",
|
||||
"version": "1.0.0-beta"
|
||||
},
|
||||
{
|
||||
"module_name": "@minecraft/server-ui",
|
||||
"version": "1.3.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,19 +1,82 @@
|
||||
import { world, system, ItemStack } from "@minecraft/server";
|
||||
import { transferPlayer, variables } from "@minecraft/server-admin";
|
||||
import { ActionFormData, ModalFormData, MessageFormData } from "@minecraft/server-ui";
|
||||
|
||||
const LOBBY_HOST = "10.0.0.247";
|
||||
const LOBBY_PORT = 19132;
|
||||
const COMPASS_ID = "minecraft:recovery_compass";
|
||||
const MARKER_BLOCK = "minecraft:lodestone";
|
||||
const PORTAL_PROP = "hub_portal_location";
|
||||
const WAYPOINTS_PROP = "waypoints_v1";
|
||||
const PORTAL_RADIUS = 3;
|
||||
const TRANSFER_COOLDOWN = 5000; // 5 seconds
|
||||
const SPAWN_PROTECTION = 10000; // 10 seconds — ignore portal detection after spawn
|
||||
const TRANSFER_COOLDOWN = 5000;
|
||||
const SPAWN_PROTECTION = 10000;
|
||||
|
||||
const MAX_MARKERS_PER_PLAYER = 10;
|
||||
const ARRIVAL_RADIUS = 3;
|
||||
const HUD_TICK_INTERVAL = 5;
|
||||
const ARROWS = ["\u2191", "\u2197", "\u2192", "\u2198", "\u2193", "\u2199", "\u2190", "\u2196"];
|
||||
|
||||
// Track recently transferred players
|
||||
const recentTransfers = new Map();
|
||||
// Track when players spawned (to prevent transfer loop on arrival)
|
||||
const spawnTimes = new Map();
|
||||
|
||||
// ─── Waypoint State ─────────────────────────────────────────────
|
||||
|
||||
// Persisted: markers per player { [ownerId]: [ {x,y,z,dim,label,key}, ... ] }
|
||||
let markers = {};
|
||||
// Session-only: which marker each player is actively guiding toward
|
||||
const active = new Map();
|
||||
|
||||
function keyOf(loc, dimensionId) {
|
||||
return `${Math.floor(loc.x)},${Math.floor(loc.y)},${Math.floor(loc.z)},${dimensionId}`;
|
||||
}
|
||||
|
||||
function loadWaypoints() {
|
||||
try {
|
||||
const raw = world.getDynamicProperty(WAYPOINTS_PROP);
|
||||
if (raw && typeof raw === "string") markers = JSON.parse(raw);
|
||||
} catch (e) {
|
||||
world.sendMessage(`§c[Waypoints] Failed to load: ${e.message}`);
|
||||
markers = {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveWaypoints() {
|
||||
try {
|
||||
world.setDynamicProperty(WAYPOINTS_PROP, JSON.stringify(markers));
|
||||
} catch (e) {
|
||||
world.sendMessage(`§c[Waypoints] Failed to save: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getMarkers(ownerId) {
|
||||
if (!markers[ownerId]) markers[ownerId] = [];
|
||||
return markers[ownerId];
|
||||
}
|
||||
|
||||
function findMarkerByKey(key) {
|
||||
for (const ownerId of Object.keys(markers)) {
|
||||
const list = markers[ownerId];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i].key === key) return { ownerId, index: i, marker: list[i] };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function removeMarker(ownerId, index) {
|
||||
const list = getMarkers(ownerId);
|
||||
list.splice(index, 1);
|
||||
const activeIdx = active.get(ownerId);
|
||||
if (activeIdx != null) {
|
||||
if (activeIdx === index) active.delete(ownerId);
|
||||
else if (activeIdx > index) active.set(ownerId, activeIdx - 1);
|
||||
}
|
||||
saveWaypoints();
|
||||
}
|
||||
|
||||
// ─── Hub Transfer ───────────────────────────────────────────────
|
||||
|
||||
function doTransfer(player) {
|
||||
const now = Date.now();
|
||||
if (recentTransfers.has(player.name) && now - recentTransfers.get(player.name) < TRANSFER_COOLDOWN) {
|
||||
@@ -24,8 +87,6 @@ function doTransfer(player) {
|
||||
recentTransfers.set(player.name, now);
|
||||
world.sendMessage(`§a${player.name} is returning to the Hub...`);
|
||||
|
||||
// Teleport player away from return portal before transfer so their saved
|
||||
// position is NOT on the portal (prevents re-transfer loop on lobby arrival)
|
||||
const portalJson = world.getDynamicProperty(PORTAL_PROP);
|
||||
if (portalJson) {
|
||||
try {
|
||||
@@ -33,17 +94,15 @@ function doTransfer(player) {
|
||||
const pos = player.location;
|
||||
const dx = Math.abs(pos.x - portal.x);
|
||||
const dz = Math.abs(pos.z - portal.z);
|
||||
// Only teleport if player is near the return portal
|
||||
if (dx <= PORTAL_RADIUS + 1 && dz <= PORTAL_RADIUS + 1) {
|
||||
const safePos = { x: pos.x, y: pos.y, z: pos.z + 4 };
|
||||
player.teleport(safePos);
|
||||
}
|
||||
} catch (e) {
|
||||
// Non-fatal — proceed with transfer anyway
|
||||
// Non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
// Wait 5 ticks for position to save, then transfer
|
||||
system.runTimeout(() => {
|
||||
try {
|
||||
transferPlayer(player, { hostname: LOBBY_HOST, port: LOBBY_PORT });
|
||||
@@ -53,7 +112,7 @@ function doTransfer(player) {
|
||||
}, 5);
|
||||
}
|
||||
|
||||
// ─── A. Compass Distribution ────────────────────────────────────
|
||||
// ─── Compass Distribution ───────────────────────────────────────
|
||||
|
||||
function giveCompassIfMissing(player) {
|
||||
const inventory = player.getComponent("minecraft:inventory");
|
||||
@@ -62,48 +121,333 @@ function giveCompassIfMissing(player) {
|
||||
const container = inventory.container;
|
||||
for (let i = 0; i < container.size; i++) {
|
||||
const item = container.getItem(i);
|
||||
if (item && item.typeId === COMPASS_ID) return; // already has one
|
||||
if (item && item.typeId === COMPASS_ID) return;
|
||||
}
|
||||
|
||||
container.addItem(new ItemStack(COMPASS_ID, 1));
|
||||
player.sendMessage("§b[Hub] §fYou received a §dHub Compass§f — use it to return to the lobby!");
|
||||
player.sendMessage("§b[Hub] §fYou received a §dHub Compass§f — right-click to open the menu.");
|
||||
}
|
||||
|
||||
// Get the world name from server variables.json for the welcome message
|
||||
let WORLD_NAME = "this world";
|
||||
try {
|
||||
WORLD_NAME = variables.get("world_name") || "this world";
|
||||
} catch (e) {
|
||||
// variables.json may not exist — use fallback
|
||||
// variables.json may not exist
|
||||
}
|
||||
|
||||
world.afterEvents.playerSpawn.subscribe((event) => {
|
||||
const player = event.player;
|
||||
// Mark spawn time for portal protection (prevents transfer loop on arrival)
|
||||
spawnTimes.set(player.name, Date.now());
|
||||
// Show welcome title and give compass after a short delay
|
||||
system.runTimeout(() => {
|
||||
try {
|
||||
player.runCommand(`titleraw @s title {"rawtext":[{"text":"§6Welcome!"}]}`);
|
||||
player.runCommand(`titleraw @s subtitle {"rawtext":[{"text":"§7You are now in §e${WORLD_NAME}"}]}`);
|
||||
giveCompassIfMissing(player);
|
||||
} catch (e) {
|
||||
// Silently ignore — player may have disconnected
|
||||
// Player may have disconnected
|
||||
}
|
||||
}, 20);
|
||||
});
|
||||
|
||||
// ─── B. Compass Use Transfer ────────────────────────────────────
|
||||
world.afterEvents.playerLeave.subscribe((event) => {
|
||||
// Clear session HUD state for whoever left (keyed by id)
|
||||
// event.playerId exists; event.playerName for compat
|
||||
if (event.playerId) active.delete(event.playerId);
|
||||
});
|
||||
|
||||
// ─── Compass Menu ───────────────────────────────────────────────
|
||||
|
||||
world.afterEvents.itemUse.subscribe((event) => {
|
||||
const player = event.source;
|
||||
const item = event.itemStack;
|
||||
|
||||
if (item.typeId !== COMPASS_ID) return;
|
||||
doTransfer(player);
|
||||
if (event.itemStack.typeId !== COMPASS_ID) return;
|
||||
system.run(() => openCompassMenu(player));
|
||||
});
|
||||
|
||||
// ─── C. Portal Zone Detection ───────────────────────────────────
|
||||
async function openCompassMenu(player) {
|
||||
const list = getMarkers(player.id);
|
||||
const dimId = player.dimension.id;
|
||||
const visible = list
|
||||
.map((m, i) => ({ m, i, dist: distanceXZ(player.location, m) }))
|
||||
.filter(({ m }) => m.dim === dimId);
|
||||
|
||||
const form = new ActionFormData()
|
||||
.title("Hub Compass")
|
||||
.body(`§7Waypoints: §f${list.length}§7 / ${MAX_MARKERS_PER_PLAYER}`);
|
||||
|
||||
const actions = [];
|
||||
form.button("\uD83C\uDFE0 Return to Hub");
|
||||
actions.push({ kind: "hub" });
|
||||
|
||||
for (const { m, i, dist } of visible) {
|
||||
form.button(`§f${m.label}\n§7${Math.round(dist)}m away`);
|
||||
actions.push({ kind: "select", index: i });
|
||||
}
|
||||
|
||||
const activeIdx = active.get(player.id);
|
||||
if (activeIdx != null && list[activeIdx]) {
|
||||
form.button(`§c\u274C Clear active waypoint`);
|
||||
actions.push({ kind: "clear" });
|
||||
}
|
||||
|
||||
if (list.length > 0) {
|
||||
form.button("\uD83D\uDDD1 Delete a waypoint…");
|
||||
actions.push({ kind: "delete_menu" });
|
||||
}
|
||||
|
||||
form.button("\uD83E\uDDED Get a marker block");
|
||||
actions.push({ kind: "give_marker" });
|
||||
|
||||
form.button("§7Cancel");
|
||||
actions.push({ kind: "cancel" });
|
||||
|
||||
let res;
|
||||
try { res = await form.show(player); } catch (_) { return; }
|
||||
if (res.canceled || res.selection === undefined) return;
|
||||
|
||||
const a = actions[res.selection];
|
||||
if (!a || a.kind === "cancel") return;
|
||||
|
||||
if (a.kind === "hub") {
|
||||
doTransfer(player);
|
||||
return;
|
||||
}
|
||||
if (a.kind === "select") {
|
||||
active.set(player.id, a.index);
|
||||
const m = list[a.index];
|
||||
player.sendMessage(`§b[Waypoints] §fGuiding you to §d${m.label}§f.`);
|
||||
return;
|
||||
}
|
||||
if (a.kind === "clear") {
|
||||
active.delete(player.id);
|
||||
player.onScreenDisplay.setActionBar("");
|
||||
player.sendMessage("§b[Waypoints] §fActive waypoint cleared.");
|
||||
return;
|
||||
}
|
||||
if (a.kind === "delete_menu") {
|
||||
await openDeleteMenu(player);
|
||||
return;
|
||||
}
|
||||
if (a.kind === "give_marker") {
|
||||
giveMarkerBlock(player);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function giveMarkerBlock(player) {
|
||||
try {
|
||||
const inv = player.getComponent("minecraft:inventory")?.container;
|
||||
if (!inv) {
|
||||
player.sendMessage("§c[Waypoints] §fCouldn't access your inventory.");
|
||||
return;
|
||||
}
|
||||
const leftover = inv.addItem(new ItemStack(MARKER_BLOCK, 1));
|
||||
if (leftover) {
|
||||
const pos = player.location;
|
||||
player.dimension.spawnItem(new ItemStack(MARKER_BLOCK, 1), { x: pos.x, y: pos.y + 1, z: pos.z });
|
||||
player.sendMessage("§b[Waypoints] §fInventory full — dropped a §dlodestone§f at your feet.");
|
||||
} else {
|
||||
player.sendMessage("§b[Waypoints] §fHere's a §dlodestone§f — place it to set a waypoint.");
|
||||
}
|
||||
} catch (e) {
|
||||
player.sendMessage(`§c[Waypoints] §fCouldn't give lodestone: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function openDeleteMenu(player) {
|
||||
const list = getMarkers(player.id);
|
||||
if (list.length === 0) return;
|
||||
|
||||
const form = new ActionFormData()
|
||||
.title("Delete a Waypoint")
|
||||
.body("§7Pick a waypoint to remove. The lodestone block stays — break it to clean up.");
|
||||
|
||||
for (const m of list) {
|
||||
form.button(`§f${m.label}\n§7${m.x}, ${m.y}, ${m.z}`);
|
||||
}
|
||||
form.button("§7Cancel");
|
||||
|
||||
let res;
|
||||
try { res = await form.show(player); } catch (_) { return; }
|
||||
if (res.canceled || res.selection === undefined) return;
|
||||
if (res.selection >= list.length) return;
|
||||
|
||||
const chosen = list[res.selection];
|
||||
const confirm = new MessageFormData()
|
||||
.title("Confirm Delete")
|
||||
.body(`Remove waypoint §f${chosen.label}§r?`)
|
||||
.button1("Delete")
|
||||
.button2("Cancel");
|
||||
|
||||
let cf;
|
||||
try { cf = await confirm.show(player); } catch (_) { return; }
|
||||
if (cf.canceled || cf.selection !== 0) return;
|
||||
|
||||
removeMarker(player.id, res.selection);
|
||||
player.sendMessage(`§b[Waypoints] §fDeleted §d${chosen.label}§f.`);
|
||||
}
|
||||
|
||||
// ─── Placement → Label Prompt ───────────────────────────────────
|
||||
|
||||
world.afterEvents.playerPlaceBlock.subscribe((event) => {
|
||||
if (event.block.typeId !== MARKER_BLOCK) return;
|
||||
const player = event.player;
|
||||
const block = event.block;
|
||||
const dim = block.dimension;
|
||||
const loc = { x: block.location.x, y: block.location.y, z: block.location.z };
|
||||
|
||||
const list = getMarkers(player.id);
|
||||
if (list.length >= MAX_MARKERS_PER_PLAYER) {
|
||||
system.run(() => {
|
||||
try {
|
||||
dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`);
|
||||
dim.spawnItem(new ItemStack(MARKER_BLOCK, 1), { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 });
|
||||
} catch (_) {}
|
||||
player.sendMessage(`§c[Waypoints] §fMax of ${MAX_MARKERS_PER_PLAYER} waypoints reached — delete one first.`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
system.run(() => promptForLabel(player, loc, dim.id, list.length));
|
||||
});
|
||||
|
||||
async function promptForLabel(player, loc, dimId, nextIndex) {
|
||||
const form = new ModalFormData()
|
||||
.title("Name this Waypoint")
|
||||
.textField("Label", "e.g. Mine, Base, Farm", `Waypoint ${nextIndex + 1}`);
|
||||
|
||||
let res;
|
||||
try { res = await form.show(player); } catch (_) { return; }
|
||||
if (res.canceled) {
|
||||
// Still register with default label so the physical lodestone isn't orphaned
|
||||
saveMarker(player, loc, dimId, `Waypoint ${nextIndex + 1}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const raw = (res.formValues && res.formValues[0]) || "";
|
||||
const label = String(raw).trim().slice(0, 24) || `Waypoint ${nextIndex + 1}`;
|
||||
saveMarker(player, loc, dimId, label);
|
||||
}
|
||||
|
||||
function saveMarker(player, loc, dimId, label) {
|
||||
const list = getMarkers(player.id);
|
||||
const key = keyOf(loc, dimId);
|
||||
if (list.some((m) => m.key === key)) return; // already registered (e.g. re-placed)
|
||||
list.push({
|
||||
x: Math.floor(loc.x),
|
||||
y: Math.floor(loc.y),
|
||||
z: Math.floor(loc.z),
|
||||
dim: dimId,
|
||||
label,
|
||||
key,
|
||||
});
|
||||
saveWaypoints();
|
||||
player.sendMessage(`§b[Waypoints] §fSaved §d${label}§f — select it from the compass to navigate.`);
|
||||
}
|
||||
|
||||
// ─── Break → Unregister ─────────────────────────────────────────
|
||||
|
||||
try {
|
||||
world.beforeEvents.playerBreakBlock.subscribe((event) => {
|
||||
const block = event.block;
|
||||
if (block.typeId !== MARKER_BLOCK) return;
|
||||
|
||||
const key = keyOf(block.location, block.dimension.id);
|
||||
const hit = findMarkerByKey(key);
|
||||
if (!hit) return; // untracked lodestone — normal vanilla break
|
||||
|
||||
const player = event.player;
|
||||
if (hit.ownerId !== player.id) {
|
||||
event.cancel = true;
|
||||
const ownerName = findOwnerName(hit.ownerId);
|
||||
system.run(() =>
|
||||
player.sendMessage(
|
||||
`§c[Waypoints] §7This waypoint belongs to §f${ownerName}§7. You can't break it.`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
event.cancel = true;
|
||||
const loc = { x: block.location.x, y: block.location.y, z: block.location.z };
|
||||
const dim = block.dimension;
|
||||
const label = hit.marker.label;
|
||||
system.run(() => {
|
||||
try {
|
||||
const dropPos = { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 };
|
||||
dim.spawnItem(new ItemStack(MARKER_BLOCK, 1), dropPos);
|
||||
dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`);
|
||||
} catch (e) {
|
||||
player.sendMessage(`§c[Waypoints] Error during break: ${e.message}`);
|
||||
}
|
||||
removeMarker(hit.ownerId, hit.index);
|
||||
player.sendMessage(`§b[Waypoints] §fRemoved §d${label}§f.`);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`[Waypoints] beforeEvents.playerBreakBlock unavailable: ${e}`);
|
||||
}
|
||||
|
||||
function findOwnerName(ownerId) {
|
||||
for (const p of world.getAllPlayers()) if (p.id === ownerId) return p.name;
|
||||
return "another player";
|
||||
}
|
||||
|
||||
// ─── Guidance HUD ───────────────────────────────────────────────
|
||||
|
||||
function distanceXZ(a, b) {
|
||||
const dx = (b.x + 0.5) - a.x;
|
||||
const dz = (b.z + 0.5) - a.z;
|
||||
return Math.sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
|
||||
system.runInterval(() => {
|
||||
for (const player of world.getAllPlayers()) {
|
||||
const idx = active.get(player.id);
|
||||
if (idx == null) continue;
|
||||
|
||||
const list = markers[player.id];
|
||||
const m = list && list[idx];
|
||||
if (!m) {
|
||||
active.delete(player.id);
|
||||
continue;
|
||||
}
|
||||
if (m.dim !== player.dimension.id) {
|
||||
player.onScreenDisplay.setActionBar(`§b\u27A4 §f${m.label} §7• §cdifferent dimension`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const pos = player.location;
|
||||
const dx = (m.x + 0.5) - pos.x;
|
||||
const dz = (m.z + 0.5) - pos.z;
|
||||
const distXZ = Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
if (distXZ <= ARRIVAL_RADIUS) {
|
||||
active.delete(player.id);
|
||||
try {
|
||||
player.onScreenDisplay.setTitle(`§aArrived`, { subtitle: `§f${m.label}`, fadeInDuration: 5, stayDuration: 30, fadeOutDuration: 10 });
|
||||
player.onScreenDisplay.setActionBar("");
|
||||
} catch (_) {}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bedrock yaw: 0 faces +Z (south), +yaw turns clockwise (toward -X, i.e. west).
|
||||
// Bearing to target in the same yaw convention:
|
||||
const bearingDeg = (Math.atan2(-dx, dz) * 180 / Math.PI + 360) % 360;
|
||||
let yaw;
|
||||
try { yaw = player.getRotation().y; } catch (_) { continue; }
|
||||
const relative = ((bearingDeg - yaw + 540) % 360) - 180; // -180..180
|
||||
const bucket = ((Math.round(relative / 45) % 8) + 8) % 8;
|
||||
const arrow = ARROWS[bucket];
|
||||
|
||||
try {
|
||||
player.onScreenDisplay.setActionBar(
|
||||
`§b\u27A4 §f${m.label} §7• §f${Math.round(distXZ)}m §8[§a${arrow}§8]`
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
}, HUD_TICK_INTERVAL);
|
||||
|
||||
// ─── Portal Zone Detection ──────────────────────────────────────
|
||||
|
||||
system.runInterval(() => {
|
||||
const portalJson = world.getDynamicProperty(PORTAL_PROP);
|
||||
@@ -120,14 +464,8 @@ system.runInterval(() => {
|
||||
const players = world.getAllPlayers();
|
||||
|
||||
for (const player of players) {
|
||||
// Skip if recently transferred (cooldown)
|
||||
if (recentTransfers.has(player.name) && now - recentTransfers.get(player.name) < TRANSFER_COOLDOWN) {
|
||||
continue;
|
||||
}
|
||||
// Skip if player just spawned (prevents loop when arriving from lobby)
|
||||
if (spawnTimes.has(player.name) && now - spawnTimes.get(player.name) < SPAWN_PROTECTION) {
|
||||
continue;
|
||||
}
|
||||
if (recentTransfers.has(player.name) && now - recentTransfers.get(player.name) < TRANSFER_COOLDOWN) continue;
|
||||
if (spawnTimes.has(player.name) && now - spawnTimes.get(player.name) < SPAWN_PROTECTION) continue;
|
||||
|
||||
const pos = player.location;
|
||||
const dx = Math.abs(pos.x - portal.x);
|
||||
@@ -140,7 +478,7 @@ system.runInterval(() => {
|
||||
}
|
||||
}, 20);
|
||||
|
||||
// ─── D. Chat Commands ───────────────────────────────────────────
|
||||
// ─── Chat Commands ──────────────────────────────────────────────
|
||||
|
||||
function handleChatCommand(player, msg) {
|
||||
if (msg === "!compass") {
|
||||
@@ -155,11 +493,7 @@ function handleChatCommand(player, msg) {
|
||||
|
||||
if (msg === "!setportal") {
|
||||
const pos = player.location;
|
||||
const loc = {
|
||||
x: Math.floor(pos.x),
|
||||
y: Math.floor(pos.y),
|
||||
z: Math.floor(pos.z),
|
||||
};
|
||||
const loc = { x: Math.floor(pos.x), y: Math.floor(pos.y), z: Math.floor(pos.z) };
|
||||
world.setDynamicProperty(PORTAL_PROP, JSON.stringify(loc));
|
||||
buildPortal(loc, player.dimension);
|
||||
player.sendMessage(`§b[Hub] §fReturn portal set at §d${loc.x}, ${loc.y}, ${loc.z}§f!`);
|
||||
@@ -171,10 +505,37 @@ function handleChatCommand(player, msg) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg === "!waypoints") {
|
||||
system.run(() => openCompassMenu(player));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg === "!marker") {
|
||||
giveMarkerBlock(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg === "!clearwaypoints") {
|
||||
system.run(async () => {
|
||||
const confirm = new MessageFormData()
|
||||
.title("Clear all waypoints?")
|
||||
.body("§7This deletes every waypoint you've saved. Lodestone blocks stay in the world — break them to reclaim.")
|
||||
.button1("§cDelete all")
|
||||
.button2("Cancel");
|
||||
let res;
|
||||
try { res = await confirm.show(player); } catch (_) { return; }
|
||||
if (res.canceled || res.selection !== 0) return;
|
||||
markers[player.id] = [];
|
||||
active.delete(player.id);
|
||||
saveWaypoints();
|
||||
player.sendMessage("§b[Waypoints] §fAll your waypoints were cleared.");
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Listen for chat commands — try beforeEvents first (beta API), fall back gracefully
|
||||
try {
|
||||
world.beforeEvents.chatSend.subscribe((event) => {
|
||||
const msg = event.message.trim().toLowerCase();
|
||||
@@ -185,12 +546,9 @@ try {
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// beforeEvents.chatSend requires beta API — chat commands won't work via chat,
|
||||
// but compass and portal transfers still function
|
||||
world.sendMessage("§e[Hub] §fChat commands (!hub, !lobby) unavailable — use compass or portal instead.");
|
||||
}
|
||||
|
||||
// scriptEvent fallback — players can use: /scriptevent hub:cmd hub
|
||||
system.afterEvents.scriptEventReceive.subscribe((event) => {
|
||||
if (event.id !== "hub:cmd") return;
|
||||
const player = event.sourceEntity;
|
||||
@@ -199,7 +557,7 @@ system.afterEvents.scriptEventReceive.subscribe((event) => {
|
||||
handleChatCommand(player, `!${msg}`);
|
||||
});
|
||||
|
||||
// ─── E. Portal Auto-Build on First Load ─────────────────────────
|
||||
// ─── Portal Auto-Build ──────────────────────────────────────────
|
||||
|
||||
function buildPortal(loc, dimension) {
|
||||
const x = loc.x;
|
||||
@@ -207,57 +565,39 @@ function buildPortal(loc, dimension) {
|
||||
const z = loc.z;
|
||||
|
||||
const commands = [];
|
||||
|
||||
// Clear space for portal (3 wide, 5 tall, 1 deep)
|
||||
commands.push(`fill ${x - 1} ${y} ${z} ${x + 1} ${y + 4} ${z} air`);
|
||||
|
||||
// Base row: 3 obsidian
|
||||
commands.push(`setblock ${x - 1} ${y} ${z} obsidian`);
|
||||
commands.push(`setblock ${x} ${y} ${z} obsidian`);
|
||||
commands.push(`setblock ${x + 1} ${y} ${z} obsidian`);
|
||||
|
||||
// Left column: 3 crying obsidian
|
||||
commands.push(`setblock ${x - 1} ${y + 1} ${z} crying_obsidian`);
|
||||
commands.push(`setblock ${x - 1} ${y + 2} ${z} crying_obsidian`);
|
||||
commands.push(`setblock ${x - 1} ${y + 3} ${z} crying_obsidian`);
|
||||
|
||||
// Right column: 3 crying obsidian
|
||||
commands.push(`setblock ${x + 1} ${y + 1} ${z} crying_obsidian`);
|
||||
commands.push(`setblock ${x + 1} ${y + 2} ${z} crying_obsidian`);
|
||||
commands.push(`setblock ${x + 1} ${y + 3} ${z} crying_obsidian`);
|
||||
|
||||
// Center: 3 purple stained glass
|
||||
commands.push(`setblock ${x} ${y + 1} ${z} stained_glass ["color":"purple"]`);
|
||||
commands.push(`setblock ${x} ${y + 2} ${z} stained_glass ["color":"purple"]`);
|
||||
commands.push(`setblock ${x} ${y + 3} ${z} stained_glass ["color":"purple"]`);
|
||||
|
||||
// Top: 1 obsidian cap
|
||||
commands.push(`setblock ${x} ${y + 4} ${z} obsidian`);
|
||||
|
||||
for (const cmd of commands) {
|
||||
try {
|
||||
dimension.runCommand(cmd);
|
||||
} catch (e) {
|
||||
// Block placement may fail if chunks not loaded — non-fatal
|
||||
// Non-fatal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
system.runTimeout(() => {
|
||||
const existing = world.getDynamicProperty(PORTAL_PROP);
|
||||
if (existing) return; // Portal location already set
|
||||
if (existing) return;
|
||||
|
||||
// Get spawn point and offset by 5 blocks on X
|
||||
const spawn = world.getDefaultSpawnLocation();
|
||||
const loc = {
|
||||
x: spawn.x + 5,
|
||||
y: spawn.y,
|
||||
z: spawn.z,
|
||||
};
|
||||
const loc = { x: spawn.x + 5, y: spawn.y, z: spawn.z };
|
||||
|
||||
world.setDynamicProperty(PORTAL_PROP, JSON.stringify(loc));
|
||||
|
||||
// Need a dimension reference — use overworld
|
||||
const overworld = world.getDimension("overworld");
|
||||
buildPortal(loc, overworld);
|
||||
|
||||
@@ -265,5 +605,6 @@ system.runTimeout(() => {
|
||||
}, 100);
|
||||
|
||||
system.run(() => {
|
||||
world.sendMessage("§b[World] §fHub return system loaded! Use compass, step on portal, or type §d!hub§f to return.");
|
||||
loadWaypoints();
|
||||
world.sendMessage("§b[World] §fHub return system loaded! Place a §dlodestone§f to set a waypoint; right-click your compass to navigate.");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user