|
|
@ -22,14 +22,28 @@ import { readFile } from 'fs/promises'; |
|
|
|
import { join } from 'path'; |
|
|
|
import { join } from 'path'; |
|
|
|
import { default as Sharp } from 'sharp'; |
|
|
|
import { default as Sharp } from 'sharp'; |
|
|
|
|
|
|
|
|
|
|
|
export const CharacterOptionTemplate = { |
|
|
|
export enum CharacterOptionCount { |
|
|
|
|
|
|
|
Single = "single", |
|
|
|
|
|
|
|
Multi = "multi", |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const SingleCharacterOptionTemplate = { |
|
|
|
name: 'character', |
|
|
|
name: 'character', |
|
|
|
|
|
|
|
description: 'The character to operate on.', |
|
|
|
|
|
|
|
required: false, |
|
|
|
|
|
|
|
type: CommandOptionType.STRING, |
|
|
|
|
|
|
|
autocomplete: true, |
|
|
|
|
|
|
|
isCharacterOption: CharacterOptionCount.Single, |
|
|
|
|
|
|
|
} as const satisfies ApplicationCommandOptionAutocompletable & { isCharacterOption: CharacterOptionCount.Single }; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const MultiCharacterOptionTemplate = { |
|
|
|
|
|
|
|
name: 'characters', |
|
|
|
description: 'The character(s) to operate on.', |
|
|
|
description: 'The character(s) to operate on.', |
|
|
|
required: false, |
|
|
|
required: false, |
|
|
|
type: CommandOptionType.STRING, |
|
|
|
type: CommandOptionType.STRING, |
|
|
|
autocomplete: true, |
|
|
|
autocomplete: true, |
|
|
|
isCharacterOption: true |
|
|
|
isCharacterOption: CharacterOptionCount.Multi, |
|
|
|
} as const satisfies ApplicationCommandOptionAutocompletable & { isCharacterOption: true }; |
|
|
|
} as const satisfies ApplicationCommandOptionAutocompletable & { isCharacterOption: CharacterOptionCount.Multi }; |
|
|
|
|
|
|
|
|
|
|
|
export interface CharacterDataBase { |
|
|
|
export interface CharacterDataBase { |
|
|
|
readonly success: boolean; |
|
|
|
readonly success: boolean; |
|
|
@ -70,7 +84,7 @@ const NORMAL_STATUS_WIDTH = 1446; |
|
|
|
const enableStackedForTwoCharacters = false; |
|
|
|
const enableStackedForTwoCharacters = false; |
|
|
|
|
|
|
|
|
|
|
|
export abstract class AbstractCharacterStatusCommand extends SlashCommand { |
|
|
|
export abstract class AbstractCharacterStatusCommand extends SlashCommand { |
|
|
|
readonly characterOptions: (ApplicationCommandOption & { isCharacterOption: true })[]; |
|
|
|
readonly characterOptions: (ApplicationCommandOption & { isCharacterOption: CharacterOptionCount })[]; |
|
|
|
readonly dataDir: string; |
|
|
|
readonly dataDir: string; |
|
|
|
|
|
|
|
|
|
|
|
protected constructor(creator: SlashCreator, opts: SlashCommandOptions & CharacterStatusOptions) { |
|
|
|
protected constructor(creator: SlashCreator, opts: SlashCommandOptions & CharacterStatusOptions) { |
|
|
@ -78,8 +92,8 @@ export abstract class AbstractCharacterStatusCommand extends SlashCommand { |
|
|
|
|
|
|
|
|
|
|
|
this.dataDir = opts.dataDir; |
|
|
|
this.dataDir = opts.dataDir; |
|
|
|
this.characterOptions = (opts.options?.filter( |
|
|
|
this.characterOptions = (opts.options?.filter( |
|
|
|
(s) => 'isCharacterOption' in s && s.isCharacterOption === true && s.type === CommandOptionType.STRING |
|
|
|
(s) => 'isCharacterOption' in s && (s.isCharacterOption === CharacterOptionCount.Multi || s.isCharacterOption === CharacterOptionCount.Single) && s.type === CommandOptionType.STRING |
|
|
|
) ?? []) as (ApplicationCommandOption & { isCharacterOption: true })[]; |
|
|
|
) ?? []) as (ApplicationCommandOption & { isCharacterOption: CharacterOptionCount })[]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async autocomplete(ctx: AutocompleteContext): Promise<boolean> { |
|
|
|
async autocomplete(ctx: AutocompleteContext): Promise<boolean> { |
|
|
@ -87,10 +101,11 @@ export abstract class AbstractCharacterStatusCommand extends SlashCommand { |
|
|
|
if (!option) { |
|
|
|
if (!option) { |
|
|
|
return ctx.sendResults([]); |
|
|
|
return ctx.sendResults([]); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
const isMulti = option.isCharacterOption === CharacterOptionCount.Multi |
|
|
|
const party = await loadParty(this.dataDir); |
|
|
|
const party = await loadParty(this.dataDir); |
|
|
|
const defaultCharacter = party.defaultCharacters[ctx.user.id] || null; |
|
|
|
const defaultCharacter = party.defaultCharacters[ctx.user.id] || null; |
|
|
|
const activeParty = new Set(Array.isArray(party.activeParty) ? party.activeParty : []); |
|
|
|
const activeParty = new Set(Array.isArray(party.activeParty) ? party.activeParty : []); |
|
|
|
const completedNames = (ctx.options[option.name] as string).trimStart().split(nameDelimiter); |
|
|
|
const completedNames = isMulti ? (ctx.options[option.name] as string).trimStart().split(nameDelimiter) : [ctx.options[option.name]]; |
|
|
|
const completingName = completedNames.pop()!; |
|
|
|
const completingName = completedNames.pop()!; |
|
|
|
const completingLowercase = completingName.toLowerCase(); |
|
|
|
const completingLowercase = completingName.toLowerCase(); |
|
|
|
const characters = (await listCharacters(this.dataDir)) |
|
|
|
const characters = (await listCharacters(this.dataDir)) |
|
|
@ -139,7 +154,7 @@ export abstract class AbstractCharacterStatusCommand extends SlashCommand { |
|
|
|
const selectedElements = new Set<string>(); |
|
|
|
const selectedElements = new Set<string>(); |
|
|
|
completedNames.forEach((entry) => { |
|
|
|
completedNames.forEach((entry) => { |
|
|
|
const trimmed = entry.trim(); |
|
|
|
const trimmed = entry.trim(); |
|
|
|
if (trimmed === '*') { |
|
|
|
if (trimmed === '*' && isMulti) { |
|
|
|
for (const partyMember of activeParty) { |
|
|
|
for (const partyMember of activeParty) { |
|
|
|
selectedElements.add(partyMember); |
|
|
|
selectedElements.add(partyMember); |
|
|
|
} |
|
|
|
} |
|
|
@ -154,7 +169,7 @@ export abstract class AbstractCharacterStatusCommand extends SlashCommand { |
|
|
|
return ctx.sendResults( |
|
|
|
return ctx.sendResults( |
|
|
|
[ |
|
|
|
[ |
|
|
|
...characters.map((s) => selectionPrefixValue + s).slice(0, 20), |
|
|
|
...characters.map((s) => selectionPrefixValue + s).slice(0, 20), |
|
|
|
...(unselectedParty.size > 0 && activeParty.size > 1 && (partyName.includes(completingName) || completingName.trim() === '*') |
|
|
|
...(isMulti && unselectedParty.size > 0 && activeParty.size > 1 && (partyName.includes(completingName) || completingName.trim() === '*') |
|
|
|
? [selectionPrefixValue + partyName] |
|
|
|
? [selectionPrefixValue + partyName] |
|
|
|
: []) |
|
|
|
: []) |
|
|
|
] |
|
|
|
] |
|
|
@ -168,33 +183,31 @@ export abstract class AbstractCharacterStatusCommand extends SlashCommand { |
|
|
|
partyPromise: Promise<GameParty> |
|
|
|
partyPromise: Promise<GameParty> |
|
|
|
): Promise<[option: string, names: Set<string>][]> { |
|
|
|
): Promise<[option: string, names: Set<string>][]> { |
|
|
|
const [party, allCharacters] = await Promise.all([partyPromise, listCharacters(this.dataDir)]); |
|
|
|
const [party, allCharacters] = await Promise.all([partyPromise, listCharacters(this.dataDir)]); |
|
|
|
|
|
|
|
function correctName(item: string): string { |
|
|
|
|
|
|
|
if (allCharacters.includes(item)) { |
|
|
|
|
|
|
|
return item; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
const found = allCharacters.find((v) => item.toLowerCase() === v.toLowerCase()); |
|
|
|
|
|
|
|
if (found) { |
|
|
|
|
|
|
|
return found; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
return item; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
const activeParty = new Set(Array.isArray(party.activeParty) ? party.activeParty : []); |
|
|
|
const activeParty = new Set(Array.isArray(party.activeParty) ? party.activeParty : []); |
|
|
|
return this.characterOptions.map((o) => |
|
|
|
return this.characterOptions.map((o) => { |
|
|
|
options[o.name] && typeof options[o.name] === 'string' |
|
|
|
if (!options[o.name] || typeof options[o.name] !== 'string') { |
|
|
|
? [ |
|
|
|
return [o.name, new Set<string>()] |
|
|
|
o.name, |
|
|
|
} |
|
|
|
new Set( |
|
|
|
const isMulti = o.isCharacterOption === CharacterOptionCount.Multi |
|
|
|
(options[o.name] as string) |
|
|
|
const trimmed = (options[o.name] as string).trim() |
|
|
|
.trim() |
|
|
|
if (!isMulti) { |
|
|
|
.split(nameDelimiter) |
|
|
|
return [o.name, new Set<string>([correctName(trimmed)])] |
|
|
|
.flatMap((item) => { |
|
|
|
} |
|
|
|
if (item === '*') { |
|
|
|
return [o.name, new Set<string>( |
|
|
|
return Array.from(activeParty); |
|
|
|
trimmed.split(nameDelimiter).flatMap(item => item === '*' ? Array.from(activeParty) : [correctName(item)]))] |
|
|
|
} else if (allCharacters.includes(item)) { |
|
|
|
}); |
|
|
|
return [item]; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
const found = allCharacters.find((v) => item.toLowerCase() === v.toLowerCase()); |
|
|
|
|
|
|
|
if (found) { |
|
|
|
|
|
|
|
return [found]; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
return [item]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
: [o.name, new Set()] |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
abstract process( |
|
|
|
abstract process( |
|
|
|