From e5568fe1f3b6a9d2860041d056c2c53d1c957ff7 Mon Sep 17 00:00:00 2001 From: SysAdmin Date: Sun, 5 Apr 2026 02:46:26 +0100 Subject: [PATCH] fix(easter-egg): persistent cross-server counts via inventory basket; 2-block tall eggs Player tags are server-local and wiped on transfer, breaking lobby scores. Switch to an Egg Basket (nether_star, custom nameTag "[j:N,l:N,m:N]") that travels with the player's inventory so the lobby can always read accurate counts. Child worlds rebuild lost tags from block-state on each player join (missing egg block = previously collected), keeping within-world deduplication intact. Basket is updated on every collection and after every tag rebuild. Lobby reads basket counts directly instead of tags. Also make eggs visually egg-shaped: 2 glazed-terracotta blocks tall with opposing facing_direction (2 bottom, 3 top) to create an oval silhouette. collectEgg() now clears both Y and Y+1; detection dy extended to 3.0. Co-Authored-By: Claude Sonnet 4.6 --- .../easter_egg_child_BP/scripts/main.js | 236 ++++++++++++------ .../easter_egg_lobby_BP/scripts/main.js | 64 +++-- 2 files changed, 203 insertions(+), 97 deletions(-) diff --git a/easter-egg-addon/easter_egg_child_BP/scripts/main.js b/easter-egg-addon/easter_egg_child_BP/scripts/main.js index ec3628f..c4d7747 100644 --- a/easter-egg-addon/easter_egg_child_BP/scripts/main.js +++ b/easter-egg-addon/easter_egg_child_BP/scripts/main.js @@ -1,10 +1,7 @@ -import { world, system } from "@minecraft/server"; +import { world, system, ItemStack } from "@minecraft/server"; import { variables } from "@minecraft/server-admin"; // ─── World Detection ───────────────────────────────────────────── -// Each world uses a unique prefix and egg colour. -// Detection follows the hub-return pattern: read variables.json world_name. -// Persisted in dynamic properties so it only detects once. let PREFIX = ""; let EGG_BLOCK = ""; @@ -13,7 +10,7 @@ try { const p = world.getDynamicProperty("egg_prefix"); const b = world.getDynamicProperty("egg_block"); if (p && b) { PREFIX = p; EGG_BLOCK = b; } -} catch (e) { /* will detect below */ } +} catch (e) {} if (!PREFIX) { let worldName = ""; @@ -31,7 +28,7 @@ if (!PREFIX) { } // ─── Egg Positions ─────────────────────────────────────────────── -// 50 (dx, dz) offsets from world spawn. +// 50 (dx, dz) offsets from the first player's spawn position. // Three rings: close (5-20), medium (20-50), far (50-100). const EGG_OFFSETS = [ @@ -49,10 +46,7 @@ const EGG_OFFSETS = [ [ 45, 82], [-48, 78], [75, -70], [-70, 75], ]; -// ─── Scene Descriptions ────────────────────────────────────────── -// 8 hiding scenes cycle across the 50 eggs (6-7 per type). -// Each scene places supporting blocks then the egg itself. -// Eggs are visible but require exploration to find. +// ─── Scene Names ───────────────────────────────────────────────── const SCENE_NAMES = [ "atop a barrel", @@ -65,9 +59,102 @@ const SCENE_NAMES = [ "hidden in a leaf cluster", ]; +// ─── Egg Basket (Cross-Server Persistence) ──────────────────────── +// A nether_star item with a custom nameTag carries egg counts between servers. +// nameTag format: "§6Egg Basket §8[j:N,l:N,m:N]" +// Tags (egg_j_1, etc.) track which specific eggs were found per-session. +// The basket is the authoritative count the lobby reads. + +const BASKET_ITEM = "minecraft:nether_star"; +const BASKET_HEAD = "§6Egg Basket "; +const BASKET_REGEX = /\[j:(\d+),l:(\d+),m:(\d+)\]/; + +function getBasket(player) { + try { + const inv = player.getComponent("minecraft:inventory"); + if (!inv) return null; + const container = inv.container; + for (let i = 0; i < container.size; i++) { + const item = container.getItem(i); + if (item && item.typeId === BASKET_ITEM && + item.nameTag && item.nameTag.startsWith(BASKET_HEAD)) { + return { item, slot: i, container }; + } + } + } catch (e) {} + return null; +} + +function getBasketCounts(player) { + const result = { j: 0, l: 0, m: 0 }; + const b = getBasket(player); + if (!b) return result; + const m = b.item.nameTag.match(BASKET_REGEX); + if (m) { result.j = +m[1]; result.l = +m[2]; result.m = +m[3]; } + return result; +} + +function updateBasket(player, j, l, m) { + try { + const nameTag = `${BASKET_HEAD}§8[j:${j},l:${l},m:${m}]`; + const b = getBasket(player); + if (b) { + b.item.nameTag = nameTag; + b.container.setItem(b.slot, b.item); + } else { + const item = new ItemStack(BASKET_ITEM, 1); + item.nameTag = nameTag; + const inv = player.getComponent("minecraft:inventory"); + if (inv) { + const container = inv.container; + for (let i = 0; i < container.size; i++) { + if (!container.getItem(i)) { + container.setItem(i, item); + player.sendMessage("§6[Easter Eggs] §fYou received an §6Egg Basket§f — it tracks your egg count across all worlds! Keep it safe."); + break; + } + } + } + } + } catch (e) {} +} + +// ─── Tag Rebuild ───────────────────────────────────────────────── +// When a player (re)joins, rebuild their local found-egg tags based on which +// egg blocks are actually missing from the world. This handles the case where +// a player transferred away and their server-local tags were wiped. +// After rebuilding, sync the basket count to the accurate value. + +function rebuildTagsAndBasket(player) { + const positions = getEggPositions(); + if (!positions || positions.length === 0) return; + const overworld = world.getDimension("overworld"); + let found = 0; + for (const egg of positions) { + const tagId = `egg_${PREFIX}_${egg.id}`; + if (player.hasTag(tagId)) { + found++; + continue; + } + try { + const block = overworld.getBlock({ x: egg.x, y: egg.y, z: egg.z }); + // Block gone (air/non-terracotta) means this egg was previously collected + if (!block || !block.typeId.includes("glazed_terracotta")) { + player.addTag(tagId); + found++; + } + } catch (e) {} + } + // Update basket with accurate count for this world + const counts = getBasketCounts(player); + if (PREFIX === "j") updateBasket(player, found, counts.l, counts.m); + else if (PREFIX === "l") updateBasket(player, counts.j, found, counts.m); + else if (PREFIX === "m") updateBasket(player, counts.j, counts.l, found); +} + // ─── Cached Positions ──────────────────────────────────────────── -let eggPositions = null; // null = not yet loaded from storage +let eggPositions = null; function getEggPositions() { if (eggPositions !== null) return eggPositions; @@ -92,28 +179,27 @@ function findSurfaceY(dimension, x, spawnY, z) { if (block && block.typeId !== "minecraft:air") { return y + 1; // first empty block above the topmost solid block } - } catch (e) { /* chunk not loaded — skip */ } + } catch (e) {} } return spawnY; } // ─── Scene Builder ─────────────────────────────────────────────── +// Each egg is 2 blocks tall — bottom faces south (2), top faces north (3). +// This opposing rotation gives the glazed terracotta an oval/egg appearance. +// Stored Y is always the bottom block. -/** - * Constructs a hiding scene and places the egg. - * Returns the actual Y coordinate the egg was placed at. - */ function buildScene(dim, sceneIndex, x, surfY, z) { const cmds = []; let eggY = surfY; switch (sceneIndex % 8) { - case 0: // Barrel nook — coloured egg perched on a barrel + case 0: // Barrel nook — egg perched on a barrel cmds.push(`setblock ${x} ${surfY} ${z} barrel`); eggY = surfY + 1; break; - case 1: // Hay bale — egg resting on a bale of hay + case 1: // Hay bale — egg resting on a bale cmds.push(`setblock ${x} ${surfY} ${z} hay_block`); eggY = surfY + 1; break; @@ -122,7 +208,7 @@ function buildScene(dim, sceneIndex, x, surfY, z) { cmds.push(`setblock ${x-1} ${surfY} ${z} poppy`); cmds.push(`setblock ${x+1} ${surfY} ${z} cornflower`); cmds.push(`setblock ${x} ${surfY} ${z-1} dandelion`); - eggY = surfY; // egg at centre, same level as the flowers + eggY = surfY; // egg rises up through the flower level break; case 3: // Log perch — egg balanced atop a cut log @@ -130,11 +216,11 @@ function buildScene(dim, sceneIndex, x, surfY, z) { eggY = surfY + 1; break; - case 4: // Fence posts — egg elevated on a fence post pedestal + case 4: // Fence posts — egg elevated on a fence pedestal cmds.push(`setblock ${x-1} ${surfY} ${z} oak_fence`); cmds.push(`setblock ${x} ${surfY} ${z} oak_fence`); cmds.push(`setblock ${x+1} ${surfY} ${z} oak_fence`); - eggY = surfY + 1; // sits atop the centre post + eggY = surfY + 1; break; case 5: // Crafting table — egg left on a workbench @@ -142,46 +228,35 @@ function buildScene(dim, sceneIndex, x, surfY, z) { eggY = surfY + 1; break; - case 6: // Mossy ledge — egg raised on a small stone platform + case 6: // Mossy ledge — egg raised on a stone platform cmds.push(`setblock ${x} ${surfY} ${z} mossy_cobblestone`); cmds.push(`setblock ${x-1} ${surfY} ${z} mossy_cobblestone`); eggY = surfY + 1; break; - case 7: // Leaf cluster — egg tucked up in a low canopy of leaves - // Lower ring of leaves at surfY+1 (partial enclosure) + case 7: // Leaf cluster — egg tucked in a low leaf canopy + // Side leaves frame the egg without fully hiding it cmds.push(`setblock ${x+1} ${surfY+1} ${z} oak_leaves`); cmds.push(`setblock ${x-1} ${surfY+1} ${z} oak_leaves`); - // Upper ring of leaves at surfY+2 (surrounds the egg) - cmds.push(`setblock ${x+1} ${surfY+2} ${z} oak_leaves`); - cmds.push(`setblock ${x-1} ${surfY+2} ${z} oak_leaves`); - cmds.push(`setblock ${x} ${surfY+2} ${z+1} oak_leaves`); - cmds.push(`setblock ${x} ${surfY+2} ${z-1} oak_leaves`); - eggY = surfY + 2; // visible from the side through the semi-transparent leaves + cmds.push(`setblock ${x} ${surfY+1} ${z+1} oak_leaves`); + cmds.push(`setblock ${x} ${surfY+1} ${z-1} oak_leaves`); + eggY = surfY + 1; // bottom block nestled in the leaf ring, top sticks above break; } - // Always place the egg block last so it overwrites anything at its position - cmds.push(`setblock ${x} ${eggY} ${z} ${EGG_BLOCK}`); + // 2-block tall egg: opposing facing directions create an oval silhouette + cmds.push(`setblock ${x} ${eggY} ${z} ${EGG_BLOCK} ["facing_direction":2]`); + cmds.push(`setblock ${x} ${eggY + 1} ${z} ${EGG_BLOCK} ["facing_direction":3]`); for (const cmd of cmds) { - try { dim.runCommand(cmd); } catch (e) { /* non-fatal — chunk may not be loaded */ } + try { dim.runCommand(cmd); } catch (e) {} } - return eggY; + return eggY; // caller stores this as the bottom block Y } // ─── Egg Placement ─────────────────────────────────────────────── -/** - * Place all eggs around a centre position. - * On first call: centre comes from the joining player's location. - * Centre is stored so all players (and resets) use the same area. - * - * If eggs_placed is "true" but no egg blocks actually exist in the world - * (e.g. blocks were never written because chunks weren't loaded), the flag - * is cleared automatically and eggs are re-placed. - */ function placeAllEggs(playerPos) { const overworld = world.getDimension("overworld"); @@ -196,16 +271,16 @@ function placeAllEggs(playerPos) { if (block && block.typeId.includes("glazed_terracotta")) found++; } catch (e) {} } - if (found > 0) return; // eggs are genuinely there + if (found > 0) return; } - // Eggs flagged as placed but none found — clear and re-place + // Flagged as placed but blocks are absent — clear and re-place world.setDynamicProperty("eggs_placed", "false"); world.setDynamicProperty("eggs_data", ""); world.setDynamicProperty("egg_center", ""); eggPositions = null; } - // Determine centre: use stored centre, or the joining player's position + // Determine centre: stored centre takes priority, then joining player, then world spawn let cx, cy, cz; try { const stored = JSON.parse(world.getDynamicProperty("egg_center") || "null"); @@ -225,7 +300,6 @@ function placeAllEggs(playerPos) { } const positions = []; - for (let i = 0; i < EGG_OFFSETS.length; i++) { const [dx, dz] = EGG_OFFSETS[i]; const x = cx + dx; @@ -248,32 +322,41 @@ function placeAllEggs(playerPos) { function collectEgg(player, egg, tagId) { try { - // Count before adding the tag (getTags is synchronous) - const before = player.getTags().filter(t => t.startsWith(`egg_${PREFIX}_`)).length; - const newCount = before + 1; + // Confirm the block still exists before collecting (prevents ghost re-collection) + const block = player.dimension.getBlock({ x: egg.x, y: egg.y, z: egg.z }); + if (!block || !block.typeId.includes("glazed_terracotta")) return; - player.runCommand(`tag @s add ${tagId}`); - player.dimension.runCommand(`setblock ${egg.x} ${egg.y} ${egg.z} air`); + player.addTag(tagId); + const count = player.getTags().filter(t => t.startsWith(`egg_${PREFIX}_`)).length; + + // Update basket with the new count for this world + const counts = getBasketCounts(player); + if (PREFIX === "j") updateBasket(player, count, counts.l, counts.m); + else if (PREFIX === "l") updateBasket(player, counts.j, count, counts.m); + else if (PREFIX === "m") updateBasket(player, counts.j, counts.l, count); + + // Remove both blocks of the 2-block tall egg + player.dimension.runCommand(`setblock ${egg.x} ${egg.y} ${egg.z} air`); + player.dimension.runCommand(`setblock ${egg.x} ${egg.y + 1} ${egg.z} air`); player.dimension.runCommand(`particle minecraft:egg_destroy_emitter ${egg.x} ${egg.y} ${egg.z}`); player.runCommand(`playsound random.orb @s`); player.runCommand(`playsound random.levelup @s`); player.runCommand(`give @s emerald 1`); player.runCommand(`titleraw @s title {"rawtext":[{"text":"§6Easter Egg Found!"}]}`); - player.runCommand(`titleraw @s subtitle {"rawtext":[{"text":"§e${newCount}/50 §7found in this world"}]}`); + player.runCommand(`titleraw @s subtitle {"rawtext":[{"text":"§e${count}/50 §7found in this world"}]}`); player.sendMessage( - `§6[Eggs] §fEgg §e#${egg.id} §f(${SCENE_NAMES[(egg.id - 1) % 8]}) §7| §e${newCount}/50 §7in this world` + `§6[Eggs] §fEgg §e#${egg.id} §f(${SCENE_NAMES[(egg.id - 1) % 8]}) §7| §e${count}/50 §7in this world` ); - if (newCount === 50) { + if (count === 50) { world.sendMessage(`§6§l✨ ${player.name} found all 50 eggs! ✨ §r§6Return to the lobby for your final score!`); } - } catch (e) { - // Player may have disconnected — non-fatal - } + } catch (e) {} } // ─── Detection Loop ────────────────────────────────────────────── +// dy <= 3.0 covers both blocks of a 2-block tall egg (bottom at eggY, top at eggY+1). system.runInterval(() => { const positions = getEggPositions(); @@ -285,13 +368,13 @@ system.runInterval(() => { for (const egg of positions) { const tagId = `egg_${PREFIX}_${egg.id}`; - if (tags.includes(tagId)) continue; // already found + if (tags.includes(tagId)) continue; const dx = Math.abs(pos.x - egg.x); const dy = Math.abs(pos.y - egg.y); const dz = Math.abs(pos.z - egg.z); - if (dx <= 2.5 && dy <= 2.0 && dz <= 2.5) { + if (dx <= 2.5 && dy <= 3.0 && dz <= 2.5) { collectEgg(player, egg, tagId); } } @@ -303,12 +386,11 @@ system.runInterval(() => { function handleEggCommand(player, msg) { if (msg === "!eggs") { const count = player.getTags().filter(t => t.startsWith(`egg_${PREFIX}_`)).length; - const color = PREFIX === "j" ? "§a" : PREFIX === "l" ? "§d" : "§b"; player.sendMessage(`§6[Eggs] §fFound §e${count}§f/50 eggs in this world.`); if (count === 50) { player.sendMessage("§a§lYou found all 50! Return to the lobby to see the leaderboard."); } else { - player.sendMessage(`§7${color}Hint: eggs are hidden in flower patches, on logs, barrels, hay bales, fences, crafting tables, mossy ledges, and leaf clusters.`); + player.sendMessage("§7Hint: eggs are hidden in flower patches, on logs, barrels, hay bales, fences, crafting tables, mossy ledges, and leaf clusters."); } return true; } @@ -330,7 +412,7 @@ function handleEggCommand(player, msg) { } catch (e) {} eggPositions = null; player.sendMessage(`§6[Eggs] §fWorld prefix set to §e${arg}§f — re-placing eggs...`); - system.runTimeout(() => placeAllEggs(), 20); + system.runTimeout(() => placeAllEggs(player.location), 20); return true; } @@ -360,9 +442,7 @@ try { system.run(() => handleEggCommand(player, msg)); } }); -} catch (e) { - // beforeEvents.chatSend requires beta API — chat commands unavailable -} +} catch (e) {} system.afterEvents.scriptEventReceive.subscribe((event) => { if (event.id !== "eggs:cmd") return; @@ -372,18 +452,26 @@ system.afterEvents.scriptEventReceive.subscribe((event) => { }); // ─── Startup ───────────────────────────────────────────────────── -// Egg placement fires on first player spawn, not server startup. -// This guarantees spawn chunks are loaded before setblock commands run. +// Egg placement fires on first player spawn so chunks are guaranteed loaded. +// Tag rebuild fires for every player that (re)joins — recovers lost tags from +// cross-server transfers by checking which egg blocks are actually missing. let eggsPlacedThisSession = false; world.afterEvents.playerSpawn.subscribe((event) => { - if (eggsPlacedThisSession) return; - eggsPlacedThisSession = true; - // Capture player position now; chunks around them are guaranteed loaded - const playerPos = event.player.location; - // Short delay so the player is fully settled before blocks are placed - system.runTimeout(() => placeAllEggs(playerPos), 40); + const player = event.player; + const playerPos = player.location; + + // Place eggs on first spawn of the session (40 tick delay to settle chunks) + if (!eggsPlacedThisSession) { + eggsPlacedThisSession = true; + system.runTimeout(() => placeAllEggs(playerPos), 40); + } + + // Rebuild lost tags and sync basket (80 ticks gives placeAllEggs time to finish) + system.runTimeout(() => { + try { rebuildTagsAndBasket(player); } catch (e) {} + }, 80); }); system.run(() => { diff --git a/easter-egg-addon/easter_egg_lobby_BP/scripts/main.js b/easter-egg-addon/easter_egg_lobby_BP/scripts/main.js index 4fbdee2..5f36415 100644 --- a/easter-egg-addon/easter_egg_lobby_BP/scripts/main.js +++ b/easter-egg-addon/easter_egg_lobby_BP/scripts/main.js @@ -2,8 +2,10 @@ import { world, system } from "@minecraft/server"; // ─── Constants ─────────────────────────────────────────────────── -const OBJECTIVE = "egg_count"; -const EGG_PATTERN = /^egg_[jlm]_\d+$/; +const OBJECTIVE = "egg_count"; +const BASKET_ITEM = "minecraft:nether_star"; +const BASKET_HEAD = "§6Egg Basket "; +const BASKET_REGEX = /\[j:(\d+),l:(\d+),m:(\d+)\]/; // ─── Scoreboard Setup ──────────────────────────────────────────── @@ -11,28 +13,47 @@ function ensureScoreboard() { const overworld = world.getDimension("overworld"); try { overworld.runCommand(`scoreboard objectives add ${OBJECTIVE} dummy "§6Easter Eggs §7(total)"`); - } catch (e) { - // Objective already exists — not an error - } + } catch (e) {} try { overworld.runCommand(`scoreboard objectives setdisplay sidebar ${OBJECTIVE} descending`); } catch (e) {} } -// ─── Egg Counting ──────────────────────────────────────────────── +// ─── Basket Reading ────────────────────────────────────────────── +// Counts are read from the player's Egg Basket (nether_star with encoded nameTag). +// The basket travels with the player's inventory across server transfers, making +// it the reliable cross-world persistence mechanism — unlike tags which are +// server-local and wiped on each transfer. + +function getEggBasket(player) { + try { + const inv = player.getComponent("minecraft:inventory"); + if (!inv) return null; + const container = inv.container; + for (let i = 0; i < container.size; i++) { + const item = container.getItem(i); + if (item && item.typeId === BASKET_ITEM && + item.nameTag && item.nameTag.startsWith(BASKET_HEAD)) { + return item; + } + } + } catch (e) {} + return null; +} function countPlayerEggs(player) { - const tags = player.getTags(); - let j = 0, l = 0, m = 0; - for (const tag of tags) { - if (!EGG_PATTERN.test(tag)) continue; - if (tag.startsWith("egg_j_")) j++; - else if (tag.startsWith("egg_l_")) l++; - else if (tag.startsWith("egg_m_")) m++; - } + const basket = getEggBasket(player); + if (!basket || !basket.nameTag) return { total: 0, j: 0, l: 0, m: 0 }; + const match = basket.nameTag.match(BASKET_REGEX); + if (!match) return { total: 0, j: 0, l: 0, m: 0 }; + const j = parseInt(match[1], 10); + const l = parseInt(match[2], 10); + const m = parseInt(match[3], 10); return { total: j + l + m, j, l, m }; } +// ─── Score Update ──────────────────────────────────────────────── + function updatePlayerScore(player) { const counts = countPlayerEggs(player); try { @@ -42,8 +63,8 @@ function updatePlayerScore(player) { } // ─── Player Spawn ──────────────────────────────────────────────── -// Wait 40 ticks (2 seconds) after spawn for player state/tags to fully load, -// then update their scoreboard position and show a welcome subtitle. +// Wait 40 ticks (2 s) after spawn for inventory to fully load, +// then update scoreboard and show a welcome subtitle. world.afterEvents.playerSpawn.subscribe((event) => { const player = event.player; @@ -81,14 +102,14 @@ function handleLobbyEggCommand(player) { player.sendMessage(` §bMya's World: §e${m}§7/50 ${"§3▓".repeat(Math.floor(m / 5))}§8${"░".repeat(10 - Math.floor(m / 5))}`); player.sendMessage(` §6Total: §e${total}§7/150`); - if (total === 150) { + if (total === 0) { + player.sendMessage("§7Visit Jamie, Lyla, and Mya's worlds to find hidden eggs! Look in flower patches, on barrels, hay bales, logs and more."); + } else if (total === 150) { player.sendMessage("§a§l🥚 YOU FOUND ALL 150 EGGS! You are the Easter Champion! 🥚"); } else if (total >= 100) { player.sendMessage("§6Almost there! Just " + (150 - total) + " more to go!"); } else if (total >= 50) { player.sendMessage("§eGreat progress! Keep exploring each world."); - } else if (total === 0) { - player.sendMessage("§7Visit Jamie, Lyla, and Mya's worlds to find hidden eggs!"); } } @@ -101,9 +122,7 @@ try { system.run(() => handleLobbyEggCommand(player)); } }); -} catch (e) { - // beforeEvents.chatSend requires beta API — chat commands unavailable -} +} catch (e) {} system.afterEvents.scriptEventReceive.subscribe((event) => { if (event.id !== "eggs:cmd") return; @@ -118,7 +137,6 @@ system.afterEvents.scriptEventReceive.subscribe((event) => { system.runTimeout(() => { ensureScoreboard(); - // Update scores for anyone already online for (const player of world.getAllPlayers()) { updatePlayerScore(player); }