feat(addons): update portal blocks, recipes, and transfer scripts
All checks were successful
Deploy Addons / deploy (push) Successful in 17s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-21 11:31:40 +00:00
parent 4bc7eb05b6
commit c32dbf42c4
14 changed files with 154 additions and 51 deletions

View File

@@ -1,7 +1,7 @@
{ {
"mcpServers": { "mcpServers": {
"minecraft": { "mc-ai-bridge": {
"url": "http://10.0.0.247:3002/mcp" "url": "http://10.0.0.247:3002/sse"
} }
} }
} }

View File

@@ -5,7 +5,7 @@
"dragon_whistle": { "dragon_whistle": {
"textures": "textures/items/dragon_whistle" "textures": "textures/items/dragon_whistle"
}, },
"dragon_egg": { "dragon_egg": {
"textures": "textures/items/dragon_egg" "textures": "textures/items/dragon_egg"
}, },
"dragon_toy": { "dragon_toy": {
@@ -16,6 +16,6 @@
}, },
"dragon_gravestone": { "dragon_gravestone": {
"textures": "textures/items/dragon_gravestone" "textures": "textures/items/dragon_gravestone"
}, }
} }
} }

View File

@@ -24,11 +24,33 @@ function doTransfer(player) {
recentTransfers.set(player.name, now); recentTransfers.set(player.name, now);
world.sendMessage(`§a${player.name} is returning to the Hub...`); 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 {
const portal = JSON.parse(portalJson);
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
}
}
// Wait 5 ticks for position to save, then transfer
system.runTimeout(() => {
try { try {
transferPlayer(player, { hostname: LOBBY_HOST, port: LOBBY_PORT }); transferPlayer(player, { hostname: LOBBY_HOST, port: LOBBY_PORT });
} catch (e) { } catch (e) {
player.sendMessage(`§cTransfer failed: ${e.message}`); player.sendMessage(`§cTransfer failed: ${e.message}`);
} }
}, 5);
} }
// ─── A. Compass Distribution ──────────────────────────────────── // ─── A. Compass Distribution ────────────────────────────────────

View File

