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 { readFile, writeFile, readdir } from 'fs/promises';
import { join } from 'path';
import * as string_decoder from "node:string_decoder";
export interface GameCharacter {
fullName?: string;
playbook?: string;
health: number;
armor?: number;
unstable?: boolean;
@ -16,14 +20,14 @@ export interface GameCharacter {
Tough?: number;
Weird?: number;
color: number;
defaultFacePath: string;
additionalFaces?: GameCharacterFace[];
activeFaceSets?: string[];
moves?: GameCharacterMove[];
improvementsTaken?: string[];
improvementsAvailable?: string[];
improvementsAdvanced?: string[];
availableMoves?: GameCharacterMove[];
improvementsTaken?: TakenImprovement[];
improvementsAvailable?: AvailableImprovement[];
improvementsAdvanced?: AvailableImprovement[];
}
export enum GameAttribute {
@ -47,6 +51,7 @@ export interface GameCharacterMoveBase {
name: string;
summary?: string;
description?: string;
playbook?: string;
}
export interface GameCharacterPassiveMove extends GameCharacterMoveBase {
@ -64,6 +69,56 @@ export interface GameCharacterRollableMove extends GameCharacterMoveBase {
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 interface FaceConditionBase {

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

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

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

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

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

Loading…
Cancel
Save