last upload from radzathan

main
Mari 4 months ago
parent 74f1710722
commit 723a3e6640
  1. 63
      src/character.ts
  2. 73
      src/commands/base.ts
  3. 8
      src/commands/experience.ts
  4. 7
      src/commands/harm.ts
  5. 8
      src/commands/heal.ts
  6. 43
      src/commands/levelup.ts
  7. 8
      src/commands/luck.ts
  8. 4
      src/commands/party.ts
  9. 8
      src/commands/status.ts

@ -1,8 +1,12 @@
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
import { readFile, writeFile, readdir } from 'fs/promises'; import { readFile, writeFile, readdir } from 'fs/promises';
import { join } from 'path'; import { join } from 'path';
import * as string_decoder from "node:string_decoder";
export interface GameCharacter { export interface GameCharacter {
fullName?: string;
playbook?: string;
health: number; health: number;
armor?: number; armor?: number;
unstable?: boolean; unstable?: boolean;
@ -16,14 +20,14 @@ export interface GameCharacter {
Tough?: number; Tough?: number;
Weird?: number; Weird?: number;
color: number;
defaultFacePath: string; defaultFacePath: string;
additionalFaces?: GameCharacterFace[]; additionalFaces?: GameCharacterFace[];
activeFaceSets?: string[]; activeFaceSets?: string[];
moves?: GameCharacterMove[]; moves?: GameCharacterMove[];
improvementsTaken?: string[]; availableMoves?: GameCharacterMove[];
improvementsAvailable?: string[]; improvementsTaken?: TakenImprovement[];
improvementsAdvanced?: string[]; improvementsAvailable?: AvailableImprovement[];
improvementsAdvanced?: AvailableImprovement[];
} }
export enum GameAttribute { export enum GameAttribute {
@ -47,6 +51,7 @@ export interface GameCharacterMoveBase {
name: string; name: string;
summary?: string; summary?: string;
description?: string; description?: string;
playbook?: string;
} }
export interface GameCharacterPassiveMove extends GameCharacterMoveBase { export interface GameCharacterPassiveMove extends GameCharacterMoveBase {
@ -64,6 +69,56 @@ export interface GameCharacterRollableMove extends GameCharacterMoveBase {
onMiss?: string; onMiss?: string;
} }
export interface ImprovementBase {
type: string;
}
export interface AvailableAttributeBonusImprovement extends ImprovementBase {
type: GameAttribute|'attribute';
max: number;
}
export interface TakenAttributeBonusImprovement extends AvailableAttributeBonusImprovement {
type: GameAttribute;
from: number;
to: number;
}
export interface AvailableMoveImprovement extends ImprovementBase {
type: 'playbookMove'|'otherMove';
}
export interface TakenOtherPlaybookMoveImprovement extends AvailableMoveImprovement {
type: 'otherMove';
playbook: string;
name: string;
}
export interface TakenSamePlaybookMoveImprovement extends AvailableMoveImprovement {
type: 'playbookMove';
name: string;
}
export interface AvailableAdvanceBasicMoveImprovement extends ImprovementBase {
type: 'advance';
}
export interface TakenAdvanceBasicMoveImprovement extends AvailableAdvanceBasicMoveImprovement {
names: string[]
}
export interface LuckImprovement extends ImprovementBase {
type: 'luck'
}
export interface OtherImprovement extends ImprovementBase {
type: 'other';
text: string;
}
export type AvailableImprovement = AvailableMoveImprovement|AvailableAdvanceBasicMoveImprovement|AvailableAttributeBonusImprovement|LuckImprovement|OtherImprovement
export type TakenImprovement = TakenSamePlaybookMoveImprovement|TakenOtherPlaybookMoveImprovement|TakenAdvanceBasicMoveImprovement|TakenAttributeBonusImprovement|LuckImprovement|OtherImprovement
export type GameCharacterMove = GameCharacterPassiveMove | GameCharacterRollableMove; export type GameCharacterMove = GameCharacterPassiveMove | GameCharacterRollableMove;
export interface FaceConditionBase { export interface FaceConditionBase {

@ -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)]);
const activeParty = new Set(Array.isArray(party.activeParty) ? party.activeParty : []); function correctName(item: string): string {
return this.characterOptions.map((o) => if (allCharacters.includes(item)) {
options[o.name] && typeof options[o.name] === 'string' return item;
? [
o.name,
new Set(
(options[o.name] as string)
.trim()
.split(nameDelimiter)
.flatMap((item) => {
if (item === '*') {
return Array.from(activeParty);
} else if (allCharacters.includes(item)) {
return [item];
} else { } else {
const found = allCharacters.find((v) => item.toLowerCase() === v.toLowerCase()); const found = allCharacters.find((v) => item.toLowerCase() === v.toLowerCase());
if (found) { if (found) {
return [found]; return found;
} else { } else {
return [item]; return item;
} }
} }
}) }
) const activeParty = new Set(Array.isArray(party.activeParty) ? party.activeParty : []);
] return this.characterOptions.map((o) => {
: [o.name, new Set()] if (!options[o.name] || typeof options[o.name] !== 'string') {
); return [o.name, new Set<string>()]
}
const isMulti = o.isCharacterOption === CharacterOptionCount.Multi
const trimmed = (options[o.name] as string).trim()
if (!isMulti) {
return [o.name, new Set<string>([correctName(trimmed)])]
}
return [o.name, new Set<string>(
trimmed.split(nameDelimiter).flatMap(item => item === '*' ? Array.from(activeParty) : [correctName(item)]))]
});
} }
abstract process( abstract process(

@ -1,7 +1,7 @@
import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create'; import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create';
import { import {
AbstractCharacterStatusCommand, AbstractCharacterStatusCommand,
CharacterOptionTemplate, MultiCharacterOptionTemplate,
type CharacterStatusOptions, type CharacterStatusOptions,
type GameCharacterData, type GameCharacterData,
type LoadedCharacterData type LoadedCharacterData
@ -17,8 +17,8 @@ export class ExperienceCharacterCommand extends AbstractCharacterStatusCommand {
guildIDs: process.env.DEVELOPMENT_GUILD_ID, guildIDs: process.env.DEVELOPMENT_GUILD_ID,
options: [ options: [
{ {
...CharacterOptionTemplate, ...MultiCharacterOptionTemplate,
name: 'character', name: 'characters',
description: 'The name of the character(s) to grant EXP to or remove EXP from.', description: 'The name of the character(s) to grant EXP to or remove EXP from.',
required: true required: true
}, },
@ -47,7 +47,7 @@ export class ExperienceCharacterCommand extends AbstractCharacterStatusCommand {
const delta = ctx.options['delta'] ?? 1; const delta = ctx.options['delta'] ?? 1;
const description: string[] = []; const description: string[] = [];
const result: LoadedCharacterData[] = []; const result: LoadedCharacterData[] = [];
for (const character of characters.get('character')!) { for (const character of characters.get('characters')!) {
if (!character.success) { if (!character.success) {
description.push( description.push(
`**${character.name}** ${delta >= 0 ? 'gained' : 'lost'} ${Math.abs(delta)} EXP${delta >= 0 ? '!' : '.'}` `**${character.name}** ${delta >= 0 ? 'gained' : 'lost'} ${Math.abs(delta)} EXP${delta >= 0 ? '!' : '.'}`

@ -1,7 +1,7 @@
import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create'; import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create';
import { import {
AbstractCharacterStatusCommand, AbstractCharacterStatusCommand,
CharacterOptionTemplate, MultiCharacterOptionTemplate,
type CharacterStatusOptions, type CharacterStatusOptions,
type GameCharacterData, type GameCharacterData,
type LoadedCharacterData type LoadedCharacterData
@ -17,7 +17,8 @@ export class HarmCharacterCommand extends AbstractCharacterStatusCommand {
guildIDs: process.env.DEVELOPMENT_GUILD_ID, guildIDs: process.env.DEVELOPMENT_GUILD_ID,
options: [ options: [
{ {
...CharacterOptionTemplate, ...MultiCharacterOptionTemplate,
name: 'characters',
description: 'The name of the character(s) to harm.', description: 'The name of the character(s) to harm.',
required: true required: true
}, },
@ -59,7 +60,7 @@ export class HarmCharacterCommand extends AbstractCharacterStatusCommand {
const makeUnstable: boolean | null = ctx.options['destabilize'] ?? null; const makeUnstable: boolean | null = ctx.options['destabilize'] ?? null;
const description: string[] = []; const description: string[] = [];
const result: LoadedCharacterData[] = []; const result: LoadedCharacterData[] = [];
for (const character of characters.get('character')!) { for (const character of characters.get('characters')!) {
if (!character.success) { if (!character.success) {
description.push( description.push(
`**${character.name}** took ${baseDamage} Harm${piercing ? ' ignore-armour' : ''}${ `**${character.name}** took ${baseDamage} Harm${piercing ? ' ignore-armour' : ''}${

@ -1,7 +1,7 @@
import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create'; import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create';
import { import {
AbstractCharacterStatusCommand, AbstractCharacterStatusCommand,
CharacterOptionTemplate, MultiCharacterOptionTemplate,
type CharacterStatusOptions, type CharacterStatusOptions,
type GameCharacterData, type GameCharacterData,
type LoadedCharacterData type LoadedCharacterData
@ -17,8 +17,8 @@ export class HealCharacterCommand extends AbstractCharacterStatusCommand {
guildIDs: process.env.DEVELOPMENT_GUILD_ID, guildIDs: process.env.DEVELOPMENT_GUILD_ID,
options: [ options: [
{ {
...CharacterOptionTemplate, ...MultiCharacterOptionTemplate,
name: 'character', name: 'characters',
description: 'The name of the character(s) to heal.', description: 'The name of the character(s) to heal.',
required: true required: true
}, },
@ -53,7 +53,7 @@ export class HealCharacterCommand extends AbstractCharacterStatusCommand {
const stabilize: boolean = ctx.options['stabilize'] ?? false; const stabilize: boolean = ctx.options['stabilize'] ?? false;
const description: string[] = []; const description: string[] = [];
const result: LoadedCharacterData[] = []; const result: LoadedCharacterData[] = [];
for (const character of characters.get('character')!) { for (const character of characters.get('characters')!) {
if (!character.success) { if (!character.success) {
description.push( description.push(
`**${character.name}** healed ${healing} Harm${healing > 0 ? '!' : '.'}${ `**${character.name}** healed ${healing} Harm${healing > 0 ? '!' : '.'}${

@ -0,0 +1,43 @@
import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create';
import {
AbstractCharacterStatusCommand,
type CharacterStatusOptions,
type GameCharacterData,
type LoadedCharacterData, SingleCharacterOptionTemplate
} from './base.js';
export class LevelUpCharacterCommand extends AbstractCharacterStatusCommand {
constructor(creator: SlashCreator, opts: CharacterStatusOptions) {
super(creator, {
...opts,
name: 'levelup',
description: 'Levels up the given character.',
type: ApplicationCommandType.CHAT_INPUT,
guildIDs: process.env.DEVELOPMENT_GUILD_ID,
options: [
{
...SingleCharacterOptionTemplate,
name: 'character',
description: 'The name of the character to level up. This character must exist.',
required: true
},
]
});
}
async process(
ctx: CommandContext,
{ characters }: { characters: Map<string, readonly GameCharacterData[]> }
): Promise<
| readonly [string, LoadedCharacterData[]]
| readonly [string, LoadedCharacterData]
| readonly LoadedCharacterData[]
| LoadedCharacterData
| string
> {
const characterList = characters.get("character")
if (!characterList || characterList.length === 0) {
return
}
}
}

@ -1,7 +1,7 @@
import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create'; import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create';
import { import {
AbstractCharacterStatusCommand, AbstractCharacterStatusCommand,
CharacterOptionTemplate, MultiCharacterOptionTemplate,
type CharacterStatusOptions, type CharacterStatusOptions,
type GameCharacterData, type GameCharacterData,
type LoadedCharacterData type LoadedCharacterData
@ -17,8 +17,8 @@ export class LuckCharacterCommand extends AbstractCharacterStatusCommand {
guildIDs: process.env.DEVELOPMENT_GUILD_ID, guildIDs: process.env.DEVELOPMENT_GUILD_ID,
options: [ options: [
{ {
...CharacterOptionTemplate, ...MultiCharacterOptionTemplate,
name: 'character', name: 'characters',
description: 'The name of the character(s) to grant luck to or remove luck from.', description: 'The name of the character(s) to grant luck to or remove luck from.',
required: true required: true
}, },
@ -47,7 +47,7 @@ export class LuckCharacterCommand extends AbstractCharacterStatusCommand {
const delta: number = ctx.options['delta'] ?? -1; const delta: number = ctx.options['delta'] ?? -1;
const description: string[] = []; const description: string[] = [];
const result: LoadedCharacterData[] = []; const result: LoadedCharacterData[] = [];
for (const character of characters.get('character')!) { for (const character of characters.get('characters')!) {
if (!character.success) { if (!character.success) {
description.push( description.push(
`**${character.name}** ${delta > 0 ? 'recovered' : 'spent'} ${Math.abs(delta)} Luck${delta !== 0 ? '!' : '.'}` `**${character.name}** ${delta > 0 ? 'recovered' : 'spent'} ${Math.abs(delta)} Luck${delta !== 0 ? '!' : '.'}`

@ -1,6 +1,6 @@
import { import {
AbstractCharacterStatusCommand, AbstractCharacterStatusCommand,
CharacterOptionTemplate, MultiCharacterOptionTemplate,
type CharacterStatusOptions, type CharacterStatusOptions,
type GameCharacterData, type GameCharacterData,
type LoadedCharacterData type LoadedCharacterData
@ -18,7 +18,7 @@ export class PartyCommand extends AbstractCharacterStatusCommand {
guildIDs: process.env.DEVELOPMENT_GUILD_ID, guildIDs: process.env.DEVELOPMENT_GUILD_ID,
options: [ options: [
{ {
...CharacterOptionTemplate, ...MultiCharacterOptionTemplate,
name: 'characters', name: 'characters',
description: 'The name of the character(s) to become the current party. If not set, shows the party instead.', description: 'The name of the character(s) to become the current party. If not set, shows the party instead.',
required: false required: false

@ -1,6 +1,6 @@
import { ApplicationCommandType, type CommandContext, type SlashCreator } from 'slash-create'; import { ApplicationCommandType, type CommandContext, type SlashCreator } from 'slash-create';
import { import {
CharacterOptionTemplate, MultiCharacterOptionTemplate,
AbstractCharacterStatusCommand, AbstractCharacterStatusCommand,
type GameCharacterData, type GameCharacterData,
type LoadedCharacterData, type LoadedCharacterData,
@ -17,8 +17,8 @@ export class CharacterStatusCommand extends AbstractCharacterStatusCommand {
guildIDs: process.env.DEVELOPMENT_GUILD_ID, guildIDs: process.env.DEVELOPMENT_GUILD_ID,
options: [ options: [
{ {
...CharacterOptionTemplate, ...MultiCharacterOptionTemplate,
name: 'character', name: 'characters',
description: 'The name of the character(s) to get the status of.', description: 'The name of the character(s) to get the status of.',
required: true required: true
} }
@ -36,6 +36,6 @@ export class CharacterStatusCommand extends AbstractCharacterStatusCommand {
| LoadedCharacterData | LoadedCharacterData
| string | string
> { > {
return characters.get('character')!.flatMap((x) => (x.success ? [x] : [])); return characters.get('characters')!.flatMap((x) => (x.success ? [x] : []));
} }
} }

Loading…
Cancel
Save