menus/shared.ts
4.6 KB · sha256:19f11132c28360e8c05423288be60703d4af156c9592a85c20db17a80118a2b5
import { awaitChat, cancelAwait } from "../lib/chatbridge";
const RUNE_PLUGIN_NAME = "Rune";
let runePlugin: bukkit.plugin.Plugin | null = null;
function getRunePlugin(): bukkit.plugin.Plugin {
if (runePlugin) return runePlugin;
runePlugin = rune.bukkit.getPluginManager().getPlugin(RUNE_PLUGIN_NAME);
if (!runePlugin) {
throw new Error(`ward.menu: cannot find host plugin '${RUNE_PLUGIN_NAME}'`);
}
return runePlugin;
}
export function runOnMain(fn: () => void): void {
try {
const runnable = rune.implement("java.lang.Runnable", {
run: () => {
try {
fn();
} catch (e) {
console.error(
"ward.menu: runOnMain task threw:",
(e as Error)?.stack ?? e,
);
}
},
});
bukkit.Bukkit.getScheduler().runTask(getRunePlugin(), runnable);
} catch (e) {
console.warn("ward.menu: runOnMain schedule failed, calling inline:", e);
fn();
}
}
export function prompt(player: Player, question: string): Promise<string | null> {
const uuid = String(player.getUniqueId());
return new Promise((resolve) => {
runOnMain(() => {
player.closeInventory();
player.sendMessage(
rune.mm(
`<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━<newline>` +
`<gradient:#9b87f5:#5b3df5>${question}</gradient><newline>` +
`<gray>Type your answer in chat, or <white>cancel</white> to abort.<newline>` +
`<dark_gray>━━━━━━━━━━━━━━━━━━━━━━━━`,
),
);
});
awaitChat(uuid, (text) => {
const trimmed = text.trim();
if (!trimmed || trimmed.toLowerCase() === "cancel") {
runOnMain(() => player.sendMessage(rune.mm("<red>Cancelled.")));
resolve(null);
return;
}
resolve(trimmed);
});
// No expiry timer -- if the player just walks away the awaiter sits
// until they type or until /rune reload wipes the map. Acceptable
// for an admin tool used by trusted operators.
});
}
export function cancelPrompt(player: Player): void {
cancelAwait(String(player.getUniqueId()));
}
// ---------------------------------------------------------------------------
// Item factories shared across menus.
// ---------------------------------------------------------------------------
export const GLASS = () =>
rune.item(bukkit.Material.GRAY_STAINED_GLASS_PANE).name(" ").build();
export const BACK = () =>
rune
.item(bukkit.Material.ARROW)
.name("<yellow>« Back")
.lore(["<gray>Return to the previous menu"])
.build();
export const CLOSE = () =>
rune.item(bukkit.Material.BARRIER).name("<red>Close").build();
export const PREV = (page: number) =>
rune
.item(bukkit.Material.SPECTRAL_ARROW)
.name("<yellow>« Previous Page")
.lore([`<gray>Currently on page <white>${page + 1}`])
.build();
export const NEXT = (page: number) =>
rune
.item(bukkit.Material.SPECTRAL_ARROW)
.name("<yellow>Next Page »")
.lore([`<gray>Currently on page <white>${page + 1}`])
.build();
export const PAGE_INDICATOR = (page: number, total: number, label: string) =>
rune
.item(bukkit.Material.PAPER)
.name(`<gradient:#9b87f5:#5b3df5>${label}</gradient>`)
.lore([
`<gray>Page <white>${page + 1}<gray> / <white>${Math.max(1, total)}`,
])
.build();
// ---------------------------------------------------------------------------
// Pagination math.
// ---------------------------------------------------------------------------
export interface Page<T> {
items: T[];
page: number;
total: number;
hasPrev: boolean;
hasNext: boolean;
}
export function paginate<T>(all: T[], page: number, size: number): Page<T> {
const total = Math.max(1, Math.ceil(all.length / size));
const clamped = Math.max(0, Math.min(page, total - 1));
return {
items: all.slice(clamped * size, clamped * size + size),
page: clamped,
total,
hasPrev: clamped > 0,
hasNext: clamped < total - 1,
};
}
// ---------------------------------------------------------------------------
// MiniMessage escape -- used when injecting raw user-typed strings into
// item names/lore to keep "<click>" payloads etc. from being parsed.
// ---------------------------------------------------------------------------
export function escapeMM(s: string): string {
return String(s).replace(/</g, "\\<");
}
/** Show a transient ack message to the player above their hotbar. */
export function ack(player: Player, mm: string): void {
try {
player.sendActionBar(rune.mm(mm));
} catch {
try {
player.sendMessage(rune.mm(mm));
} catch {}
}
}