All checks were successful
Deploy Addons / deploy (push) Successful in 14s
- camping: replace cube tent with A-frame slope panels (tent_panel_l/r) + cardinal_direction permutations; vote-skip sleep that mixes player.isSleeping bed sleepers with tent occupants and respects the playersSleepingPercentage gamerule; new weathered-canvas texture. - lobby: walk-through silverlabs:portal_field block (no collision, translucent swirl, cross-plane geo) auto-placed above each portal frame; invisible silverlabs:portal_label entity floats above each portal with the destination world name; transfer detection now scans down through the field to find the destination frame. - postal: regenerate post_office and mailbox block textures so they fill the full block face (brick + POST plaque, full red panel with slot/latch /flag/rivets) instead of small sprites floating on transparent. - dynamite + tow-boat: ship the addons (volumes wired into all four worlds; enabled_packs registers them into Mya's world). - art: build-textures.py extended; build-art-catalog.py added to project. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
197 lines
7.4 KiB
JavaScript
197 lines
7.4 KiB
JavaScript
import { world, system } from "@minecraft/server";
|
|
import { transferPlayer } from "@minecraft/server-admin";
|
|
|
|
// Portal frame block → transfer target mapping. The frame is the floor block;
|
|
// the walk-through field column above it inherits the destination by looking down.
|
|
const PORTAL_BLOCKS = {
|
|
"silverlabs:portal_jamie": { name: "Jamie's World", host: "10.0.0.247", port: 19133, color: "§a" },
|
|
"silverlabs:portal_lyla": { name: "Lyla's World", host: "10.0.0.247", port: 19134, color: "§d" },
|
|
"silverlabs:portal_mya": { name: "Mya's World", host: "10.0.0.247", port: 19135, color: "§b" },
|
|
};
|
|
|
|
const PORTAL_FIELD_BLOCK = "silverlabs:portal_field";
|
|
const PORTAL_LABEL_ENTITY = "silverlabs:portal_label";
|
|
|
|
// Coordinate-based portal zones (fallback detection + auto-spawn anchors).
|
|
// `frame_y` is the floor block where the destination frame sits; the field
|
|
// blocks are placed at frame_y+1 and frame_y+2 (2-tall walk-through column),
|
|
// and the floating label entity sits at frame_y+3.5.
|
|
const PORTAL_ZONES = [
|
|
{ name: "Jamie's World", x: 436, y: 66, z: -296, frame_y: 66, host: "10.0.0.247", port: 19133, color: "§a" },
|
|
{ name: "Lyla's World", x: 462, y: 65, z: -322, frame_y: 65, host: "10.0.0.247", port: 19134, color: "§d" },
|
|
{ name: "Lyla's World", x: 474, y: 65, z: -281, frame_y: 65, host: "10.0.0.247", port: 19134, color: "§d", label_suffix: " (Super Kitties)" },
|
|
{ name: "Mya's World", x: 488, y: 66, z: -296, frame_y: 66, host: "10.0.0.247", port: 19135, color: "§b" },
|
|
];
|
|
|
|
const PORTAL_RADIUS_X = 2.5;
|
|
const PORTAL_RADIUS_Z = 2.0;
|
|
const PORTAL_RADIUS_Y = 2.0;
|
|
const COOLDOWN_TICKS = 100; // 5 seconds cooldown
|
|
const SPAWN_PROTECTION_TICKS = 200; // 10 seconds — ignore portal detection after spawn
|
|
|
|
// Track cooldowns per player
|
|
const cooldowns = new Map();
|
|
// Track when players spawned (to prevent transfer loop on arrival)
|
|
const spawnTicks = new Map();
|
|
|
|
world.afterEvents.playerSpawn.subscribe((event) => {
|
|
spawnTicks.set(event.player.id, system.currentTick);
|
|
});
|
|
|
|
/**
|
|
* Check if player is standing on/in a custom portal block (priority method).
|
|
* Returns the portal config or null.
|
|
*/
|
|
function checkBlockPortal(player) {
|
|
const pos = player.location;
|
|
const dimension = player.dimension;
|
|
const fx = Math.floor(pos.x);
|
|
const fy = Math.floor(pos.y);
|
|
const fz = Math.floor(pos.z);
|
|
const blockAtFeet = dimension.getBlock({ x: fx, y: fy, z: fz });
|
|
const blockHead = dimension.getBlock({ x: fx, y: fy + 1, z: fz });
|
|
const blockBelow = dimension.getBlock({ x: fx, y: fy - 1, z: fz });
|
|
|
|
// Walk-through portal field: scan downward for the destination frame block.
|
|
for (const b of [blockAtFeet, blockHead]) {
|
|
if (b?.typeId === PORTAL_FIELD_BLOCK) {
|
|
for (let dy = 1; dy <= 4; dy++) {
|
|
const probe = dimension.getBlock({ x: fx, y: fy - dy, z: fz });
|
|
const dest = probe && PORTAL_BLOCKS[probe.typeId];
|
|
if (dest) return dest;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Legacy: standing on a frame block directly.
|
|
return PORTAL_BLOCKS[blockAtFeet?.typeId] || PORTAL_BLOCKS[blockBelow?.typeId] || null;
|
|
}
|
|
|
|
/**
|
|
* Check if player is within a coordinate-based portal zone (fallback method).
|
|
* Returns the portal config or null.
|
|
*/
|
|
function checkZonePortal(player) {
|
|
const pos = player.location;
|
|
for (const portal of PORTAL_ZONES) {
|
|
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_X && dy < PORTAL_RADIUS_Y && dz < PORTAL_RADIUS_Z) {
|
|
return portal;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
system.runInterval(() => {
|
|
for (const player of world.getAllPlayers()) {
|
|
const playerId = player.id;
|
|
|
|
// Check cooldown
|
|
const lastTransfer = cooldowns.get(playerId) || 0;
|
|
if (system.currentTick - lastTransfer < COOLDOWN_TICKS) continue;
|
|
|
|
// Skip if player just spawned (prevents loop when arriving from child world)
|
|
const spawnedAt = spawnTicks.get(playerId) || 0;
|
|
if (system.currentTick - spawnedAt < SPAWN_PROTECTION_TICKS) continue;
|
|
|
|
// Hybrid detection: custom block first, then coordinate fallback
|
|
const portal = checkBlockPortal(player) || checkZonePortal(player);
|
|
if (!portal) continue;
|
|
|
|
cooldowns.set(playerId, system.currentTick);
|
|
|
|
// Show title notification
|
|
player.runCommand(`titleraw @s title {"rawtext":[{"text":"${portal.color}${portal.name}"}]}`);
|
|
player.runCommand(`titleraw @s subtitle {"rawtext":[{"text":"§7Transferring..."}]}`);
|
|
player.sendMessage(`§6Transferring to ${portal.name}...`);
|
|
|
|
// Teleport player away from portal before transfer so their saved position
|
|
// is NOT on the portal (prevents re-transfer loop when they return to lobby)
|
|
const safePos = player.location;
|
|
safePos.z += 4; // Move 4 blocks away from portals (portals are at z=-24)
|
|
player.teleport(safePos);
|
|
|
|
// Wait 5 ticks for position to save, then transfer.
|
|
// transferPlayer from @minecraft/server-admin beta API — requires gametest experiment + permissions.json allows server-admin.
|
|
system.runTimeout(() => {
|
|
try {
|
|
transferPlayer(player, { hostname: portal.host, port: portal.port });
|
|
} catch (e) {
|
|
player.sendMessage(`§cTransfer failed: ${e.message}`);
|
|
}
|
|
}, 5);
|
|
}
|
|
}, 10); // Check every half second
|
|
|
|
// Clean up tracking when players leave
|
|
world.afterEvents.playerLeave.subscribe((event) => {
|
|
cooldowns.delete(event.playerId);
|
|
spawnTicks.delete(event.playerId);
|
|
});
|
|
|
|
// ─── Walk-through portal fields + floating destination labels ──
|
|
// For each known portal zone, place a 2-block-tall column of `portal_field`
|
|
// blocks (no collision, translucent, light-emitting) directly above the frame
|
|
// block, and summon an invisible `portal_label` entity above with the
|
|
// destination world's name visible as a floating tag.
|
|
|
|
function ensurePortalField(overworld, x, fy, z) {
|
|
for (const dy of [1, 2]) {
|
|
try {
|
|
const b = overworld.getBlock({ x, y: fy + dy, z });
|
|
if (!b) continue;
|
|
if (b.typeId !== PORTAL_FIELD_BLOCK) {
|
|
overworld.runCommand(`setblock ${x} ${fy + dy} ${z} ${PORTAL_FIELD_BLOCK}`);
|
|
}
|
|
} catch (_) { /* chunks may be unloaded; retried next boot */ }
|
|
}
|
|
}
|
|
|
|
function ensurePortalLabel(overworld, zone) {
|
|
const labelY = zone.frame_y + 3.5;
|
|
// Check if a label already exists nearby; replace its tag if outdated.
|
|
let existing = null;
|
|
try {
|
|
const matches = overworld.getEntities({
|
|
type: PORTAL_LABEL_ENTITY,
|
|
location: { x: zone.x + 0.5, y: labelY, z: zone.z + 0.5 },
|
|
maxDistance: 1.5,
|
|
});
|
|
existing = matches[0] || null;
|
|
} catch (_) {}
|
|
|
|
const tag = `${zone.color}${zone.name}${zone.label_suffix || ""}`;
|
|
if (existing) {
|
|
try { existing.nameTag = tag; } catch (_) {}
|
|
return;
|
|
}
|
|
try {
|
|
const entity = overworld.spawnEntity(
|
|
PORTAL_LABEL_ENTITY,
|
|
{ x: zone.x + 0.5, y: labelY, z: zone.z + 0.5 }
|
|
);
|
|
if (entity) {
|
|
try { entity.nameTag = tag; } catch (_) {}
|
|
}
|
|
} catch (_) { /* chunks may be unloaded */ }
|
|
}
|
|
|
|
function placePortalDressing() {
|
|
const overworld = world.getDimension("overworld");
|
|
for (const zone of PORTAL_ZONES) {
|
|
if (zone.frame_y === undefined) continue;
|
|
ensurePortalField(overworld, zone.x, zone.frame_y, zone.z);
|
|
ensurePortalLabel(overworld, zone);
|
|
}
|
|
}
|
|
|
|
// Initial place + periodic re-ensure (handles chunk loading after first attempt).
|
|
system.runTimeout(() => placePortalDressing(), 40);
|
|
system.runInterval(() => placePortalDressing(), 600); // every 30s
|
|
|
|
system.run(() => {
|
|
world.sendMessage("§6[Hub] §7Portal transfer system loaded!");
|
|
});
|