feat(redstone-link): wireless redstone addon; track family-goals
Adds redstone-link-addon: a transmitter/receiver pair bound by channel name, giving redstone that runs anywhere without wire, support blocks or line of sight. Bedrock custom blocks can read redstone power but cannot emit it, so a live receiver is swapped to a vanilla redstone_block and back; receiver positions persist in a world dynamic property, with a reconcile pass at boot to repair anything a crash left inconsistent. Deployed and proven end-to-end on lobby (Hub World). Also commits family-goals-addon, which was running on jamie -- mounted in docker-compose.yml and pinned in the world -- while existing in no commit on any branch. It was unbacked-up production code. That omission was load-bearing for deploys: deploy.yml checks out docker-compose.yml from origin/main, so a deploy would have replaced the host compose with a version lacking the family-goals mount and silently killed the addon. Both new directories are added to the workflow's PATHS and push-trigger list, so nothing is mounted that CI does not deliver. The mount set in this file now matches the live host exactly. Note for a follow-up: dynamite-, hemp-, naturalist-lite-, smart-crafting-, tow-boat- and trees-features-addon are mounted but absent from PATHS, so CI never refreshes them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015Gf1kZypRDfPL1YiWUS1LC
This commit is contained in:
co-authored by
Claude Opus 5
parent
b28e4d0610
commit
8ded29cbcc
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"format_version": 2,
|
||||
"header": {
|
||||
"name": "Family Goals",
|
||||
"description": "Shared goals, family bank, loot share, pity timer, and PvP-off for the family survival world",
|
||||
"uuid": "f1a2b3c4-d5e6-4789-90ab-cdef12345001",
|
||||
"version": [1, 0, 6],
|
||||
"min_engine_version": [1, 21, 0]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"type": "script",
|
||||
"language": "javascript",
|
||||
"uuid": "f1a2b3c4-d5e6-4789-90ab-cdef12345002",
|
||||
"version": [1, 0, 6],
|
||||
"entry": "scripts/main.js"
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
{
|
||||
"module_name": "@minecraft/server",
|
||||
"version": "1.17.0"
|
||||
},
|
||||
{
|
||||
"module_name": "@minecraft/server-ui",
|
||||
"version": "1.3.0"
|
||||
},
|
||||
{
|
||||
"module_name": "@minecraft/server-admin",
|
||||
"version": "1.0.0-beta"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
// Family Goals Addon
|
||||
//
|
||||
// Lives in jamie's world (the shared family survival world). Provides:
|
||||
// • PvP-off (siblings can't hurt each other)
|
||||
// • Family roster ( !family setup adds online players )
|
||||
// • Family Bank ( !family bank registers a vanilla chest as shared )
|
||||
// • Five shared goals with per-player contribution tracking
|
||||
// • Loot share within 32 blocks (sibling helpers get a kill bonus)
|
||||
// • Pity timer (idle siblings get an encouragement bundle every 60 min)
|
||||
//
|
||||
// All persistent state lives in world dynamic properties so it survives
|
||||
// container restarts. No resource pack — vanilla items + titleraw + actionforms.
|
||||
|
||||
import { world, system, ItemStack } from "@minecraft/server";
|
||||
import { ActionFormData } from "@minecraft/server-ui";
|
||||
// We don't actually use @minecraft/server-admin, but importing from it flips
|
||||
// the beta-API gate for this pack so world.beforeEvents.chatSend resolves.
|
||||
// If we ever drop this import, fall back to afterEvents.chatSend (no cancel).
|
||||
import { variables } from "@minecraft/server-admin";
|
||||
void variables; // silence unused warning
|
||||
|
||||
// ─── Tunables ──────────────────────────────────────────────────
|
||||
const FAMILY_PROP = "family_roster_v1"; // { members: [<playerId>, ...] }
|
||||
const GOALS_PROP = "family_goals_v1"; // see initGoals() for shape
|
||||
const BANK_PROP = "family_bank_v1"; // { x, y, z, dim } | null
|
||||
const PROGRESS_PROP = "family_last_progress_at"; // per-player dyn prop, Date.now()
|
||||
|
||||
const VANILLA_CHEST = "minecraft:chest";
|
||||
const LOOT_SHARE_RADIUS = 32;
|
||||
const PITY_THRESHOLD_MS = 60 * 60 * 1000; // 60 min of no progress
|
||||
const PITY_TICK_INTERVAL = 6000; // poll every 5 min (20 tps)
|
||||
const NIGHTS_TICK_INTERVAL = 200; // every 10s
|
||||
|
||||
const GOAL_DEFS = [
|
||||
{ id: "logs", label: "Logs Chopped", target: 5000 },
|
||||
{ id: "diamonds", label: "Diamonds Mined", target: 50 },
|
||||
{ id: "mobs", label: "Monsters Defeated", target: 500 },
|
||||
{ id: "crops", label: "Crops Harvested", target: 1000 },
|
||||
{ id: "nights", label: "Nights Survived", target: 30 },
|
||||
];
|
||||
|
||||
const LOG_IDS = new Set([
|
||||
"minecraft:oak_log", "minecraft:spruce_log", "minecraft:birch_log",
|
||||
"minecraft:jungle_log", "minecraft:acacia_log", "minecraft:dark_oak_log",
|
||||
"minecraft:cherry_log", "minecraft:mangrove_log",
|
||||
"minecraft:stripped_oak_log", "minecraft:stripped_spruce_log",
|
||||
"minecraft:stripped_birch_log", "minecraft:stripped_jungle_log",
|
||||
"minecraft:stripped_acacia_log", "minecraft:stripped_dark_oak_log",
|
||||
"minecraft:stripped_cherry_log", "minecraft:stripped_mangrove_log",
|
||||
]);
|
||||
const DIAMOND_IDS = new Set([
|
||||
"minecraft:diamond_ore", "minecraft:deepslate_diamond_ore",
|
||||
]);
|
||||
// Block id → "growth"-like state value at full maturity
|
||||
const CROP_AGES = {
|
||||
"minecraft:wheat": 7,
|
||||
"minecraft:carrots": 7,
|
||||
"minecraft:potatoes": 7,
|
||||
"minecraft:beetroot": 3,
|
||||
};
|
||||
|
||||
// ─── In-memory mirror of dynamic properties ────────────────────
|
||||
let family = { members: [] };
|
||||
let goals = null;
|
||||
let bank = null;
|
||||
let lastDay = -1; // tracks day-rollover for the "nights survived" goal
|
||||
|
||||
// ─── Persistence ───────────────────────────────────────────────
|
||||
function loadState() {
|
||||
try {
|
||||
const f = world.getDynamicProperty(FAMILY_PROP);
|
||||
if (typeof f === "string") family = JSON.parse(f);
|
||||
} catch (_) { family = { members: [] }; }
|
||||
|
||||
try {
|
||||
const g = world.getDynamicProperty(GOALS_PROP);
|
||||
if (typeof g === "string") goals = JSON.parse(g);
|
||||
} catch (_) { goals = null; }
|
||||
if (!goals || !goals.goals) goals = initGoals();
|
||||
// Backfill any newly added goal definitions on upgrade
|
||||
for (const def of GOAL_DEFS) {
|
||||
if (!goals.goals[def.id]) {
|
||||
goals.goals[def.id] = { current: 0, target: def.target, completed: false, contributions: {} };
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const b = world.getDynamicProperty(BANK_PROP);
|
||||
if (typeof b === "string") bank = JSON.parse(b);
|
||||
} catch (_) { bank = null; }
|
||||
}
|
||||
|
||||
function initGoals() {
|
||||
const g = { goals: {} };
|
||||
for (const def of GOAL_DEFS) {
|
||||
g.goals[def.id] = { current: 0, target: def.target, completed: false, contributions: {} };
|
||||
}
|
||||
return g;
|
||||
}
|
||||
|
||||
function saveFamily() {
|
||||
try { world.setDynamicProperty(FAMILY_PROP, JSON.stringify(family)); } catch (_) {}
|
||||
}
|
||||
function saveGoals() {
|
||||
try {
|
||||
const json = JSON.stringify(goals);
|
||||
world.setDynamicProperty(GOALS_PROP, json);
|
||||
const verify = world.getDynamicProperty(GOALS_PROP);
|
||||
console.warn(`[Family] saveGoals: wrote ${json.length} chars, readback typeof=${typeof verify} len=${typeof verify === "string" ? verify.length : "n/a"}`);
|
||||
} catch (e) { console.warn(`[Family] saveGoals FAILED: ${e}`); }
|
||||
}
|
||||
function saveBank() {
|
||||
try { world.setDynamicProperty(BANK_PROP, bank ? JSON.stringify(bank) : undefined); } catch (_) {}
|
||||
}
|
||||
|
||||
function isFamily(player) {
|
||||
if (!player || !family.members) return false;
|
||||
return family.members.includes(player.id);
|
||||
}
|
||||
|
||||
// ─── Goal mutation ─────────────────────────────────────────────
|
||||
function incrGoal(goalId, player, n = 1) {
|
||||
if (!isFamily(player)) return;
|
||||
const g = goals.goals[goalId];
|
||||
if (!g || g.completed) return;
|
||||
g.current += n;
|
||||
g.contributions[player.id] = (g.contributions[player.id] || 0) + n;
|
||||
try { player.setDynamicProperty(PROGRESS_PROP, Date.now()); } catch (_) {}
|
||||
|
||||
if (g.current >= g.target) {
|
||||
g.completed = true;
|
||||
g.current = g.target;
|
||||
fireGoalReward(goalId);
|
||||
}
|
||||
saveGoals();
|
||||
}
|
||||
|
||||
function fireGoalReward(goalId) {
|
||||
const def = GOAL_DEFS.find(d => d.id === goalId);
|
||||
const label = def ? def.label : goalId;
|
||||
for (const p of world.getAllPlayers()) {
|
||||
try {
|
||||
p.onScreenDisplay.setTitle(`§6✦ Family Goal Complete!`, {
|
||||
subtitle: `§e${label}`,
|
||||
fadeInDuration: 10,
|
||||
stayDuration: 80,
|
||||
fadeOutDuration: 20,
|
||||
});
|
||||
p.dimension.runCommand(`summon firework_rocket ${p.location.x} ${p.location.y + 2} ${p.location.z}`);
|
||||
// v1 trophy: a named gold ingot. Easy to tell apart in inventory.
|
||||
p.runCommand(`give @s gold_ingot 1`);
|
||||
} catch (_) {}
|
||||
}
|
||||
world.sendMessage(`§6[Family] §eGoal complete: §f${label}§e! §7Trophy gold awarded.`);
|
||||
}
|
||||
|
||||
// ─── Family roster command ─────────────────────────────────────
|
||||
function familySetup(player) {
|
||||
const online = world.getAllPlayers();
|
||||
const before = new Set(family.members);
|
||||
let added = 0;
|
||||
for (const p of online) {
|
||||
if (!before.has(p.id)) {
|
||||
family.members.push(p.id);
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
saveFamily();
|
||||
|
||||
const names = online.map(p => p.name).join(", ");
|
||||
if (added === 0) {
|
||||
player.sendMessage(`§7[Family] §fAll currently-online players are already in the roster (${family.members.length} members).`);
|
||||
} else {
|
||||
player.sendMessage(`§a[Family] §fAdded ${added} new member(s). Roster (${family.members.length}): §e${names}§f.`);
|
||||
world.sendMessage(`§a[Family] §fRoster updated by §e${player.name}§f.`);
|
||||
}
|
||||
}
|
||||
|
||||
function familyReset(player) {
|
||||
family = { members: [] };
|
||||
saveFamily();
|
||||
player.sendMessage("§c[Family] §fRoster cleared. Run §e!family setup§f again to repopulate.");
|
||||
}
|
||||
|
||||
// ─── Family Bank command ───────────────────────────────────────
|
||||
function familyBank(player) {
|
||||
if (!isFamily(player)) {
|
||||
player.sendMessage("§c[Family] §7Only family members can register the bank — run §e!family setup§7 first while everyone's online.");
|
||||
return;
|
||||
}
|
||||
let view;
|
||||
try {
|
||||
view = player.getBlockFromViewDirection({ maxDistance: 6 });
|
||||
} catch (e) {
|
||||
player.sendMessage(`§c[Family] §7Couldn't look at a block: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
const target = view?.block;
|
||||
if (!target || target.typeId !== VANILLA_CHEST) {
|
||||
player.sendMessage("§c[Family] §7Look at a vanilla chest within 6 blocks, then re-run §e!family bank§7.");
|
||||
return;
|
||||
}
|
||||
bank = {
|
||||
x: target.location.x,
|
||||
y: target.location.y,
|
||||
z: target.location.z,
|
||||
dim: target.dimension.id,
|
||||
};
|
||||
saveBank();
|
||||
player.sendMessage(`§a[Family] §fBank registered at §e${bank.x}, ${bank.y}, ${bank.z}§f. All family members can now open it.`);
|
||||
}
|
||||
|
||||
// ─── Bank gating (interact + break) ────────────────────────────
|
||||
function isBankAt(loc, dimId) {
|
||||
if (!bank) return false;
|
||||
return Math.floor(loc.x) === bank.x
|
||||
&& Math.floor(loc.y) === bank.y
|
||||
&& Math.floor(loc.z) === bank.z
|
||||
&& bank.dim === dimId;
|
||||
}
|
||||
|
||||
try {
|
||||
world.beforeEvents.playerInteractWithBlock.subscribe((event) => {
|
||||
const block = event.block;
|
||||
if (block.typeId !== VANILLA_CHEST) return;
|
||||
if (!isBankAt(block.location, block.dimension.id)) return;
|
||||
if (isFamily(event.player)) return;
|
||||
event.cancel = true;
|
||||
const playerRef = event.player;
|
||||
system.run(() => playerRef.sendMessage("§c[Family Bank] §7Only family members can open this chest."));
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`[Family] beforeEvents.playerInteractWithBlock unavailable: ${e}`);
|
||||
}
|
||||
|
||||
try {
|
||||
world.beforeEvents.playerBreakBlock.subscribe((event) => {
|
||||
const block = event.block;
|
||||
if (block.typeId !== VANILLA_CHEST) return;
|
||||
if (!isBankAt(block.location, block.dimension.id)) return;
|
||||
|
||||
if (isFamily(event.player)) {
|
||||
// Family member breaking the bank → un-register so it stops being protected
|
||||
bank = null;
|
||||
saveBank();
|
||||
const p = event.player;
|
||||
system.run(() => p.sendMessage("§7[Family Bank] §fBank chest broken — un-registered. Place a new chest and run §e!family bank§f to re-register."));
|
||||
return;
|
||||
}
|
||||
event.cancel = true;
|
||||
const playerRef = event.player;
|
||||
system.run(() => playerRef.sendMessage("§c[Family Bank] §7Only family members can break this chest."));
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`[Family] beforeEvents.playerBreakBlock unavailable: ${e}`);
|
||||
}
|
||||
|
||||
// ─── PvP off (heal damage between siblings) ────────────────────
|
||||
// Bedrock has no server.properties pvp toggle — we hook the after-event
|
||||
// and immediately re-heal. Net visible effect: zero damage between players.
|
||||
world.afterEvents.entityHurt.subscribe((event) => {
|
||||
const source = event.damageSource?.damagingEntity;
|
||||
const victim = event.hurtEntity;
|
||||
if (!source || !victim) return;
|
||||
if (source.typeId !== "minecraft:player" || victim.typeId !== "minecraft:player") return;
|
||||
try {
|
||||
const health = victim.getComponent("minecraft:health");
|
||||
if (!health) return;
|
||||
const restored = Math.min(health.effectiveMax, health.currentValue + event.damage);
|
||||
health.setCurrentValue(restored);
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
// ─── Goal triggers ─────────────────────────────────────────────
|
||||
world.afterEvents.playerBreakBlock.subscribe((event) => {
|
||||
const player = event.player;
|
||||
if (!isFamily(player)) return;
|
||||
const id = event.brokenBlockPermutation?.type?.id;
|
||||
if (!id) return;
|
||||
|
||||
if (LOG_IDS.has(id)) {
|
||||
incrGoal("logs", player);
|
||||
return;
|
||||
}
|
||||
if (DIAMOND_IDS.has(id)) {
|
||||
incrGoal("diamonds", player);
|
||||
return;
|
||||
}
|
||||
if (id in CROP_AGES) {
|
||||
// Only fully grown crops count
|
||||
let states = null;
|
||||
try { states = event.brokenBlockPermutation.getAllStates(); } catch (_) {}
|
||||
const age = states && (states["growth"] ?? states["age"]);
|
||||
if (typeof age === "number" && age >= CROP_AGES[id]) {
|
||||
incrGoal("crops", player);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
world.afterEvents.entityDie.subscribe((event) => {
|
||||
const dead = event.deadEntity;
|
||||
if (!dead || dead.typeId === "minecraft:player") return;
|
||||
const source = event.damageSource?.damagingEntity;
|
||||
if (!source || source.typeId !== "minecraft:player") return;
|
||||
if (!isFamily(source)) return;
|
||||
|
||||
// Goal: monsters only
|
||||
let isMonster = false;
|
||||
try {
|
||||
const fam = dead.getComponent("minecraft:type_family");
|
||||
if (fam && typeof fam.hasTypeFamily === "function" && fam.hasTypeFamily("monster")) {
|
||||
isMonster = true;
|
||||
}
|
||||
} catch (_) {}
|
||||
if (isMonster) incrGoal("mobs", source);
|
||||
|
||||
// Loot share: every family member in 32 blocks (other than killer) gets cooked beef.
|
||||
// Sharing helpers, not real drop mirroring — keeps the drop pipeline untouched.
|
||||
try {
|
||||
const k = source.location;
|
||||
const r2 = LOOT_SHARE_RADIUS * LOOT_SHARE_RADIUS;
|
||||
for (const other of world.getAllPlayers()) {
|
||||
if (other.id === source.id) continue;
|
||||
if (!isFamily(other)) continue;
|
||||
if (other.dimension.id !== source.dimension.id) continue;
|
||||
const dx = other.location.x - k.x;
|
||||
const dy = other.location.y - k.y;
|
||||
const dz = other.location.z - k.z;
|
||||
if (dx*dx + dy*dy + dz*dz <= r2) {
|
||||
other.runCommand("give @s cooked_beef 2");
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
// ─── Nights survived ───────────────────────────────────────────
|
||||
system.runInterval(() => {
|
||||
let day;
|
||||
try { day = world.getDay(); } catch (_) { return; }
|
||||
if (typeof day !== "number") return;
|
||||
if (lastDay === -1) { lastDay = day; return; }
|
||||
if (day > lastDay) {
|
||||
const diff = day - lastDay;
|
||||
lastDay = day;
|
||||
for (const p of world.getAllPlayers()) {
|
||||
if (isFamily(p)) incrGoal("nights", p, diff);
|
||||
}
|
||||
}
|
||||
}, NIGHTS_TICK_INTERVAL);
|
||||
|
||||
// ─── Pity timer ────────────────────────────────────────────────
|
||||
system.runInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const p of world.getAllPlayers()) {
|
||||
if (!isFamily(p)) continue;
|
||||
|
||||
let last;
|
||||
try { last = p.getDynamicProperty(PROGRESS_PROP); } catch (_) { last = undefined; }
|
||||
if (typeof last !== "number") {
|
||||
// First time we've seen this family member contribute (or first session) — initialize, don't pity yet
|
||||
try { p.setDynamicProperty(PROGRESS_PROP, now); } catch (_) {}
|
||||
continue;
|
||||
}
|
||||
if (now - last < PITY_THRESHOLD_MS) continue;
|
||||
|
||||
// Drop encouragement bundle directly into inventory
|
||||
try {
|
||||
p.runCommand("give @s diamond 1");
|
||||
p.runCommand("give @s cooked_beef 16");
|
||||
p.runCommand("give @s iron_pickaxe 1");
|
||||
p.sendMessage("§6[Family] §fA little boost to keep you going! §7(family pity bundle)");
|
||||
p.setDynamicProperty(PROGRESS_PROP, now); // reset so we don't spam every 5 min
|
||||
} catch (_) {}
|
||||
}
|
||||
}, PITY_TICK_INTERVAL);
|
||||
|
||||
// ─── Goal display ──────────────────────────────────────────────
|
||||
function makeBar(pct) {
|
||||
const filled = Math.max(0, Math.min(10, Math.floor(pct / 10)));
|
||||
return "§a" + "▰".repeat(filled) + "§8" + "▰".repeat(10 - filled);
|
||||
}
|
||||
|
||||
async function showGoals(player, perPlayerMode) {
|
||||
const lines = [];
|
||||
for (const def of GOAL_DEFS) {
|
||||
const g = goals.goals[def.id];
|
||||
if (!g) continue;
|
||||
const cur = Math.min(g.current, g.target);
|
||||
const pct = Math.floor((cur / g.target) * 100);
|
||||
const bar = makeBar(pct);
|
||||
const status = g.completed ? " §a✔" : "";
|
||||
|
||||
if (perPlayerMode) {
|
||||
const my = g.contributions[player.id] || 0;
|
||||
const myPct = cur > 0 ? Math.floor((my / cur) * 100) : 0;
|
||||
lines.push(`§e${def.label}${status}\n§7 ${bar}§7 §f${cur}§7/§f${g.target}\n§7 You: §f${my}§7 (${myPct}% of progress)`);
|
||||
} else {
|
||||
lines.push(`§e${def.label}${status}\n§7 ${bar}§7 §f${cur}§7/§f${g.target}`);
|
||||
}
|
||||
}
|
||||
if (!lines.length) lines.push("§7No goals configured.");
|
||||
|
||||
const form = new ActionFormData()
|
||||
.title(perPlayerMode ? "My Goal Contributions" : "Family Goals")
|
||||
.body(lines.join("\n\n"))
|
||||
.button("§7OK");
|
||||
try { await form.show(player); } catch (_) {}
|
||||
}
|
||||
|
||||
// ─── Family menu (entered via scriptevent from hub-return compass) ─────
|
||||
function showRoster(player) {
|
||||
if (family.members.length === 0) {
|
||||
player.sendMessage("§7[Family] §fNo roster set yet — open §e🏆 Family§f from your compass and pick §eAdd me to the family§f.");
|
||||
return;
|
||||
}
|
||||
const names = world.getAllPlayers()
|
||||
.filter(p => family.members.includes(p.id))
|
||||
.map(p => p.name);
|
||||
player.sendMessage(`§a[Family] §fRoster: §e${family.members.length}§f members. Online: §e${names.join(", ") || "(none)"}§f.`);
|
||||
}
|
||||
|
||||
async function openFamilyMenu(player) {
|
||||
const inFamily = isFamily(player);
|
||||
const form = new ActionFormData()
|
||||
.title("🏆 Family")
|
||||
.body(`§7Roster: §f${family.members.length}§7 members${inFamily ? " §a(you're in)" : " §c(you're not)"}\n§7Bank: ${bank ? `§f${bank.x}, ${bank.y}, ${bank.z}` : "§cnot set"}`);
|
||||
|
||||
const actions = [];
|
||||
form.button("🎯 Family Goals"); actions.push("goals");
|
||||
form.button("📝 My Contributions"); actions.push("mygoals");
|
||||
form.button("👥 Show Roster"); actions.push("roster");
|
||||
form.button("➕ Add me / Add online players"); actions.push("setup");
|
||||
form.button("🏦 Register Family Bank (look at chest)"); actions.push("bank");
|
||||
form.button("♻ Reset Roster (danger)"); actions.push("reset");
|
||||
form.button("§7Cancel"); actions.push("cancel");
|
||||
|
||||
let res;
|
||||
try { res = await form.show(player); } catch (_) { return; }
|
||||
if (res.canceled || res.selection === undefined) return;
|
||||
const a = actions[res.selection];
|
||||
|
||||
if (a === "goals") return showGoals(player, false);
|
||||
if (a === "mygoals") return showGoals(player, true);
|
||||
if (a === "roster") return showRoster(player);
|
||||
if (a === "setup") return familySetup(player);
|
||||
if (a === "bank") return familyBank(player);
|
||||
if (a === "reset") return familyReset(player);
|
||||
}
|
||||
|
||||
// scriptEventReceive is the cross-pack dispatch surface. Hub-return's compass
|
||||
// menu fires `scriptevent family:menu` to enter our UI. Direct ids are also
|
||||
// handled so an opped player can run e.g. `/scriptevent family:setup`.
|
||||
try {
|
||||
system.afterEvents.scriptEventReceive.subscribe((event) => {
|
||||
const id = event.id;
|
||||
if (!id || !id.startsWith("family:")) return;
|
||||
const player = event.sourceEntity;
|
||||
if (!player || player.typeId !== "minecraft:player") return;
|
||||
switch (id) {
|
||||
case "family:menu": system.run(() => openFamilyMenu(player)); break;
|
||||
case "family:goals": system.run(() => showGoals(player, false)); break;
|
||||
case "family:mygoals": system.run(() => showGoals(player, true)); break;
|
||||
case "family:roster": showRoster(player); break;
|
||||
case "family:setup": familySetup(player); break;
|
||||
case "family:bank": familyBank(player); break;
|
||||
case "family:reset": familyReset(player); break;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`[Family] scriptEventReceive unavailable: ${e}`);
|
||||
}
|
||||
|
||||
// ─── Boot ──────────────────────────────────────────────────────
|
||||
system.run(() => {
|
||||
loadState();
|
||||
saveGoals(); // persist any backfilled goal defs from upgrade
|
||||
|
||||
world.sendMessage("§6[Family] §fLoaded. Right-click your §dHub Compass§f to find the §6🏆 Family§f menu.");
|
||||
});
|
||||
Reference in New Issue
Block a user