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!"); });