From 90a0b966342d0e537ec8fc97c64c62cca883a28d Mon Sep 17 00:00:00 2001 From: SysAdmin Date: Sun, 5 Apr 2026 02:00:11 +0100 Subject: [PATCH] feat(easter-eggs): add easter egg hunt addon across all worlds 50 eggs hidden per child world (Jamie/Lyla/Mya) in 8 creative hiding scenes (barrels, hay bales, flower patches, logs, fences, crafting tables, mossy ledges, leaf clusters). Each world uses a distinct glazed terracotta colour. Found eggs are tracked via player tags which persist across server transfers; lobby reads tags and maintains a scoreboard leaderboard (egg_count objective). Gitea deploy workflow updated to include easter-egg-addon/ and docker-compose.yml in checkout. Co-Authored-By: Claude Sonnet 4.6 --- .gitea/workflows/deploy.yml | 5 +- docker-compose.yml | 4 + .../easter_egg_child_BP/manifest.json | 29 ++ .../easter_egg_child_BP/scripts/main.js | 333 ++++++++++++++++++ .../easter_egg_lobby_BP/manifest.json | 25 ++ .../easter_egg_lobby_BP/scripts/main.js | 129 +++++++ 6 files changed, 523 insertions(+), 2 deletions(-) create mode 100644 easter-egg-addon/easter_egg_child_BP/manifest.json create mode 100644 easter-egg-addon/easter_egg_child_BP/scripts/main.js create mode 100644 easter-egg-addon/easter_egg_lobby_BP/manifest.json create mode 100644 easter-egg-addon/easter_egg_lobby_BP/scripts/main.js diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 26ec4ab..c529bee 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -7,6 +7,7 @@ on: - 'addon/**' - 'lobby-addon/**' - 'hub-return-addon/**' + - 'easter-egg-addon/**' - 'docker-compose.yml' - 'scripts/**' @@ -31,11 +32,11 @@ jobs: git init git remote add origin https://git.silverlabs.uk/SilverLABS/minecraft-aiworld.git git fetch origin main - git checkout -f origin/main -- addon/ lobby-addon/ hub-return-addon/ + git checkout -f origin/main -- addon/ lobby-addon/ hub-return-addon/ easter-egg-addon/ docker-compose.yml else cd "$APP_DIR" git fetch origin main - git checkout -f origin/main -- addon/ lobby-addon/ hub-return-addon/ + git checkout -f origin/main -- addon/ lobby-addon/ hub-return-addon/ easter-egg-addon/ docker-compose.yml fi # Restart all 4 Minecraft containers to pick up changes diff --git a/docker-compose.yml b/docker-compose.yml index 6dc3bc3..01b9787 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,6 +22,7 @@ services: - ./lobby-addon/lobby_transfer_RP:/data/resource_packs/lobby_transfer_RP - ./addon/spark_pet_BP:/data/behavior_packs/spark_pet_BP - ./addon/spark_pet_RP:/data/resource_packs/spark_pet_RP + - ./easter-egg-addon/easter_egg_lobby_BP:/data/behavior_packs/easter_egg_lobby_BP restart: unless-stopped networks: - mc-network @@ -48,6 +49,7 @@ services: - ./hub-return-addon/hub_return_transfer_BP:/data/behavior_packs/hub_return_transfer_BP - ./addon/spark_pet_BP:/data/behavior_packs/spark_pet_BP - ./addon/spark_pet_RP:/data/resource_packs/spark_pet_RP + - ./easter-egg-addon/easter_egg_child_BP:/data/behavior_packs/easter_egg_child_BP restart: unless-stopped networks: - mc-network @@ -74,6 +76,7 @@ services: - ./hub-return-addon/hub_return_transfer_BP:/data/behavior_packs/hub_return_transfer_BP - ./addon/spark_pet_BP:/data/behavior_packs/spark_pet_BP - ./addon/spark_pet_RP:/data/resource_packs/spark_pet_RP + - ./easter-egg-addon/easter_egg_child_BP:/data/behavior_packs/easter_egg_child_BP restart: unless-stopped networks: - mc-network @@ -100,6 +103,7 @@ services: - ./hub-return-addon/hub_return_transfer_BP:/data/behavior_packs/hub_return_transfer_BP - ./addon/spark_pet_BP:/data/behavior_packs/spark_pet_BP - ./addon/spark_pet_RP:/data/resource_packs/spark_pet_RP + - ./easter-egg-addon/easter_egg_child_BP:/data/behavior_packs/easter_egg_child_BP restart: unless-stopped networks: - mc-network diff --git a/easter-egg-addon/easter_egg_child_BP/manifest.json b/easter-egg-addon/easter_egg_child_BP/manifest.json new file mode 100644 index 0000000..193d801 --- /dev/null +++ b/easter-egg-addon/easter_egg_child_BP/manifest.json @@ -0,0 +1,29 @@ +{ + "format_version": 2, + "header": { + "name": "Easter Egg Hunt", + "description": "Hides 50 easter eggs around spawn for players to find", + "uuid": "c1e2a3b4-5678-9012-abcd-ef1234567890", + "version": [1, 0, 0], + "min_engine_version": [1, 21, 0] + }, + "modules": [ + { + "type": "script", + "language": "javascript", + "uuid": "d2f3b4c5-6789-0123-bcde-f12345678901", + "version": [1, 0, 0], + "entry": "scripts/main.js" + } + ], + "dependencies": [ + { + "module_name": "@minecraft/server", + "version": "1.17.0" + }, + { + "module_name": "@minecraft/server-admin", + "version": "1.0.0-beta" + } + ] +} diff --git a/easter-egg-addon/easter_egg_child_BP/scripts/main.js b/easter-egg-addon/easter_egg_child_BP/scripts/main.js new file mode 100644 index 0000000..22da500 --- /dev/null +++ b/easter-egg-addon/easter_egg_child_BP/scripts/main.js @@ -0,0 +1,333 @@ +import { world, system } 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 = ""; + +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 */ } + +if (!PREFIX) { + let worldName = ""; + try { worldName = (variables.get("world_name") || "").toLowerCase(); } catch (e) {} + + if (worldName.includes("jamie")) { PREFIX = "j"; EGG_BLOCK = "lime_glazed_terracotta"; } + else if (worldName.includes("lyla")) { PREFIX = "l"; EGG_BLOCK = "purple_glazed_terracotta"; } + else if (worldName.includes("mya")) { PREFIX = "m"; EGG_BLOCK = "cyan_glazed_terracotta"; } + else { PREFIX = "?"; EGG_BLOCK = "yellow_glazed_terracotta"; } + + try { + world.setDynamicProperty("egg_prefix", PREFIX); + world.setDynamicProperty("egg_block", EGG_BLOCK); + } catch (e) {} +} + +// ─── Egg Positions ─────────────────────────────────────────────── +// 50 (dx, dz) offsets from world spawn. +// Three rings: close (5-20), medium (20-50), far (50-100). + +const EGG_OFFSETS = [ + // Close ring — 12 eggs + [ 8, 5], [-6, 9], [12, -3], [-10, 7], [ 5, -14], [-14, -5], + [ 7, 11], [-8, -12], [15, 8], [-15, 3], [ 4, 18], [ -3, -16], + // Medium ring — 22 eggs + [ 22, 15], [-25, 8], [18, -28], [-20, -22], [35, 5], [-38, 12], + [ 30,-18], [-32, 25], [45, 10], [-42, -8], [15, 40], [-18, -35], + [ 28, 35], [-30,-28], [48, -12], [-45, 20], [38,-32], [-22, 45], + [ 12,-48], [-48,-15], [50, 22], [-38, -40], + // Far ring — 16 eggs + [ 65, 15], [-68, 22], [55, -48], [-58, 42], [ 72, 35], [-75, -18], + [ 80,-55], [-82, 40], [58, 70], [-60, -65], [ 90, 12], [-85, -50], + [ 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. + +const SCENE_NAMES = [ + "atop a barrel", + "on a hay bale", + "nestled in a flower patch", + "perched on a log", + "between fence posts", + "on a crafting table", + "on a mossy ledge", + "hidden in a leaf cluster", +]; + +// ─── Cached Positions ──────────────────────────────────────────── + +let eggPositions = null; // null = not yet loaded from storage + +function getEggPositions() { + if (eggPositions !== null) return eggPositions; + try { + const json = world.getDynamicProperty("eggs_data"); + if (json && json.length > 2) { + eggPositions = JSON.parse(json); + return eggPositions; + } + } catch (e) {} + return null; +} + +// ─── Surface Scanner ───────────────────────────────────────────── + +function findSurfaceY(dimension, x, spawnY, z) { + const top = Math.min(320, spawnY + 60); + const bottom = Math.max(-60, spawnY - 30); + for (let y = top; y >= bottom; y--) { + try { + const block = dimension.getBlock({ x, y, z }); + if (block && block.typeId !== "minecraft:air") { + return y + 1; // first empty block above the topmost solid block + } + } catch (e) { /* chunk not loaded — skip */ } + } + return spawnY; +} + +// ─── Scene Builder ─────────────────────────────────────────────── + +/** + * 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 + cmds.push(`setblock ${x} ${surfY} ${z} barrel`); + eggY = surfY + 1; + break; + + case 1: // Hay bale — egg resting on a bale of hay + cmds.push(`setblock ${x} ${surfY} ${z} hay_block`); + eggY = surfY + 1; + break; + + case 2: // Flower patch — egg nestled among wildflowers + 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 + break; + + case 3: // Log perch — egg balanced atop a cut log + cmds.push(`setblock ${x} ${surfY} ${z} oak_log`); + eggY = surfY + 1; + break; + + case 4: // Fence posts — egg elevated on a fence post 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 + break; + + case 5: // Crafting table — egg left on a workbench + cmds.push(`setblock ${x} ${surfY} ${z} crafting_table`); + eggY = surfY + 1; + break; + + case 6: // Mossy ledge — egg raised on a small 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) + 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 + break; + } + + // Always place the egg block last so it overwrites anything at its position + cmds.push(`setblock ${x} ${eggY} ${z} ${EGG_BLOCK}`); + + for (const cmd of cmds) { + try { dim.runCommand(cmd); } catch (e) { /* non-fatal — chunk may not be loaded */ } + } + + return eggY; +} + +// ─── Egg Placement ─────────────────────────────────────────────── + +function placeAllEggs() { + if (world.getDynamicProperty("eggs_placed") === "true") return; + + const spawn = world.getDefaultSpawnLocation(); + const overworld = world.getDimension("overworld"); + const positions = []; + + for (let i = 0; i < EGG_OFFSETS.length; i++) { + const [dx, dz] = EGG_OFFSETS[i]; + const x = Math.floor(spawn.x) + dx; + const z = Math.floor(spawn.z) + dz; + const surfY = findSurfaceY(overworld, x, Math.floor(spawn.y), z); + const eggY = buildScene(overworld, i, x, surfY, z); + positions.push({ x, y: eggY, z, id: i + 1 }); + } + + try { + world.setDynamicProperty("eggs_data", JSON.stringify(positions)); + world.setDynamicProperty("eggs_placed", "true"); + world.sendMessage(`§6[Easter Eggs] §f${positions.length} eggs are now hidden around spawn — happy hunting!`); + } catch (e) { + world.sendMessage(`§c[Easter Eggs] §fFailed to save egg data: ${e.message}`); + } +} + +// ─── Egg Collection ────────────────────────────────────────────── + +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; + + player.runCommand(`tag @s add ${tagId}`); + player.dimension.runCommand(`setblock ${egg.x} ${egg.y} ${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.sendMessage( + `§6[Eggs] §fEgg §e#${egg.id} §f(${SCENE_NAMES[(egg.id - 1) % 8]}) §7| §e${newCount}/50 §7in this world` + ); + + if (newCount === 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 + } +} + +// ─── Detection Loop ────────────────────────────────────────────── + +system.runInterval(() => { + const positions = getEggPositions(); + if (!positions || positions.length === 0) return; + + for (const player of world.getAllPlayers()) { + const pos = player.location; + const tags = player.getTags(); + + for (const egg of positions) { + const tagId = `egg_${PREFIX}_${egg.id}`; + if (tags.includes(tagId)) continue; // already found + + 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) { + collectEgg(player, egg, tagId); + } + } + } +}, 10); + +// ─── Chat Commands ─────────────────────────────────────────────── + +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.`); + } + return true; + } + + if (msg.startsWith("!seteggworld ")) { + const arg = msg.split(" ")[1]; + if (!["j", "l", "m"].includes(arg)) { + player.sendMessage("§c[Eggs] Usage: §e!seteggworld j§c (Jamie), §el§c (Lyla), §em§c (Mya)"); + return true; + } + const blockMap = { j: "lime_glazed_terracotta", l: "purple_glazed_terracotta", m: "cyan_glazed_terracotta" }; + PREFIX = arg; + EGG_BLOCK = blockMap[arg]; + try { + world.setDynamicProperty("egg_prefix", PREFIX); + world.setDynamicProperty("egg_block", EGG_BLOCK); + world.setDynamicProperty("eggs_placed", "false"); + world.setDynamicProperty("eggs_data", ""); + } catch (e) {} + eggPositions = null; + player.sendMessage(`§6[Eggs] §fWorld prefix set to §e${arg}§f — re-placing eggs...`); + system.runTimeout(() => placeAllEggs(), 20); + return true; + } + + if (msg === "!reseteggsworld") { + try { + world.setDynamicProperty("eggs_placed", "false"); + world.setDynamicProperty("eggs_data", ""); + } catch (e) {} + eggPositions = null; + player.sendMessage("§6[Eggs] §fEgg data cleared — re-placing all eggs..."); + system.runTimeout(() => placeAllEggs(), 20); + return true; + } + + return false; +} + +try { + world.beforeEvents.chatSend.subscribe((event) => { + const msg = event.message.trim().toLowerCase(); + if (msg === "!eggs" || msg.startsWith("!seteggworld ") || msg === "!reseteggsworld") { + event.cancel = true; + const player = event.sender; + system.run(() => handleEggCommand(player, msg)); + } + }); +} catch (e) { + // beforeEvents.chatSend requires beta API — chat commands unavailable +} + +system.afterEvents.scriptEventReceive.subscribe((event) => { + if (event.id !== "eggs:cmd") return; + const player = event.sourceEntity; + if (!player) return; + handleEggCommand(player, `!${(event.message || "").trim().toLowerCase()}`); +}); + +// ─── Startup ───────────────────────────────────────────────────── + +system.runTimeout(() => { + placeAllEggs(); +}, 120); // 6 second delay to allow world chunks to load near spawn + +system.run(() => { + world.sendMessage("§6[Easter Eggs] §fHunt active! Type §e!eggs §fto check your progress."); +}); diff --git a/easter-egg-addon/easter_egg_lobby_BP/manifest.json b/easter-egg-addon/easter_egg_lobby_BP/manifest.json new file mode 100644 index 0000000..7b1f2cc --- /dev/null +++ b/easter-egg-addon/easter_egg_lobby_BP/manifest.json @@ -0,0 +1,25 @@ +{ + "format_version": 2, + "header": { + "name": "Easter Egg Lobby", + "description": "Tracks egg counts from all worlds and shows a leaderboard in the lobby", + "uuid": "e3a4c5d6-7890-1234-cdef-012345678902", + "version": [1, 0, 0], + "min_engine_version": [1, 21, 0] + }, + "modules": [ + { + "type": "script", + "language": "javascript", + "uuid": "f4b5d6e7-8901-2345-def0-123456789013", + "version": [1, 0, 0], + "entry": "scripts/main.js" + } + ], + "dependencies": [ + { + "module_name": "@minecraft/server", + "version": "1.17.0" + } + ] +} diff --git a/easter-egg-addon/easter_egg_lobby_BP/scripts/main.js b/easter-egg-addon/easter_egg_lobby_BP/scripts/main.js new file mode 100644 index 0000000..4fbdee2 --- /dev/null +++ b/easter-egg-addon/easter_egg_lobby_BP/scripts/main.js @@ -0,0 +1,129 @@ +import { world, system } from "@minecraft/server"; + +// ─── Constants ─────────────────────────────────────────────────── + +const OBJECTIVE = "egg_count"; +const EGG_PATTERN = /^egg_[jlm]_\d+$/; + +// ─── Scoreboard Setup ──────────────────────────────────────────── + +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 + } + try { + overworld.runCommand(`scoreboard objectives setdisplay sidebar ${OBJECTIVE} descending`); + } catch (e) {} +} + +// ─── Egg Counting ──────────────────────────────────────────────── + +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++; + } + return { total: j + l + m, j, l, m }; +} + +function updatePlayerScore(player) { + const counts = countPlayerEggs(player); + try { + player.runCommand(`scoreboard players set @s ${OBJECTIVE} ${counts.total}`); + } catch (e) {} + return counts; +} + +// ─── 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. + +world.afterEvents.playerSpawn.subscribe((event) => { + const player = event.player; + system.runTimeout(() => { + try { + const { total, j, l, m } = updatePlayerScore(player); + if (total > 0) { + player.runCommand( + `titleraw @s subtitle {"rawtext":[{"text":"§7Eggs found: §6${total}§7/150 §8(§aJ:${j} §dL:${l} §bM:${m}§8)"}]}` + ); + } + if (total === 150) { + world.sendMessage(`§6§l🥚 ${player.name} has found ALL 150 easter eggs! 🥚`); + } + } catch (e) {} + }, 40); +}); + +// ─── Refresh on Interval ───────────────────────────────────────── +// Keep scores up-to-date as players arrive from child worlds. + +system.runInterval(() => { + for (const player of world.getAllPlayers()) { + updatePlayerScore(player); + } +}, 100); // every 5 seconds + +// ─── Chat Commands ─────────────────────────────────────────────── + +function handleLobbyEggCommand(player) { + const { total, j, l, m } = countPlayerEggs(player); + player.sendMessage("§6[Easter Eggs] §fYour collection:"); + player.sendMessage(` §aJamie's World: §e${j}§7/50 ${"§2▓".repeat(Math.floor(j / 5))}§8${"░".repeat(10 - Math.floor(j / 5))}`); + player.sendMessage(` §dLyla's World: §e${l}§7/50 ${"§5▓".repeat(Math.floor(l / 5))}§8${"░".repeat(10 - Math.floor(l / 5))}`); + 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) { + 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!"); + } +} + +try { + world.beforeEvents.chatSend.subscribe((event) => { + const msg = event.message.trim().toLowerCase(); + if (msg === "!eggs") { + event.cancel = true; + const player = event.sender; + system.run(() => handleLobbyEggCommand(player)); + } + }); +} catch (e) { + // beforeEvents.chatSend requires beta API — chat commands unavailable +} + +system.afterEvents.scriptEventReceive.subscribe((event) => { + if (event.id !== "eggs:cmd") return; + const player = event.sourceEntity; + if (!player) return; + if ((event.message || "").trim().toLowerCase() === "eggs") { + handleLobbyEggCommand(player); + } +}); + +// ─── Startup ───────────────────────────────────────────────────── + +system.runTimeout(() => { + ensureScoreboard(); + // Update scores for anyone already online + for (const player of world.getAllPlayers()) { + updatePlayerScore(player); + } +}, 20); + +system.run(() => { + world.sendMessage("§6[Easter Eggs] §fLeaderboard active! Type §e!eggs §fto see your full collection."); +});