menus/groups.ts
12 KB · sha256:4688ea974abce74205574392ad218ba37915ccc9ca4e4e75a1a6d02c64f39658
import {
listGroups,
findGroup,
createGroup,
deleteGroup,
setWeight,
setPrefix,
setSuffix,
addParent,
removeParent,
setDefaultGroup,
getParents,
getDefaultGroup,
addPermission,
} from "../models/Group";
import { refreshGroupMembers } from "../lib/attachments";
import { invalidateGroup } from "../placeholders/papi";
import {
GLASS,
BACK,
CLOSE,
PREV,
NEXT,
PAGE_INDICATOR,
paginate,
prompt,
runOnMain,
ack,
escapeMM,
} from "./shared";
import { openAdminMenu } from "./index";
import { openConfirm } from "./users";
const PAGE_SIZE = 28;
export async function openGroupsMenu(viewer: Player, page = 0): Promise<void> {
const groups = (await listGroups()).slice().sort(
(a, b) => (b.weight ?? 0) - (a.weight ?? 0),
);
const def = await getDefaultGroup();
const pageData = paginate(groups, page, PAGE_SIZE);
runOnMain(() => {
const gui = rune.gui({
title: "<gradient:#9b87f5:#5b3df5>Ward · Groups</gradient>",
rows: 6,
});
gui.slot(
4,
rune
.item(bukkit.Material.WRITABLE_BOOK)
.name("<green>Create Group")
.lore([
"<gray>Type a name in chat to create",
"<gray>a new (empty) group.",
"",
"<yellow>Click to create",
])
.build(),
async (e) => {
const p = e.getWhoClicked() as Player;
const name = await prompt(p, "Name the new group");
if (!name) return openGroupsMenu(p, page);
try {
await createGroup(name);
ack(p, `<green>Created group <white>${escapeMM(name)}`);
} catch (err) {
ack(p, `<red>${escapeMM((err as Error).message)}`);
}
return openGroupsMenu(p, page);
},
);
// Body: 4 rows of 7, cols 1-7 of rows 1-4
pageData.items.forEach((g, i) => {
const row = 1 + Math.floor(i / 7);
const col = 1 + (i % 7);
const slot = row * 9 + col;
const isDefault = def?.name?.toLowerCase() === g.name.toLowerCase();
const mat = isDefault
? bukkit.Material.ENCHANTED_BOOK
: g.weight >= 100
? bukkit.Material.GOLDEN_HELMET
: g.weight >= 50
? bukkit.Material.IRON_HELMET
: bukkit.Material.LEATHER_HELMET;
gui.slot(
slot,
rune
.item(mat)
.name(
`<gradient:#ffd166:#f7b500>${escapeMM(g.displayName || g.name)}</gradient>` +
(isDefault ? " <gray>(default)" : ""),
)
.lore([
`<gray>Weight: <white>${g.weight ?? 0}`,
`<gray>Nodes: <white>${g.nodes?.length ?? 0}`,
`<gray>Identifier: <dark_gray>${g.name}`,
"",
"<yellow>Click to manage",
])
.build(),
(e) => openGroupDetail(e.getWhoClicked() as Player, g.name),
);
});
// Bottom nav
if (pageData.hasPrev) {
gui.slot(45, PREV(pageData.page), (e) =>
openGroupsMenu(e.getWhoClicked() as Player, pageData.page - 1),
);
} else {
gui.slot(45, GLASS());
}
gui.slot(49, PAGE_INDICATOR(pageData.page, pageData.total, "Groups"));
if (pageData.hasNext) {
gui.slot(53, NEXT(pageData.page), (e) =>
openGroupsMenu(e.getWhoClicked() as Player, pageData.page + 1),
);
} else {
gui.slot(53, GLASS());
}
gui.slot(48, BACK(), (e) => openAdminMenu(e.getWhoClicked() as Player));
gui.slot(50, CLOSE(), (e) => e.getWhoClicked().closeInventory());
gui.open(viewer);
});
}
// ---------------------------------------------------------------------------
// Group detail
// ---------------------------------------------------------------------------
export async function openGroupDetail(
viewer: Player,
groupName: string,
): Promise<void> {
const g = await findGroup(groupName);
if (!g) {
runOnMain(() =>
viewer.sendMessage(rune.mm(`<red>Group not found: ${escapeMM(groupName)}`)),
);
return openGroupsMenu(viewer, 0);
}
const parents = await getParents(groupName);
const def = await getDefaultGroup();
const isDefault = def?.name?.toLowerCase() === g.name.toLowerCase();
runOnMain(() => {
const gui = rune.gui({
title: `<gradient:#9b87f5:#5b3df5>Group · ${escapeMM(g.name)}</gradient>`,
rows: 5,
});
gui.fill(GLASS());
// Header card
gui.slot(
4,
rune
.item(bukkit.Material.ENCHANTED_BOOK)
.name(`<gradient:#ffd166:#f7b500>${escapeMM(g.displayName || g.name)}</gradient>`)
.lore([
`<gray>Identifier: <white>${g.name}`,
`<gray>Weight: <white>${g.weight ?? 0}`,
`<gray>Parents: <white>${parents.length ? parents.map(escapeMM).join(", ") : "<dark_gray>(none)"}`,
`<gray>Nodes: <white>${g.nodes?.length ?? 0}`,
isDefault ? "<gold>★ default group" : "<dark_gray>(not default)",
])
.build(),
);
// Action row 1 (slots 19-25)
gui.slot(
19,
rune
.item(bukkit.Material.ANVIL)
.name("<yellow>Set Weight")
.lore([
`<gray>Current: <white>${g.weight ?? 0}`,
"",
"<gray>Higher weight wins on conflicts.",
"<yellow>Click to enter a new value",
])
.build(),
async (e) => {
const p = e.getWhoClicked() as Player;
const text = await prompt(p, "Type the new weight (integer)");
if (text != null) {
const n = parseInt(text, 10);
if (Number.isFinite(n)) {
await setWeight(g.name, n);
invalidateGroup(g.name);
await refreshGroupMembers(g.name);
ack(p, `<green>Weight set to <white>${n}`);
} else {
ack(p, `<red>"${escapeMM(text)}" is not an integer`);
}
}
return openGroupDetail(p, g.name);
},
);
gui.slot(
20,
rune
.item(bukkit.Material.NAME_TAG)
.name("<yellow>Set Prefix")
.lore([
"<gray>MiniMessage supported.",
"<gray>Type empty to clear.",
])
.build(),
async (e) => {
const p = e.getWhoClicked() as Player;
const text = await prompt(p, `Type prefix for ${escapeMM(g.name)}`);
if (text != null) {
await setPrefix(g.name, text);
await refreshGroupMembers(g.name);
ack(p, "<green>Prefix updated.");
}
return openGroupDetail(p, g.name);
},
);
gui.slot(
21,
rune
.item(bukkit.Material.NAME_TAG)
.name("<yellow>Set Suffix")
.lore(["<gray>MiniMessage supported."])
.build(),
async (e) => {
const p = e.getWhoClicked() as Player;
const text = await prompt(p, `Type suffix for ${escapeMM(g.name)}`);
if (text != null) {
await setSuffix(g.name, text);
await refreshGroupMembers(g.name);
ack(p, "<green>Suffix updated.");
}
return openGroupDetail(p, g.name);
},
);
gui.slot(
22,
rune
.item(bukkit.Material.WRITABLE_BOOK)
.name("<yellow>Add Permission")
.lore(["<gray>Grant a permission to this group."])
.build(),
async (e) => {
const p = e.getWhoClicked() as Player;
const perm = await prompt(p, "Type the permission node");
if (perm) {
await addPermission(g.name, perm);
await refreshGroupMembers(g.name);
ack(p, `<green>Granted <white>${escapeMM(perm)}<green> to <white>${escapeMM(g.name)}`);
}
return openGroupDetail(p, g.name);
},
);
gui.slot(
23,
rune
.item(bukkit.Material.LIME_DYE)
.name("<green>Add Parent")
.lore(["<gray>Inherit perms from another group."])
.build(),
(e) => openParentPicker(e.getWhoClicked() as Player, g.name, "add"),
);
gui.slot(
24,
rune
.item(bukkit.Material.RED_DYE)
.name("<red>Remove Parent")
.lore(["<gray>Stop inheriting from one of:", ...parents.map((p) => " <white>" + escapeMM(p))])
.build(),
(e) => openParentPicker(e.getWhoClicked() as Player, g.name, "remove"),
);
gui.slot(
25,
rune
.item(
isDefault ? bukkit.Material.GOLD_BLOCK : bukkit.Material.GOLD_INGOT,
)
.name(isDefault ? "<gold>★ Default Group" : "<yellow>Set as Default")
.lore([
isDefault
? "<gray>Already the default group."
: "<gray>New players join this group.",
])
.build(),
async (e) => {
if (isDefault) return;
const p = e.getWhoClicked() as Player;
await setDefaultGroup(g.name);
ack(p, `<green>Default group is now <white>${escapeMM(g.name)}`);
return openGroupDetail(p, g.name);
},
);
// Action row 2: delete (slot 31)
gui.slot(
31,
rune
.item(bukkit.Material.TNT)
.name("<red>Delete Group")
.lore([
`<gray>Permanently remove <white>${escapeMM(g.name)}`,
"<red>This cannot be undone.",
"",
"<yellow>Click to confirm",
])
.build(),
(e) =>
openConfirm(
e.getWhoClicked() as Player,
`Delete group ${g.name}?`,
async () => {
await deleteGroup(g.name);
invalidateGroup(g.name);
ack(
e.getWhoClicked() as Player,
`<green>Deleted <white>${escapeMM(g.name)}`,
);
return openGroupsMenu(e.getWhoClicked() as Player, 0);
},
() => openGroupDetail(e.getWhoClicked() as Player, g.name),
),
);
// Bottom nav
gui.slot(36, BACK(), (e) => openGroupsMenu(e.getWhoClicked() as Player, 0));
gui.slot(44, CLOSE(), (e) => e.getWhoClicked().closeInventory());
gui.open(viewer);
});
}
// ---------------------------------------------------------------------------
// Parent picker (add or remove). Just a list of every other group.
// ---------------------------------------------------------------------------
async function openParentPicker(
viewer: Player,
groupName: string,
mode: "add" | "remove",
): Promise<void> {
const currentParents = await getParents(groupName);
const all = (await listGroups()).map((g) => g.name);
const choices =
mode === "add"
? all.filter(
(n) =>
n.toLowerCase() !== groupName.toLowerCase() &&
!currentParents.includes(n.toLowerCase()),
)
: currentParents;
runOnMain(() => {
const rows = Math.max(2, Math.min(6, Math.ceil(choices.length / 9) + 1));
const gui = rune.gui({
title:
mode === "add"
? `<green>Add parent to ${escapeMM(groupName)}`
: `<red>Remove parent from ${escapeMM(groupName)}`,
rows,
});
if (choices.length === 0) {
gui.slot(
4,
rune
.item(bukkit.Material.BARRIER)
.name("<gray>(no candidates)")
.build(),
);
}
choices.forEach((n, i) => {
gui.slot(
i,
rune
.item(bukkit.Material.BOOK)
.name(`<white>${escapeMM(n)}`)
.lore([mode === "add" ? "<green>Click to add" : "<red>Click to remove"])
.build(),
async (e) => {
if (mode === "add") await addParent(groupName, n);
else await removeParent(groupName, n);
await refreshGroupMembers(groupName);
ack(
e.getWhoClicked() as Player,
mode === "add"
? `<green>Added parent <white>${escapeMM(n)}`
: `<red>Removed parent <white>${escapeMM(n)}`,
);
return openGroupDetail(e.getWhoClicked() as Player, groupName);
},
);
});
gui.slot((rows - 1) * 9, BACK(), (e) =>
openGroupDetail(e.getWhoClicked() as Player, groupName),
);
gui.open(viewer);
});
}