diff --git a/package-lock.json b/package-lock.json index c8d64541..1f8aa15e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "name": "vk", "dependencies": { "chess.js": "^0.11.0", - "craftjs-plugin": "^0.3.2", + "craftjs-plugin": "^0.3.3", "lodash": "^4.17.21", "yup": "^0.32.9" }, @@ -952,9 +952,9 @@ } }, "node_modules/craftjs-plugin": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/craftjs-plugin/-/craftjs-plugin-0.3.2.tgz", - "integrity": "sha512-YLAcOrDCqpUccyIAigEkos5Em8yIaOWHJAsgNWxgsuX/oLy90voGz+ZLUP+DATfW+hl5L9zOfZzysoMs0AtABQ==" + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/craftjs-plugin/-/craftjs-plugin-0.3.3.tgz", + "integrity": "sha512-vgsflKwscfvwLD9DvaeL30vL4+laZ3TiuFKT1kG9NoQoY5Jknh6p29Suvp2GuEPhbU8NmgzwuqZSXw59o0/pMw==" }, "node_modules/cross-spawn": { "version": "6.0.5", @@ -3422,9 +3422,9 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.6", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.6.tgz", - "integrity": "sha512-AP1+fQIWSM/sMiET8fyayjx/J+JmTPt2Mr0FkrgqB4todtfa53sOsrSAcIrJRD5XS20bKUwaDIuMkWKCEiQLKA==", + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", "dev": true, "engines": { "node": ">=10" diff --git a/src/profession/commands.ts b/src/profession/commands.ts deleted file mode 100644 index 3a651c97..00000000 --- a/src/profession/commands.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { Bukkit } from 'org.bukkit'; -import { CommandSender } from 'org.bukkit.command'; -import { Player } from 'org.bukkit.entity'; -import { errorMessage, successMessage } from '../chat/system'; -import { Nation, nationById } from './nation'; -import { - getProfession, - PlayerProfession, - professionInNation, - professionsByName, - professionsInNation, - removeProfession, - updateProfession, -} from './profession'; - -registerCommand( - 'ammatti', - (sender, _alias, args) => { - // Figure out the nation this command is operating on - // Admins and console can set it; for everyone else, guess it from their profession - let nation: Nation | undefined; - if (args[0] == '--valtio' && args[1]) { - if (sender.hasPermission('vk.profession.admin')) { - nation = nationById(args[1]); - if (!nation) { - errorMessage(sender, `Valtiota ${args[1]} ei ole olemassa`); - return; - } - args = args.slice(2); // Skip these arguments - } else { - errorMessage( - sender, - 'Sinulla ei ole oikeutta ammattien ylläpitokomentoihin.', - ); - return; - } - } else { - nation = guessNation(sender); - } - - if (!args[0]) { - return false; // Print usage - } - - switch (args[0]) { - case 'luo': - createProfession(sender, nation, args[1]); - break; - case 'poista': - deleteProfession(sender, nation, args[1]); - break; - default: - viewOrUpdate(sender, nation, args[1], args.slice(1)); - } - }, - { - usage: (sender) => { - // Different instructions depending on player permissions - if (sender.hasPermission('vk.profession.ruler')) { - sender.sendMessage('/ammatti - luo/poista ammatti'); - sender.sendMessage('/ammatti - katso/muokkaa ammattia'); - sender.sendMessage('/ammatti - pelaajan ammatti'); - } else { - sender.sendMessage('/ammatti - ammatin harjoittajat'); - sender.sendMessage('/ammatti - pelaajan ammatti'); - } - }, - completer: (sender, _alias, args) => { - if (args.length == 1) { - const names = []; - for (const player of Bukkit.onlinePlayers) { - names.push(player.name); - } - if (sender.hasPermission('vk.profession.ruler')) { - names.push('luo'); - names.push('poista'); - } - return names; - } else if (args.length == 2) { - const nation = guessNation(sender); - if (args[0] == 'poista' && nation) { - return professionsInNation(nation).map((prof) => prof.name); - } else if (args[0] != 'luo') { - return ['alaiset', 'kuvaile']; - } - } else if (args.length == 3) { - if (args[1] == 'alaiset') { - return ['lisää', 'poista', 'nollaa']; - } - } - return []; - }, - }, -); - -function guessNation(sender: CommandSender): Nation | undefined { - if (!(sender instanceof Player)) { - return undefined; // Console doesn't have a nation - } - const profession = getProfession(sender); - if (profession?.type != 'player') { - return undefined; // No profession or system profession (no associated nation) - } - return nationById(profession.nation); -} - -function createProfession( - sender: CommandSender, - nation: Nation | undefined, - name: string, -) { - if (!sender.hasPermission('vk.profession.ruler')) { - errorMessage(sender, 'Sinulla ei ole oikeutta luoda ammatteja.'); - return; - } else if (!nation) { - errorMessage(sender, 'Et kuulu valtioon.'); - return; - } else if (professionInNation(nation, name)) { - errorMessage(sender, `Ammatti ${name} on jo olemassa valtiossasi.`); - return; - } - const profession: PlayerProfession = { - type: 'player', - name: name, - description: '', // No description yet - nation: nation.id, - creator: sender.name, - features: [], - subordinates: [], - }; - updateProfession(profession); // Save new profession - - successMessage(sender, `Ammatti ${name} luotu.`); - viewOrUpdate(sender, nation, profession.name, []); // Show ruler overview -} - -function deleteProfession( - sender: CommandSender, - nation: Nation | undefined, - name: string, -) { - if (!sender.hasPermission('vk.profession.ruler')) { - errorMessage(sender, 'Sinulla ei ole oikeutta luoda ammatteja.'); - return; - } else if (!nation) { - errorMessage(sender, 'Et kuulu valtioon.'); - return; - } - const profession = professionInNation(nation, name); - if (!profession) { - errorMessage(sender, `Ammattia ${name} ei ole olemassa valtiossasi.`); - return; - } - removeProfession(profession); - successMessage(sender, `Ammatti ${name} poistettu.`); -} - -function viewOrUpdate( - sender: CommandSender, - nation: Nation | undefined, - name: string, - opts: string[], -) { - const professions = professionsByName(name.toLowerCase()); - if (professions.size == 0) { - // Maybe they meant to get a profession of player? - const player = Bukkit.getOfflinePlayerIfCached(name); - if (player) { - const prof = getProfession(player); - if (prof) { - sender.sendMessage(`${player.name} on ammatiltaan ${prof?.name}.`); - } else { - sender.sendMessage(`${player.name} ei tällä harjoita ammattia.`); - } - } else { - errorMessage(sender, `Pelaajaa tai ammattia ${name} ei löydy.`); - } - return; - } - - // Print list of players with the given profession - if (opts.length == 0) { - // TODO - return; - } - - // All other operations are reserved for rulers and admins - if (!sender.hasPermission('vk.profession.ruler')) { - errorMessage(sender, 'Sinulla ei ole oikeutta hallita ammatteja.'); - return; - } else if (!nation) { - errorMessage(sender, 'Et kuulu valtioon.'); - return; - } - - const profession = professions.get(nation.id); - if (!profession) { - errorMessage(sender, `Ammattia ${name} ei ole olemassa tässä valtiossa.`); - return; - } else if (profession.type != 'player') { - errorMessage( - sender, - 'Tämä ammatti ei ole muokattavissa komennoilla. Ota yhteys ylläpitoon.', - ); - return; - } - switch (opts[0]) { - case 'alaiset': - updateSubordinates(sender, profession, opts.slice(1)); - break; - case 'kuvaile': - profession.description = opts.slice(1).join(' '); - updateProfession(profession); - break; - } -} - -function updateSubordinates( - sender: CommandSender, - profession: PlayerProfession, - opts: string[], -) { - switch (opts[0]) { - case 'lisää': - if (!opts[1]) { - errorMessage(sender, 'Alaiseksi lisättävä ammatti puuttuu.'); - return; - } - // Remove profession from list to prevent diplicates - profession.subordinates = profession.subordinates.filter( - (name) => name != profession.name, - ); - profession.subordinates.push(profession.name); // Add it to end - profession.subordinates.sort(); // Sort alphabetically - break; - case 'poista': - if (!opts[1]) { - errorMessage(sender, 'Alaisista poistettava ammatti puuttuu.'); - return; - } - // Remove profession from list - profession.subordinates = profession.subordinates.filter( - (name) => name != profession.name, - ); - break; - case 'nollaa': - profession.subordinates = []; // Clear subordinates - break; - } - updateProfession(profession); // Save changes -} diff --git a/src/profession/commands/core.ts b/src/profession/commands/core.ts new file mode 100644 index 00000000..ded4074f --- /dev/null +++ b/src/profession/commands/core.ts @@ -0,0 +1,53 @@ +import { CommandSender } from 'org.bukkit.command'; +import { Player } from 'org.bukkit.entity'; +import { errorMessage } from '../../chat/system'; +import { Nation, nationById } from '../nation'; +import { getProfession } from '../profession'; + +/** + * Gets the nation that a command should be operating on. + * For most players, it is the nation of their profession. Admins can + * (and probably have to) explicitly set the nation by --valtio argument. + * @param sender Command sender. + * @param args Initial command arguments. + * @returns Arguments with nation argument removed, the nation or undefined if + * no nation could be determined. + */ +export function getContextNation( + sender: CommandSender, + args: string[], +): [string[], Nation | undefined] { + let nation: Nation | undefined; + if (args[0] == '--valtio' && args[1]) { + if (sender.hasPermission('vk.profession.admin')) { + nation = nationById(args[1]); + if (!nation) { + errorMessage(sender, `Valtiota ${args[1]} ei ole olemassa`); + return [args, undefined]; + } + args = args.slice(2); // Skip these arguments + } else { + // Overriding nation is admin-only feature + errorMessage( + sender, + 'Sinulla ei ole oikeutta ammattien ylläpitokomentoihin.', + ); + return [args, undefined]; + } + } else { + // Nation not provided as argument, try to get it from player + nation = guessNation(sender); + } + return [args, nation]; +} + +export function guessNation(sender: CommandSender): Nation | undefined { + if (!(sender instanceof Player)) { + return undefined; // Console doesn't have a nation + } + const profession = getProfession(sender); + if (profession?.type != 'player') { + return undefined; // No profession or system profession (no associated nation) + } + return nationById(profession.nation); +} diff --git a/src/profession/commands/index.ts b/src/profession/commands/index.ts new file mode 100644 index 00000000..3bedc670 --- /dev/null +++ b/src/profession/commands/index.ts @@ -0,0 +1,130 @@ +import { Bukkit } from 'org.bukkit'; +import { CommandSender } from 'org.bukkit.command'; +import { errorMessage } from '../../chat/system'; +import { getContextNation, guessNation } from './core'; +import { Nation } from '../nation'; +import { + getProfession, + professionsByName, + professionsInNation, +} from '../profession'; +import { createProfession, deleteProfession, manageProfession } from './ruler'; + +registerCommand( + 'ammatti', + (sender, _alias, originalArgs) => { + // Figure out the nation this command is operating on + // Admins and console can set it; for everyone else, guess it from their profession + const [args, nation] = getContextNation(sender, originalArgs); + + if (!args[0]) { + return false; // Print usage + } + + switch (args[0]) { + case 'luo': + createProfession(sender, nation, args[1]); + break; + case 'poista': + deleteProfession(sender, nation, args[1]); + break; + default: + viewOrManage(sender, nation, args[1], args.slice(1)); + } + }, + { + permission: 'vk.profession.player', + usage: (sender) => { + // Different instructions depending on player permissions + if (sender.hasPermission('vk.profession.ruler')) { + sender.sendMessage('/ammatti - luo/poista ammatti'); + sender.sendMessage('/ammatti - katso/muokkaa ammattia'); + sender.sendMessage('/ammatti - pelaajan ammatti'); + } else { + sender.sendMessage('/ammatti - ammatin harjoittajat'); + sender.sendMessage('/ammatti - pelaajan ammatti'); + } + }, + completer: (sender, _alias, args) => { + if (args.length == 1) { + const names = []; + for (const player of Bukkit.onlinePlayers) { + names.push(player.name); + } + if (sender.hasPermission('vk.profession.ruler')) { + names.push('luo'); + names.push('poista'); + } + return names; + } else if (args.length == 2) { + const nation = guessNation(sender); + if (args[0] == 'poista' && nation) { + return professionsInNation(nation).map((prof) => prof.name); + } else if (args[0] != 'luo') { + return ['alaiset', 'kuvaile']; + } + } else if (args.length == 3) { + if (args[1] == 'alaiset') { + return ['lisää', 'poista', 'nollaa']; + } + } + return []; + }, + }, +); + +function viewOrManage( + sender: CommandSender, + nation: Nation | undefined, + name: string, + opts: string[], +) { + const professions = professionsByName(name.toLowerCase()); + if (professions.size == 0) { + // Maybe they meant to get a profession of player? + const player = Bukkit.getOfflinePlayerIfCached(name); + if (player) { + const prof = getProfession(player); + if (prof) { + sender.sendMessage(`${player.name} on ammatiltaan ${prof?.name}.`); + } else { + sender.sendMessage( + `${player.name} ei tällä hetkellä harjoita ammattia.`, + ); + } + } else { + errorMessage(sender, `Pelaajaa tai ammattia ${name} ei löydy.`); + } + return; + } + + // Print list of players with the given profession + if (opts.length == 0) { + // TODO + return; + } + + // All other operations are reserved for rulers and admins + if (!sender.hasPermission('vk.profession.ruler')) { + return errorMessage(sender, 'Sinulla ei ole oikeutta hallita ammatteja.'); + } else if (!nation) { + return errorMessage(sender, 'Et kuulu valtioon.'); + } + + const profession = professions.get(nation.id); + if (!profession) { + return errorMessage( + sender, + `Ammattia ${name} ei ole olemassa valtiossasi.`, + ); + } else if (profession.type != 'player') { + return errorMessage( + sender, + 'Tämä ammatti ei ole muokattavissa komennoilla. Ota yhteys ylläpitoon.', + ); + } + manageProfession(sender, profession, opts); +} + +// Non-ruler management commands +require('./manager'); diff --git a/src/profession/commands/manager.ts b/src/profession/commands/manager.ts new file mode 100644 index 00000000..193bf4df --- /dev/null +++ b/src/profession/commands/manager.ts @@ -0,0 +1,89 @@ +import { Bukkit, Location } from 'org.bukkit'; +import { CommandSender } from 'org.bukkit.command'; +import { Player } from 'org.bukkit.entity'; +import { errorMessage } from '../../chat/system'; +import { + getProfession, + isSubordinateProfession, + Profession, + professionInNation, + systemProfession, +} from '../profession'; +import { getContextNation } from './core'; + +const NOMINATE_DISTANCE = 5; + +registerCommand( + 'nimitä', + (sender, alias, originalArgs) => { + const [args, nation] = getContextNation(sender, originalArgs); + if (args.length < 2) { + return false; // Missing arguments + } + + const player = Bukkit.getPlayer(args[0]); + if (!player) { + return errorMessage(sender, `${args[0]} is ole juuri nyt paikalla.`); + } + + const targetProf = nation + ? professionInNation(nation, args[1]) + : systemProfession(args[1]); + if (!targetProf) { + return errorMessage(sender, `Ammattia ${args[0]} ei löydy valtiostasi.`); + } + + // Some sanity checks for non-admin players + if (!sender.hasPermission('vk.profession.admin')) { + // Sanity check, we need sender to be player for most of the checks + if (!(sender instanceof Player)) { + // ??? + return errorMessage( + sender, + 'Not a player, but missing vk.profession.admin!', + ); + } + + // Check nominator profession (they need one) + const senderProf = getProfession(sender); + if (!senderProf) { + return errorMessage(sender, 'Sinulla ei ole ammattia.'); + } + if (!isSubordinateProfession(senderProf, targetProf)) { + return errorMessage( + sender, + `Sinulla ei ole oikeutta nimittää ammattiin ${targetProf.name}.`, + ); + } + + // Check distance between players + if ( + distanceBetween(sender.location, player.location) > NOMINATE_DISTANCE + ) { + return errorMessage(sender, `${player.name} on liian kaukana sinusta.`); + } + } + + // Send request for profession change + requestProfessionChange(sender, player, targetProf); + }, + { + permission: 'vk.profession.player', + }, +); + +async function requestProfessionChange( + nominator: CommandSender, + target: Player, + profession: Profession, +) { + // TODO +} + +// TODO wait for common/helpers/locations.ts from PR 204 (shops) +function distanceBetween(a: Location, b: Location): number { + if (a.world != b.world) { + return Number.MAX_VALUE; // Different worlds are very far away, indeed + } + return a.distance(b); // Calculate distance normally +} diff --git a/src/profession/commands/ruler.ts b/src/profession/commands/ruler.ts new file mode 100644 index 00000000..ffd164f0 --- /dev/null +++ b/src/profession/commands/ruler.ts @@ -0,0 +1,109 @@ +import { CommandSender } from 'org.bukkit.command'; +import { errorMessage, successMessage } from '../../chat/system'; +import { Nation } from '../nation'; +import { + PlayerProfession, + Profession, + professionInNation, + removeProfession, + updateProfession, +} from '../profession'; + +export function createProfession( + sender: CommandSender, + nation: Nation | undefined, + name: string, +) { + if (!sender.hasPermission('vk.profession.ruler')) { + return errorMessage(sender, 'Sinulla ei ole oikeutta luoda ammatteja.'); + } else if (!nation) { + return errorMessage(sender, 'Et kuulu valtioon.'); + } else if (professionInNation(nation, name)) { + return errorMessage(sender, `Ammatti ${name} on jo olemassa valtiossasi.`); + } + const profession: PlayerProfession = { + type: 'player', + name: name, + description: '', // No description yet + nation: nation.id, + creator: sender.name, + features: [], + subordinates: [], + }; + updateProfession(profession); // Save new profession + + successMessage(sender, `Ammatti ${name} luotu.`); + //viewOrUpdate(sender, nation, profession.name, []); // Show ruler overview +} + +export function deleteProfession( + sender: CommandSender, + nation: Nation | undefined, + name: string, +) { + if (!sender.hasPermission('vk.profession.ruler')) { + return errorMessage(sender, 'Sinulla ei ole oikeutta luoda ammatteja.'); + } else if (!nation) { + return errorMessage(sender, 'Et kuulu valtioon.'); + } + const profession = professionInNation(nation, name); + if (!profession) { + return errorMessage( + sender, + `Ammattia ${name} ei ole olemassa valtiossasi.`, + ); + } + removeProfession(profession); + //successMessage(sender, `Ammatti ${name} poistettu.`); +} + +export function manageProfession( + sender: CommandSender, + profession: PlayerProfession, + opts: string[], +) { + switch (opts[0]) { + case 'alaiset': + updateSubordinates(sender, profession, opts.slice(1)); + break; + case 'kuvaile': + profession.description = opts.slice(1).join(' '); + updateProfession(profession); + break; + } +} + +export function updateSubordinates( + sender: CommandSender, + profession: PlayerProfession, + opts: string[], +) { + // FIXME use profession ids, not names! + switch (opts[0]) { + case 'lisää': + if (!opts[1]) { + return errorMessage(sender, 'Alaiseksi lisättävä ammatti puuttuu.'); + } + // Remove profession from list to prevent diplicates + profession.subordinates = profession.subordinates.filter( + (name) => name != profession.name, + ); + profession.subordinates.push(profession.name); // Add it to end + profession.subordinates.sort(); // Sort alphabetically + break; + case 'poista': + if (!opts[1]) { + errorMessage(sender, 'Alaisista poistettava ammatti puuttuu.'); + return; + } + // Remove profession from list + profession.subordinates = profession.subordinates.filter( + (name) => name != profession.name, + ); + break; + case 'nollaa': + profession.subordinates = []; // Clear subordinates + break; + } + updateProfession(profession); // Save changes +} diff --git a/src/profession/index.ts b/src/profession/index.ts index 378f11ed..e8ec393c 100644 --- a/src/profession/index.ts +++ b/src/profession/index.ts @@ -1,4 +1,4 @@ -require('./commands'); +require('./commands/index'); require('./nation'); require('./permissions'); require('./profession'); diff --git a/src/profession/profession.ts b/src/profession/profession.ts index 7c78a158..320fa935 100644 --- a/src/profession/profession.ts +++ b/src/profession/profession.ts @@ -52,7 +52,7 @@ interface BaseProfession { type: string; /** - * Display name of the profession. + * Display name of the profession. This is always in lower case. */ name: string; @@ -107,9 +107,9 @@ export type Profession = SystemProfession | PlayerProfession; */ export function professionId(profession: Profession) { if (profession.type == 'system') { - return 'system:' + profession.name.toLowerCase(); + return 'system:' + profession.name; } else { - return `${profession.nation}:${profession.name.toLowerCase()}`; + return `${profession.nation}:${profession.name}`; } } @@ -156,14 +156,15 @@ function updateCaches(id: string) { } permissionCache.set(id, perms); - if (profession.type == 'player') { - // Update name -> nation, profession lookup table - const name = profession.name.toLowerCase(); - if (!professionsByNames.has(name)) { - professionsByNames.set(name, new Map()); - } - professionsByNames.get(name)?.set(profession.nation, profession); + // Update name -> nation, profession lookup table + const name = profession.name; + if (!professionsByNames.has(name)) { + professionsByNames.set(name, new Map()); + } + const key = profession.type == 'player' ? profession.nation : 'system'; + professionsByNames.get(name)?.set(key, profession); + if (profession.type == 'player') { // Update nation -> profession list lookup table let nationProfs = professionsByNation.get(profession.nation) ?? []; nationProfs = nationProfs.filter((prof) => prof.name == name); // Clear previous @@ -211,6 +212,10 @@ export function professionInNation( ) as PlayerProfession; } +export function systemProfession(name: string) { + return professionById('system:' + name); +} + export function professionsInNation(nation: Nation): PlayerProfession[] { return professionsByNation.get(nation.id) ?? []; } @@ -338,7 +343,7 @@ export function getAppointTime(player: OfflinePlayer): number { */ export function filterProfessions( callback: (uuid: UUID, name: string) => boolean, -) { +): void { const removeQueue = []; for (const [uuid, name] of playerProfessions) { if (!callback(uuid, name)) { @@ -352,6 +357,26 @@ export function filterProfessions( } } +export function isSubordinateProfession( + leader: Profession, + subordinate: Profession, +): boolean { + if (leader.subordinates.includes(subordinate.name)) { + return true; // Direct subordinate + } else if (leader.subordinates.length == 0) { + return false; // Definitely not subordinate of this + } + + // Not direct subordinate, but could be indirect one + for (const id of leader.subordinates) { + const prof = professionById(id); + if (prof && isSubordinateProfession(prof, subordinate)) { + return true; // Indirect subordinate + } + } + return false; // Not subordinate +} + // Plug in player profession to permission system addPermissionSource((player) => { const profession = getProfession(player);