@@ -1,5 +1,5 @@
{ {
"format_version": "1.21.40", "format_version": "1.21.0",
"minecraft:block": { "minecraft:block": {
"description": { "description": {
"identifier": "silverlabs:portal_frame", "identifier": "silverlabs:portal_frame",

View File

@@ -1,5 +1,5 @@
{ {
"format_version": "1.21.40", "format_version": "1.21.0",
"minecraft:block": { "minecraft:block": {
"description": { "description": {
"identifier": "silverlabs:portal_jamie", "identifier": "silverlabs:portal_jamie",

View File

@@ -1,5 +1,5 @@
{ {
"format_version": "1.21.40", "format_version": "1.21.0",
"minecraft:block": { "minecraft:block": {
"description": { "description": {
"identifier": "silverlabs:portal_lyla", "identifier": "silverlabs:portal_lyla",

View File

@@ -1,5 +1,5 @@
{ {
"format_version": "1.21.40", "format_version": "1.21.0",
"minecraft:block": { "minecraft:block": {
"description": { "description": {
"identifier": "silverlabs:portal_mya", "identifier": "silverlabs:portal_mya",

View File

@@ -8,6 +8,11 @@
"min_engine_version": [1, 21, 0] "min_engine_version": [1, 21, 0]
}, },
"modules": [ "modules": [
{
"type": "data",
"uuid": "a1b2c3d4-7777-8888-9999-abcdef345678",
"version": [1, 1, 0]
},
{ {
"type": "script", "type": "script",
"language": "javascript", "language": "javascript",

View File

@@ -1,8 +1,8 @@
{ {
"format_version": "1.21.40", "format_version": "1.21.0",
"minecraft:recipe_shaped": { "minecraft:recipe_shaped": {
"description": { "description": {
"identifier": "silverlabs:portal_frame" "identifier": "silverlabs:portal_frame_recipe"
}, },
"tags": ["crafting_table"], "tags": ["crafting_table"],
"pattern": [ "pattern": [
@@ -14,9 +14,6 @@
"O": { "item": "minecraft:obsidian" }, "O": { "item": "minecraft:obsidian" },
"E": { "item": "minecraft:ender_pearl" } "E": { "item": "minecraft:ender_pearl" }
}, },
"unlock": [
{ "item": "minecraft:obsidian" }
],
"result": { "result": {
"item": "silverlabs:portal_frame", "item": "silverlabs:portal_frame",
"count": 1 "count": 1

View File

@@ -1,17 +1,14 @@
{ {
"format_version": "1.21.40", "format_version": "1.21.0",
"minecraft:recipe_shapeless": { "minecraft:recipe_shapeless": {
"description": { "description": {
"identifier": "silverlabs:portal_jamie" "identifier": "silverlabs:portal_jamie_recipe"
}, },
"tags": ["crafting_table"], "tags": ["crafting_table"],
"ingredients": [ "ingredients": [
{ "item": "silverlabs:portal_frame" }, { "item": "silverlabs:portal_frame" },
{ "item": "minecraft:emerald" } { "item": "minecraft:emerald" }
], ],
"unlock": [
{ "item": "silverlabs:portal_frame" }
],
"result": { "result": {
"item": "silverlabs:portal_jamie", "item": "silverlabs:portal_jamie",
"count": 1 "count": 1

View File

@@ -1,17 +1,14 @@
{ {
"format_version": "1.21.40", "format_version": "1.21.0",
"minecraft:recipe_shapeless": { "minecraft:recipe_shapeless": {
"description": { "description": {
"identifier": "silverlabs:portal_lyla" "identifier": "silverlabs:portal_lyla_recipe"
}, },
"tags": ["crafting_table"], "tags": ["crafting_table"],
"ingredients": [ "ingredients": [
{ "item": "silverlabs:portal_frame" }, { "item": "silverlabs:portal_frame" },
{ "item": "minecraft:amethyst_shard" } { "item": "minecraft:amethyst_shard" }
], ],
"unlock": [
{ "item": "silverlabs:portal_frame" }
],
"result": { "result": {
"item": "silverlabs:portal_lyla", "item": "silverlabs:portal_lyla",
"count": 1 "count": 1

View File

@@ -1,17 +1,14 @@
{ {
"format_version": "1.21.40", "format_version": "1.21.0",
"minecraft:recipe_shapeless": { "minecraft:recipe_shapeless": {
"description": { "description": {
"identifier": "silverlabs:portal_mya" "identifier": "silverlabs:portal_mya_recipe"
}, },
"tags": ["crafting_table"], "tags": ["crafting_table"],
"ingredients": [ "ingredients": [
{ "item": "silverlabs:portal_frame" }, { "item": "silverlabs:portal_frame" },
{ "item": "minecraft:prismarine_crystals" } { "item": "minecraft:prismarine_crystals" }
], ],
"unlock": [
{ "item": "silverlabs:portal_frame" }
],
"result": { "result": {
"item": "silverlabs:portal_mya", "item": "silverlabs:portal_mya",
"count": 1 "count": 1

View File

@@ -1,13 +1,23 @@
import { world, system } from "@minecraft/server"; import { world, system } from "@minecraft/server";
import { transferPlayer } from "@minecraft/server-admin"; import { transferPlayer } from "@minecraft/server-admin";
// Portal block → transfer target mapping // Portal block → transfer target mapping (custom blocks — priority detection)
const PORTAL_BLOCKS = { const PORTAL_BLOCKS = {
"silverlabs:portal_jamie": { name: "Jamie's World", host: "10.0.0.247", port: 19133, color: "§a" }, "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_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" }, "silverlabs:portal_mya": { name: "Mya's World", host: "10.0.0.247", port: 19135, color: "§b" },
}; };
// Coordinate-based portal zones (fallback detection)
const PORTAL_ZONES = [
{ name: "Jamie's World", x: -15, y: 65, z: -24, host: "10.0.0.247", port: 19133, color: "§a" },
{ name: "Lyla's World", x: 0, y: 65, z: -24, host: "10.0.0.247", port: 19134, color: "§d" },
{ name: "Mya's World", x: 15, y: 65, z: -24, 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 COOLDOWN_TICKS = 100; // 5 seconds cooldown
const SPAWN_PROTECTION_TICKS = 200; // 10 seconds — ignore portal detection after spawn const SPAWN_PROTECTION_TICKS = 200; // 10 seconds — ignore portal detection after spawn
@@ -20,9 +30,41 @@ world.afterEvents.playerSpawn.subscribe((event) => {
spawnTicks.set(event.player.id, system.currentTick); 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 blockAtFeet = dimension.getBlock({ x: Math.floor(pos.x), y: Math.floor(pos.y), z: Math.floor(pos.z) });
const blockBelow = dimension.getBlock({ x: Math.floor(pos.x), y: Math.floor(pos.y) - 1, z: Math.floor(pos.z) });
const feetId = blockAtFeet?.typeId;
const belowId = blockBelow?.typeId;
return PORTAL_BLOCKS[feetId] || PORTAL_BLOCKS[belowId] || 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(() => { system.runInterval(() => {
for (const player of world.getAllPlayers()) { for (const player of world.getAllPlayers()) {
const pos = player.location;
const playerId = player.id; const playerId = player.id;
// Check cooldown // Check cooldown
@@ -33,29 +75,31 @@ system.runInterval(() => {
const spawnedAt = spawnTicks.get(playerId) || 0; const spawnedAt = spawnTicks.get(playerId) || 0;
if (system.currentTick - spawnedAt < SPAWN_PROTECTION_TICKS) continue; if (system.currentTick - spawnedAt < SPAWN_PROTECTION_TICKS) continue;
// Check block at player's feet and one block below // Hybrid detection: custom block first, then coordinate fallback
const dimension = player.dimension; const portal = checkBlockPortal(player) || checkZonePortal(player);
const blockAtFeet = dimension.getBlock({ x: Math.floor(pos.x), y: Math.floor(pos.y), z: Math.floor(pos.z) });
const blockBelow = dimension.getBlock({ x: Math.floor(pos.x), y: Math.floor(pos.y) - 1, z: Math.floor(pos.z) });
const feetId = blockAtFeet?.typeId;
const belowId = blockBelow?.typeId;
const portal = PORTAL_BLOCKS[feetId] || PORTAL_BLOCKS[belowId];
if (!portal) continue; if (!portal) continue;
cooldowns.set(playerId, system.currentTick); cooldowns.set(playerId, system.currentTick);
// Teleport player away from the portal block so they don't land on it on return
player.teleport({ x: pos.x, y: pos.y, z: pos.z + 3 });
// Show title notification // Show title notification
player.runCommand(`titleraw @s title {"rawtext":[{"text":"${portal.color}${portal.name}"}]}`); player.runCommand(`titleraw @s title {"rawtext":[{"text":"${portal.color}${portal.name}"}]}`);
player.runCommand(`titleraw @s subtitle {"rawtext":[{"text":"§7Transferring..."}]}`); player.runCommand(`titleraw @s subtitle {"rawtext":[{"text":"§7Transferring..."}]}`);
player.sendMessage(`§6Transferring to ${portal.name}...`); 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
system.runTimeout(() => {
try { try {
transferPlayer(player, { hostname: portal.host, port: portal.port }); transferPlayer(player, { hostname: portal.host, port: portal.port });
} catch (e) { } catch (e) {
player.sendMessage(`§cTransfer failed: ${e.message}`); player.sendMessage(`§cTransfer failed: ${e.message}`);
} }
}, 5);
} }
}, 10); // Check every half second }, 10); // Check every half second
@@ -65,6 +109,43 @@ world.afterEvents.playerLeave.subscribe((event) => {
spawnTicks.delete(event.playerId); spawnTicks.delete(event.playerId);
}); });
// ─── Place signs above each portal on load ──────────────────────
function placePortalSigns() {
const overworld = world.getDimension("overworld");
const signs = [
{ x: -15, y: 70, z: -23, name: "Jamie's", color: "§a" },
{ x: 0, y: 70, z: -23, name: "Lyla's", color: "§d" },
{ x: 15, y: 70, z: -23, name: "Mya's", color: "§b" },
];
for (const sign of signs) {
try {
overworld.runCommand(`setblock ${sign.x} ${sign.y} ${sign.z} oak_wall_sign ["facing_direction":3]`);
} catch (e) {
// Non-fatal — chunks may not be loaded
}
}
system.runTimeout(() => {
for (const sign of signs) {
try {
const block = overworld.getBlock({ x: sign.x, y: sign.y, z: sign.z });
if (!block) continue;
const signComponent = block.getComponent("minecraft:sign");
if (!signComponent) continue;
signComponent.setText(`${sign.color}${sign.name}\n${sign.color}World\n§7▼ Step in ▼`);
} catch (e) {
// Non-fatal
}
}
}, 10);
}
system.runTimeout(() => {
placePortalSigns();
}, 40);
system.run(() => { system.run(() => {
world.sendMessage("§6[Hub] §7Portal transfer system loaded! Place portal blocks to create portals."); world.sendMessage("§6[Hub] §7Portal transfer system loaded!");
}); });

View File

@@ -0,0 +1,7 @@
{
"format_version": [1, 1, 0],
"silverlabs:portal_frame": { "sound": "stone" },
"silverlabs:portal_jamie": { "sound": "stone" },
"silverlabs:portal_lyla": { "sound": "stone" },
"silverlabs:portal_mya": { "sound": "stone" }
}