feat: add multi-world hub system with lobby portals and hub-return addon
Lobby addon detects players in portal zones at X: -15/0/15 and transfers them to Jamie/Lyla/Mya survival worlds. Hub-return addon gives players a recovery compass and chat commands (!hub, !lobby) to return to the lobby. Includes docker-compose.yml for 4 Bedrock servers (lobby + 3 child worlds), spark pet behavior/resource packs, and updated .gitignore. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
25
hub-return-addon/hub_return_transfer_BP/manifest.json
Normal file
25
hub-return-addon/hub_return_transfer_BP/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"format_version": 2,
|
||||
"header": {
|
||||
"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, 1],
|
||||
"min_engine_version": [1, 21, 0]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"type": "script",
|
||||
"language": "javascript",
|
||||
"uuid": "b2c3d4e5-4444-5555-6666-fedcba987654",
|
||||
"version": [1, 0, 1],
|
||||
"entry": "scripts/main.js"
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
{
|
||||
"module_name": "@minecraft/server",
|
||||
"version": "2.0.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
224
hub-return-addon/hub_return_transfer_BP/scripts/main.js
Normal file
224
hub-return-addon/hub_return_transfer_BP/scripts/main.js
Normal file
@@ -0,0 +1,224 @@
|
||||
import { world, system, ItemStack } from "@minecraft/server";
|
||||
|
||||
const LOBBY_HOST = "10.0.0.247";
|
||||
const LOBBY_PORT = 19132;
|
||||
const COMPASS_ID = "minecraft:recovery_compass";
|
||||
const PORTAL_PROP = "hub_portal_location";
|
||||
const PORTAL_RADIUS = 3;
|
||||
const TRANSFER_COOLDOWN = 5000; // 5 seconds
|
||||
|
||||
// Track recently transferred players
|
||||
const recentTransfers = new Map();
|
||||
|
||||
function doTransfer(player) {
|
||||
const now = Date.now();
|
||||
if (recentTransfers.has(player.name) && now - recentTransfers.get(player.name) < TRANSFER_COOLDOWN) {
|
||||
player.sendMessage("§c[Hub] §fPlease wait a moment before transferring again.");
|
||||
return;
|
||||
}
|
||||
|
||||
recentTransfers.set(player.name, now);
|
||||
world.sendMessage(`§a${player.name} is returning to the Hub...`);
|
||||
|
||||
try {
|
||||
player.runCommand(`transfer "${player.name}" ${LOBBY_HOST} ${LOBBY_PORT}`);
|
||||
} catch (e) {
|
||||
world.sendMessage(`§cTransfer failed: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── A. Compass Distribution ────────────────────────────────────
|
||||
|
||||
function giveCompassIfMissing(player) {
|
||||
const inventory = player.getComponent("minecraft:inventory");
|
||||
if (!inventory) return;
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
container.addItem(new ItemStack(COMPASS_ID, 1));
|
||||
player.sendMessage("§b[Hub] §fYou received a §dHub Compass§f — use it to return to the lobby!");
|
||||
}
|
||||
|
||||
world.afterEvents.playerSpawn.subscribe((event) => {
|
||||
const player = event.player;
|
||||
// Small delay to let inventory load
|
||||
system.runTimeout(() => {
|
||||
try {
|
||||
giveCompassIfMissing(player);
|
||||
} catch (e) {
|
||||
// Silently ignore — player may have disconnected
|
||||
}
|
||||
}, 20);
|
||||
});
|
||||
|
||||
// ─── B. Compass Use Transfer ────────────────────────────────────
|
||||
|
||||
world.afterEvents.itemUse.subscribe((event) => {
|
||||
const player = event.source;
|
||||
const item = event.itemStack;
|
||||
|
||||
if (item.typeId !== COMPASS_ID) return;
|
||||
doTransfer(player);
|
||||
});
|
||||
|
||||
// ─── C. Portal Zone Detection ───────────────────────────────────
|
||||
|
||||
system.runInterval(() => {
|
||||
const portalJson = world.getDynamicProperty(PORTAL_PROP);
|
||||
if (!portalJson) return;
|
||||
|
||||
let portal;
|
||||
try {
|
||||
portal = JSON.parse(portalJson);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const players = world.getAllPlayers();
|
||||
|
||||
for (const player of players) {
|
||||
if (recentTransfers.has(player.name) && now - recentTransfers.get(player.name) < TRANSFER_COOLDOWN) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pos = player.location;
|
||||
const dx = Math.abs(pos.x - portal.x);
|
||||
const dy = Math.abs(pos.y - portal.y);
|
||||
const dz = Math.abs(pos.z - portal.z);
|
||||
|
||||
if (dx <= PORTAL_RADIUS && dy <= 3 && dz <= PORTAL_RADIUS) {
|
||||
doTransfer(player);
|
||||
}
|
||||
}
|
||||
}, 20);
|
||||
|
||||
// ─── D. Chat Commands ───────────────────────────────────────────
|
||||
|
||||
function handleChatCommand(player, msg) {
|
||||
if (msg === "!compass") {
|
||||
try {
|
||||
giveCompassIfMissing(player);
|
||||
player.sendMessage("§b[Hub] §fCompass check complete.");
|
||||
} catch (e) {
|
||||
player.sendMessage(`§c[Hub] §fCouldn't give compass: ${e.message}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg === "!setportal") {
|
||||
const pos = player.location;
|
||||
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!`);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg === "!hub" || msg === "!lobby") {
|
||||
doTransfer(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try beforeEvents.chatSend (pre-release in some BDS versions)
|
||||
try {
|
||||
world.beforeEvents.chatSend.subscribe((event) => {
|
||||
const msg = event.message.trim().toLowerCase();
|
||||
if (msg.startsWith("!")) {
|
||||
event.cancel = true;
|
||||
const player = event.sender;
|
||||
system.run(() => handleChatCommand(player, msg));
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// chatSend not available — fall back to polling player chat via scriptEvent
|
||||
}
|
||||
|
||||
// Fallback: listen for scriptevent commands (players run: /scriptevent hub:cmd <command>)
|
||||
system.afterEvents.scriptEventReceive.subscribe((event) => {
|
||||
if (event.id !== "hub:cmd") return;
|
||||
const player = event.sourceEntity;
|
||||
if (!player) return;
|
||||
const msg = (event.message || "").trim().toLowerCase();
|
||||
handleChatCommand(player, `!${msg}`);
|
||||
});
|
||||
|
||||
// ─── E. Portal Auto-Build on First Load ─────────────────────────
|
||||
|
||||
function buildPortal(loc, dimension) {
|
||||
const x = loc.x;
|
||||
const y = loc.y;
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
system.runTimeout(() => {
|
||||
const existing = world.getDynamicProperty(PORTAL_PROP);
|
||||
if (existing) return; // Portal location already set
|
||||
|
||||
// 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,
|
||||
};
|
||||
|
||||
world.setDynamicProperty(PORTAL_PROP, JSON.stringify(loc));
|
||||
|
||||
// Need a dimension reference — use overworld
|
||||
const overworld = world.getDimension("overworld");
|
||||
buildPortal(loc, overworld);
|
||||
|
||||
world.sendMessage("§b[Hub] §fReturn portal auto-built near spawn! Use §d!setportal§f to relocate it.");
|
||||
}, 100);
|
||||
|
||||
system.run(() => {
|
||||
world.sendMessage("§b[World] §fHub return system loaded! Use compass, step on portal, or type §d!hub§f to return.");
|
||||
});
|
||||
Reference in New Issue
Block a user