Files
minecraft-aiworld/easter-egg-addon/easter_egg_child_BP/scripts/main.js
SysAdmin 793c1f5f3d
All checks were successful
Deploy Addons / deploy (push) Successful in 15s
fix(easter-eggs): place eggs around player location, validate before skipping
- Use joining player's position as egg centre instead of world default
  spawn, so eggs appear where players actually play
- Store centre in dynamic property so all players find the same eggs
- Validate that egg blocks actually exist before trusting eggs_placed flag,
  auto-recovering if setblock commands silently failed (e.g. unloaded chunks)
- Trigger placement on first playerSpawn not server startup timeout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 02:29:10 +01:00

392 lines
15 KiB
JavaScript

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 ───────────────────────────────────────────────
/**
* 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");
if (world.getDynamicProperty("eggs_placed") === "true") {
// Validate at least one stored egg block actually exists
const stored = getEggPositions();
if (stored && stored.length > 0) {
let found = 0;
for (let i = 0; i < Math.min(5, stored.length); i++) {
try {
const block = overworld.getBlock({ x: stored[i].x, y: stored[i].y, z: stored[i].z });
if (block && block.typeId.includes("glazed_terracotta")) found++;
} catch (e) {}
}
if (found > 0) return; // eggs are genuinely there
}
// Eggs flagged as placed but none found — 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
let cx, cy, cz;
try {
const stored = JSON.parse(world.getDynamicProperty("egg_center") || "null");
if (stored) { cx = stored.x; cy = stored.y; cz = stored.z; }
} catch (e) {}
if (cx === undefined) {
if (playerPos) {
cx = Math.floor(playerPos.x);
cy = Math.floor(playerPos.y);
cz = Math.floor(playerPos.z);
} else {
const spawn = world.getDefaultSpawnLocation();
cx = Math.floor(spawn.x); cy = Math.floor(spawn.y); cz = Math.floor(spawn.z);
}
try { world.setDynamicProperty("egg_center", JSON.stringify({ x: cx, y: cy, z: cz })); } catch (e) {}
}
const positions = [];
for (let i = 0; i < EGG_OFFSETS.length; i++) {
const [dx, dz] = EGG_OFFSETS[i];
const x = cx + dx;
const z = cz + dz;
const surfY = findSurfaceY(overworld, x, cy, 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 here — 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", "");
world.setDynamicProperty("egg_center", "");
} catch (e) {}
eggPositions = null;
eggsPlacedThisSession = false;
player.sendMessage("§6[Eggs] §fEgg data cleared — eggs will re-place around your position...");
const pos = player.location;
system.runTimeout(() => placeAllEggs(pos), 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 ─────────────────────────────────────────────────────
// Egg placement fires on first player spawn, not server startup.
// This guarantees spawn chunks are loaded before setblock commands run.
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);
});
system.run(() => {
world.sendMessage("§6[Easter Eggs] §fHunt active! Type §e!eggs §fto check your progress.");
});