parent
2a73d5fa24
commit
25a9be8edf
@ -0,0 +1,8 @@ |
||||
/data |
||||
.dockerignore |
||||
node_modules |
||||
npm-debug.log |
||||
Dockerfile |
||||
.git |
||||
.gitignore |
||||
.npmrc |
@ -0,0 +1,19 @@ |
||||
FROM node:latest AS build |
||||
RUN apt-get update && apt-get install -y --no-install-recommends dumb-init |
||||
WORKDIR /usr/src/app |
||||
COPY src /usr/src/app/ |
||||
COPY package*.json /usr/src/app/ |
||||
RUN npm ci |
||||
RUN npm run lint |
||||
RUN npm run build |
||||
RUN npm ci --production |
||||
|
||||
FROM node:16.17.0-bullseye-slim |
||||
|
||||
ENV NODE_ENV production |
||||
COPY --from=install /usr/bin/dumb-init /usr/bin/dumb-init |
||||
USER node |
||||
WORKDIR /usr/src/app |
||||
COPY --chown=node:node --from=build /usr/src/app/node_modules /usr/src/app/node_modules |
||||
COPY --chown=node:node --from=build /usr/src/app/dist /usr/src/app/dist |
||||
CMD ["dumb-init", "node", "/usr/src/app/dist/index.js"] |
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 95 KiB |
File diff suppressed because it is too large
Load Diff
@ -1,125 +1,136 @@ |
||||
import {parse as parseYaml, stringify as stringifyYaml} from "yaml" |
||||
import {readFile, writeFile, readdir} from 'fs/promises' |
||||
import {join} from 'path' |
||||
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; |
||||
import { readFile, writeFile, readdir } from 'fs/promises'; |
||||
import { join } from 'path'; |
||||
|
||||
export interface GameCharacter { |
||||
health: number |
||||
armor?: number |
||||
unstable?: boolean |
||||
luck: number |
||||
luckSpecial?: string |
||||
experience: number |
||||
|
||||
Charm?: number |
||||
Cool?: number |
||||
Sharp?: number |
||||
Tough?: number |
||||
Weird?: number |
||||
|
||||
color: number |
||||
defaultFacePath: string |
||||
additionalFaces?: GameCharacterFace[] |
||||
activeFaceSets?: string[] |
||||
moves?: GameCharacterMove[] |
||||
improvementsTaken?: string[] |
||||
improvementsAvailable?: string[] |
||||
improvementsAdvanced?: string[] |
||||
health: number; |
||||
armor?: number; |
||||
unstable?: boolean; |
||||
luck: number; |
||||
luckSpecial?: string; |
||||
experience: number; |
||||
|
||||
Charm?: number; |
||||
Cool?: number; |
||||
Sharp?: number; |
||||
Tough?: number; |
||||
Weird?: number; |
||||
|
||||
color: number; |
||||
defaultFacePath: string; |
||||
additionalFaces?: GameCharacterFace[]; |
||||
activeFaceSets?: string[]; |
||||
moves?: GameCharacterMove[]; |
||||
improvementsTaken?: string[]; |
||||
improvementsAvailable?: string[]; |
||||
improvementsAdvanced?: string[]; |
||||
} |
||||
|
||||
export enum GameAttribute { |
||||
Charm = "Charm", |
||||
Cool = "Cool", |
||||
Sharp = "Sharp", |
||||
Tough = "Tough", |
||||
Weird = "Weird", |
||||
Charm = 'Charm', |
||||
Cool = 'Cool', |
||||
Sharp = 'Sharp', |
||||
Tough = 'Tough', |
||||
Weird = 'Weird' |
||||
} |
||||
|
||||
export const GameAttributes = [GameAttribute.Charm, GameAttribute.Cool, GameAttribute.Sharp, GameAttribute.Tough, GameAttribute.Weird] as const |
||||
export const GameAttributes = [ |
||||
GameAttribute.Charm, |
||||
GameAttribute.Cool, |
||||
GameAttribute.Sharp, |
||||
GameAttribute.Tough, |
||||
GameAttribute.Weird |
||||
] as const; |
||||
|
||||
export interface GameCharacterMoveBase { |
||||
type?: string |
||||
name: string |
||||
summary?: string |
||||
description?: string |
||||
type?: string; |
||||
name: string; |
||||
summary?: string; |
||||
description?: string; |
||||
} |
||||
|
||||
export interface GameCharacterPassiveMove extends GameCharacterMoveBase { |
||||
type?: "passive" |
||||
type?: 'passive'; |
||||
} |
||||
|
||||
export interface GameCharacterRollableMove extends GameCharacterMoveBase { |
||||
type: "rollable" |
||||
attribute?: GameAttribute |
||||
bonus?: number |
||||
advanced?: boolean |
||||
onAdvanced?: string |
||||
onSuccess?: string |
||||
onMixed?: string |
||||
onMiss?: string |
||||
type: 'rollable'; |
||||
attribute?: GameAttribute; |
||||
bonus?: number; |
||||
advanced?: boolean; |
||||
onAdvanced?: string; |
||||
onSuccess?: string; |
||||
onMixed?: string; |
||||
onMiss?: string; |
||||
} |
||||
|
||||
export type GameCharacterMove = GameCharacterPassiveMove|GameCharacterRollableMove |
||||
export type GameCharacterMove = GameCharacterPassiveMove | GameCharacterRollableMove; |
||||
|
||||
export interface FaceConditionBase { |
||||
type: string |
||||
negated?: boolean |
||||
type: string; |
||||
negated?: boolean; |
||||
} |
||||
|
||||
export interface FaceConditionStability extends FaceConditionBase { |
||||
type: "stable"|"unstable"|"dead" |
||||
type: 'stable' | 'unstable' | 'dead'; |
||||
} |
||||
|
||||
export interface FaceConditionHealth extends FaceConditionBase { |
||||
type: "hpEq"|"hpGt"|"hpLt"|"hpGtEq"|"hpLtEq" |
||||
threshold: number |
||||
type: 'hpEq' | 'hpGt' | 'hpLt' | 'hpGtEq' | 'hpLtEq'; |
||||
threshold: number; |
||||
} |
||||
|
||||
export interface FaceConditionHealthDelta extends FaceConditionBase { |
||||
type: "beingHealed"|"beingDamaged"|"healthSteady" |
||||
type: 'beingHealed' | 'beingDamaged' | 'healthSteady'; |
||||
} |
||||
|
||||
export interface FaceConditionSet extends FaceConditionBase { |
||||
type: "faceSetActive" |
||||
set: string |
||||
type: 'faceSetActive'; |
||||
set: string; |
||||
} |
||||
|
||||
export type FaceCondition = FaceConditionStability|FaceConditionHealth|FaceConditionHealthDelta|FaceConditionSet |
||||
export type FaceCondition = FaceConditionStability | FaceConditionHealth | FaceConditionHealthDelta | FaceConditionSet; |
||||
|
||||
export interface GameCharacterFace { |
||||
path: string |
||||
conditions: FaceCondition[] |
||||
path: string; |
||||
conditions: FaceCondition[]; |
||||
} |
||||
|
||||
export const FaceSetIdentifier = /^[a-z0-9_]+$/ |
||||
export const FaceSetIdentifier = /^[a-z0-9_]+$/; |
||||
|
||||
export async function listCharacters(dataDir: string): Promise<string[]> { |
||||
const list = await readdir(join(dataDir, "characters")) |
||||
return list.filter(s => s.endsWith(".yaml")).map(s => s.substring(0, s.length - 5)) |
||||
const list = await readdir(join(dataDir, 'characters')); |
||||
return list.filter((s) => s.endsWith('.yaml')).map((s) => s.substring(0, s.length - 5)); |
||||
} |
||||
|
||||
export async function loadCharacter(dataDir: string, name: string): Promise<GameCharacter> { |
||||
const contents = await readFile(join(dataDir, "characters", name + ".yaml"), {encoding: "utf-8"}) |
||||
return parseYaml(contents) |
||||
const contents = await readFile(join(dataDir, 'characters', name + '.yaml'), { encoding: 'utf-8' }); |
||||
return parseYaml(contents); |
||||
} |
||||
|
||||
export async function saveCharacter(dataDir: string, name: string, character: GameCharacter): Promise<void> { |
||||
const contents = stringifyYaml(character) |
||||
return writeFile(join(dataDir, "characters", name + ".yaml"), contents) |
||||
const contents = stringifyYaml(character); |
||||
return writeFile(join(dataDir, 'characters', name + '.yaml'), contents, { encoding: 'utf-8' }); |
||||
} |
||||
|
||||
export interface GameParty { |
||||
defaultCharacters: Record<string, string> |
||||
activeParty: string[] |
||||
keeper: string |
||||
defaultCharacters: Record<string, string>; |
||||
activeParty: string[]; |
||||
keeper: string; |
||||
} |
||||
|
||||
export interface ReadonlyGameParty { |
||||
readonly defaultCharacters: Readonly<Record<string, string>> |
||||
readonly activeParty: readonly string[] |
||||
readonly keeper: string |
||||
readonly defaultCharacters: Readonly<Record<string, string>>; |
||||
readonly activeParty: readonly string[]; |
||||
readonly keeper: string; |
||||
} |
||||
|
||||
export async function loadParty(dataDir: string): Promise<GameParty> { |
||||
const contents = await readFile(join(dataDir, "party.yaml"), {encoding: "utf-8"}) |
||||
return parseYaml(contents) |
||||
const contents = await readFile(join(dataDir, 'party.yaml'), { encoding: 'utf-8' }); |
||||
return parseYaml(contents); |
||||
} |
||||
|
||||
export async function saveParty(dataDir: string, party: GameParty): Promise<void> { |
||||
const contents = stringifyYaml(party); |
||||
return writeFile(join(dataDir, 'party.yaml'), contents, { encoding: 'utf-8' }); |
||||
} |
||||
|
@ -1,65 +1,72 @@ |
||||
import { |
||||
ApplicationCommandType, |
||||
AutocompleteContext, type CommandContext, |
||||
CommandOptionType, Message, |
||||
SlashCommand, |
||||
type SlashCreator |
||||
} from "slash-create"; |
||||
import {type GameCharacter, listCharacters, loadCharacter, saveCharacter} from "../character.js"; |
||||
import {renderStatus} from "../renderStatus.js"; |
||||
import {readFile} from "fs/promises"; |
||||
import {join} from "path"; |
||||
import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create'; |
||||
import { |
||||
AbstractCharacterStatusCommand, |
||||
CharacterOptionTemplate, |
||||
type CharacterStatusOptions, |
||||
type GameCharacterData, |
||||
type LoadedCharacterData |
||||
} from "./base.js"; |
||||
} from './base.js'; |
||||
|
||||
export class ExperienceCharacterCommand extends AbstractCharacterStatusCommand { |
||||
constructor(creator: SlashCreator) { |
||||
constructor(creator: SlashCreator, opts: CharacterStatusOptions) { |
||||
super(creator, { |
||||
name: "experience", |
||||
description: "Modifies the EXP total of the given character(s).", |
||||
...opts, |
||||
name: 'experience', |
||||
description: 'Modifies the EXP total of the given character(s).', |
||||
type: ApplicationCommandType.CHAT_INPUT, |
||||
guildIDs: process.env.DEVELOPMENT_GUILD_ID, |
||||
options: [ |
||||
{ |
||||
...CharacterOptionTemplate, |
||||
name: "character", |
||||
description: "The name of the character(s) to grant EXP to or remove EXP from.", |
||||
required: true, |
||||
name: 'character', |
||||
description: 'The name of the character(s) to grant EXP to or remove EXP from.', |
||||
required: true |
||||
}, |
||||
{ |
||||
type: CommandOptionType.INTEGER, |
||||
name: "delta", |
||||
description: "The amount of EXP to apply to the character(s) (default +1).", |
||||
name: 'delta', |
||||
description: 'The amount of EXP to apply to the character(s) (default +1).', |
||||
max_value: 25, |
||||
min_value: -25, |
||||
required: false, |
||||
}, |
||||
required: false |
||||
} |
||||
] |
||||
}); |
||||
} |
||||
|
||||
async process(ctx: CommandContext, characters: Map<string, readonly GameCharacterData[]>): Promise<readonly [string, LoadedCharacterData[]] | readonly [string, LoadedCharacterData] | readonly LoadedCharacterData[] | LoadedCharacterData | string> { |
||||
const delta = ctx.options["delta"] ?? 1 |
||||
const description: string[] = [] |
||||
const result: LoadedCharacterData[] = [] |
||||
for (const character of characters.get("character")!) { |
||||
async process( |
||||
ctx: CommandContext, |
||||
{ characters }: { characters: Map<string, readonly GameCharacterData[]> } |
||||
): Promise< |
||||
| readonly [string, LoadedCharacterData[]] |
||||
| readonly [string, LoadedCharacterData] |
||||
| readonly LoadedCharacterData[] |
||||
| LoadedCharacterData |
||||
| string |
||||
> { |
||||
const delta = ctx.options['delta'] ?? 1; |
||||
const description: string[] = []; |
||||
const result: LoadedCharacterData[] = []; |
||||
for (const character of characters.get('character')!) { |
||||
if (!character.success) { |
||||
description.push(`**${character.name}** ${delta >= 0 ? "gained" : "lost"} ${Math.abs(delta)} EXP${delta >= 0 ? "!" : "."}`) |
||||
continue |
||||
description.push( |
||||
`**${character.name}** ${delta >= 0 ? 'gained' : 'lost'} ${Math.abs(delta)} EXP${delta >= 0 ? '!' : '.'}` |
||||
); |
||||
continue; |
||||
} |
||||
character.newData = { |
||||
...character.newData ?? character.originalData |
||||
} |
||||
const oldLevels = Math.floor(character.newData.experience / 5) |
||||
character.newData.experience = Math.max(0, (character.newData.experience ?? 0) + delta) |
||||
const levels = Math.floor(character.newData.experience / 5) |
||||
result.push(character) |
||||
description.push(`**${character.name}** ${delta >= 0 ? "gained" : "lost"} ${Math.abs(delta)} EXP${delta >= 0 ? "!" : "."}${levels > oldLevels ? " ***Level up!***" : ""}`) |
||||
...(character.newData ?? character.originalData) |
||||
}; |
||||
const oldLevels = Math.floor(character.newData.experience / 5); |
||||
character.newData.experience = Math.max(0, (character.newData.experience ?? 0) + delta); |
||||
const levels = Math.floor(character.newData.experience / 5); |
||||
result.push(character); |
||||
description.push( |
||||
`**${character.name}** ${delta >= 0 ? 'gained' : 'lost'} ${Math.abs(delta)} EXP${delta >= 0 ? '!' : '.'}${ |
||||
levels > oldLevels ? ' ***Level up!***' : '' |
||||
}` |
||||
); |
||||
} |
||||
return [description.join("\n"), result] |
||||
return [description.join('\n'), result]; |
||||
} |
||||
} |
||||
|
@ -1,88 +1,95 @@ |
||||
import { |
||||
ApplicationCommandType, |
||||
AutocompleteContext, |
||||
type CommandContext, |
||||
CommandOptionType, |
||||
Message, |
||||
SlashCommand, |
||||
type SlashCreator |
||||
} from "slash-create"; |
||||
import {type GameCharacter, listCharacters, loadCharacter, saveCharacter} from "../character.js"; |
||||
import {renderStatus} from "../renderStatus.js"; |
||||
import {readFile} from "fs/promises"; |
||||
import {join} from "path"; |
||||
import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create'; |
||||
import { |
||||
AbstractCharacterStatusCommand, |
||||
CharacterOptionTemplate, |
||||
type CharacterStatusOptions, |
||||
type GameCharacterData, |
||||
type LoadedCharacterData |
||||
} from "./base.js"; |
||||
} from './base.js'; |
||||
|
||||
export class HarmCharacterCommand extends AbstractCharacterStatusCommand { |
||||
constructor(creator: SlashCreator) { |
||||
constructor(creator: SlashCreator, opts: CharacterStatusOptions) { |
||||
super(creator, { |
||||
name: "harm", |
||||
description: "Harms the given character(s).", |
||||
...opts, |
||||
name: 'harm', |
||||
description: 'Harms the given character(s).', |
||||
type: ApplicationCommandType.CHAT_INPUT, |
||||
guildIDs: process.env.DEVELOPMENT_GUILD_ID, |
||||
options: [ |
||||
{ |
||||
...CharacterOptionTemplate, |
||||
description: "The name of the character(s) to harm.", |
||||
required: true, |
||||
description: 'The name of the character(s) to harm.', |
||||
required: true |
||||
}, |
||||
{ |
||||
type: CommandOptionType.INTEGER, |
||||
name: "damage", |
||||
description: "The amount of harm to deal to the character(s).", |
||||
name: 'damage', |
||||
description: 'The amount of harm to deal to the character(s).', |
||||
max_value: 99, |
||||
min_value: 0, |
||||
required: true, |
||||
required: true |
||||
}, |
||||
{ |
||||
type: CommandOptionType.BOOLEAN, |
||||
name: "piercing", |
||||
description: "If set, ignores any armor the character(s) may have.", |
||||
name: 'piercing', |
||||
description: 'If set, ignores any armor the character(s) may have.' |
||||
}, |
||||
{ |
||||
type: CommandOptionType.BOOLEAN, |
||||
name: "destabilize", |
||||
description: "True to force unstable, False to never set unstable. Default based on remaining health.", |
||||
required: false, |
||||
name: 'destabilize', |
||||
description: 'True to force unstable, False to never set unstable. Default based on remaining health.', |
||||
required: false |
||||
} |
||||
] |
||||
}); |
||||
} |
||||
|
||||
async process(ctx: CommandContext, characters: Map<string, readonly GameCharacterData[]>): Promise<readonly [string, LoadedCharacterData[]] | readonly [string, LoadedCharacterData] | readonly LoadedCharacterData[] | LoadedCharacterData | string> { |
||||
const baseDamage: number = ctx.options["damage"] |
||||
const piercing: boolean = ctx.options["piercing"] ?? false |
||||
const makeUnstable: boolean|null = ctx.options["destabilize"] ?? null |
||||
const description: string[] = [] |
||||
const result: LoadedCharacterData[] = [] |
||||
for (const character of characters.get("character")!) { |
||||
async process( |
||||
ctx: CommandContext, |
||||
{ characters }: { characters: Map<string, readonly GameCharacterData[]> } |
||||
): Promise< |
||||
| readonly [string, LoadedCharacterData[]] |
||||
| readonly [string, LoadedCharacterData] |
||||
| readonly LoadedCharacterData[] |
||||
| LoadedCharacterData |
||||
| string |
||||
> { |
||||
const baseDamage: number = ctx.options['damage']; |
||||
const piercing: boolean = ctx.options['piercing'] ?? false; |
||||
const makeUnstable: boolean | null = ctx.options['destabilize'] ?? null; |
||||
const description: string[] = []; |
||||
const result: LoadedCharacterData[] = []; |
||||
for (const character of characters.get('character')!) { |
||||
if (!character.success) { |
||||
description.push(`**${character.name}** took ${baseDamage} Harm${piercing ? " ignore-armour" : ""}${baseDamage > 0 ? "!" : "."}${makeUnstable ? " ***Unstable!***" : ""}`) |
||||
continue |
||||
description.push( |
||||
`**${character.name}** took ${baseDamage} Harm${piercing ? ' ignore-armour' : ''}${ |
||||
baseDamage > 0 ? '!' : '.' |
||||
}${makeUnstable ? ' ***Unstable!***' : ''}` |
||||
); |
||||
continue; |
||||
} |
||||
character.newData = { |
||||
...(character.newData ?? character.originalData) |
||||
} |
||||
const effectiveDamage = Math.max(0, baseDamage - (piercing ? 0 : (character.newData.armor ?? 0))) |
||||
const blocked = Math.max(0, baseDamage - effectiveDamage) |
||||
const wasUnstable = character.newData.unstable ?? false |
||||
const wasAlive = (character.newData.health ?? 8) > 0 |
||||
character.newData.health = Math.max(0, (character.newData.health ?? 8) - effectiveDamage) |
||||
}; |
||||
const effectiveDamage = Math.max(0, baseDamage - (piercing ? 0 : character.newData.armor ?? 0)); |
||||
const blocked = Math.max(0, baseDamage - effectiveDamage); |
||||
const wasUnstable = character.newData.unstable ?? false; |
||||
const wasAlive = (character.newData.health ?? 8) > 0; |
||||
character.newData.health = Math.max(0, (character.newData.health ?? 8) - effectiveDamage); |
||||
if ((makeUnstable === null && character.newData.health <= 4) || makeUnstable) { |
||||
character.newData.unstable = true |
||||
character.newData.unstable = true; |
||||
} |
||||
const isUnstable = character.newData.unstable ?? false |
||||
const isAlive = character.newData.health > 0 |
||||
description.push(`**${character.name}** took ${effectiveDamage} Harm${ |
||||
piercing ? " ignore-armour" : blocked > 0 ? ` (${blocked} blocked)` : ""}${effectiveDamage > 0 ? "!" : "."}${ |
||||
wasAlive && !isAlive ? " ***Defeated...***" : !wasUnstable && isUnstable ? " ***Unstable!***" : ""}`)
|
||||
result.push(character) |
||||
const isUnstable = character.newData.unstable ?? false; |
||||
const isAlive = character.newData.health > 0; |
||||
description.push( |
||||
`**${character.name}** took ${effectiveDamage} Harm${ |
||||
piercing ? ' ignore-armour' : blocked > 0 ? ` (${blocked} blocked)` : '' |
||||
}${effectiveDamage > 0 ? '!' : '.'}${ |
||||
wasAlive && !isAlive ? ' ***Defeated...***' : !wasUnstable && isUnstable ? ' ***Unstable!***' : '' |
||||
}` |
||||
); |
||||
result.push(character); |
||||
} |
||||
return [description.join("\n"), result] |
||||
return [description.join('\n'), result]; |
||||
} |
||||
} |
||||
|
@ -1,77 +1,85 @@ |
||||
import { |
||||
ApplicationCommandType, |
||||
AutocompleteContext, type CommandContext, |
||||
CommandOptionType, Message, |
||||
SlashCommand, |
||||
type SlashCreator |
||||
} from "slash-create"; |
||||
import {type GameCharacter, listCharacters, loadCharacter, saveCharacter} from "../character.js"; |
||||
import {renderStatus} from "../renderStatus.js"; |
||||
import {readFile} from "fs/promises"; |
||||
import {join} from "path"; |
||||
import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create'; |
||||
import { |
||||
AbstractCharacterStatusCommand, |
||||
CharacterOptionTemplate, |
||||
type CharacterStatusOptions, |
||||
type GameCharacterData, |
||||
type LoadedCharacterData |
||||
} from "./base.js"; |
||||
} from './base.js'; |
||||
|
||||
export class HealCharacterCommand extends AbstractCharacterStatusCommand { |
||||
constructor(creator: SlashCreator) { |
||||
constructor(creator: SlashCreator, opts: CharacterStatusOptions) { |
||||
super(creator, { |
||||
name: "heal", |
||||
description: "Heals the given character(s).", |
||||
...opts, |
||||
name: 'heal', |
||||
description: 'Heals the given character(s).', |
||||
type: ApplicationCommandType.CHAT_INPUT, |
||||
guildIDs: process.env.DEVELOPMENT_GUILD_ID, |
||||
options: [ |
||||
{ |
||||
...CharacterOptionTemplate, |
||||
name: "character", |
||||
description: "The name of the character(s) to heal.", |
||||
required: true, |
||||
name: 'character', |
||||
description: 'The name of the character(s) to heal.', |
||||
required: true |
||||
}, |
||||
{ |
||||
type: CommandOptionType.INTEGER, |
||||
name: "healing", |
||||
description: "The amount of healing to apply to the character(s).", |
||||
name: 'healing', |
||||
description: 'The amount of healing to apply to the character(s).', |
||||
max_value: 99, |
||||
min_value: 0, |
||||
required: true, |
||||
required: true |
||||
}, |
||||
{ |
||||
type: CommandOptionType.BOOLEAN, |
||||
name: "stabilize", |
||||
description: "If true, repairs the unstable status of the character(s).", |
||||
}, |
||||
name: 'stabilize', |
||||
description: 'If true, repairs the unstable status of the character(s).' |
||||
} |
||||
] |
||||
}); |
||||
} |
||||
|
||||
async process(ctx: CommandContext, characters: Map<string, readonly GameCharacterData[]>): Promise<readonly [string, LoadedCharacterData[]] | readonly [string, LoadedCharacterData] | readonly LoadedCharacterData[] | LoadedCharacterData | string> { |
||||
const healing: number = ctx.options["healing"] |
||||
const stabilize: boolean = ctx.options["stabilize"] ?? false |
||||
const description: string[] = [] |
||||
const result: LoadedCharacterData[] = [] |
||||
for (const character of characters.get("character")!) { |
||||
async process( |
||||
ctx: CommandContext, |
||||
{ characters }: { characters: Map<string, readonly GameCharacterData[]> } |
||||
): Promise< |
||||
| readonly [string, LoadedCharacterData[]] |
||||
| readonly [string, LoadedCharacterData] |
||||
| readonly LoadedCharacterData[] |
||||
| LoadedCharacterData |
||||
| string |
||||
> { |
||||
const healing: number = ctx.options['healing']; |
||||
const stabilize: boolean = ctx.options['stabilize'] ?? false; |
||||
const description: string[] = []; |
||||
const result: LoadedCharacterData[] = []; |
||||
for (const character of characters.get('character')!) { |
||||
if (!character.success) { |
||||
description.push(`**${character.name}** healed ${healing} Harm${healing > 0 ? "!" : "."}${stabilize ? " ***Stabilized!***" : ""}`) |
||||
continue |
||||
description.push( |
||||
`**${character.name}** healed ${healing} Harm${healing > 0 ? '!' : '.'}${ |
||||
stabilize ? ' ***Stabilized!***' : '' |
||||
}` |
||||
); |
||||
continue; |
||||
} |
||||
character.newData = { |
||||
...(character.newData ?? character.originalData) |
||||
} |
||||
const wasUnstable = character.newData.unstable ?? false |
||||
const wasAlive = (character.newData.health ?? 8) > 0 |
||||
character.newData.health = Math.min((character.newData.health ?? 8) + healing, 8) |
||||
}; |
||||
const wasUnstable = character.newData.unstable ?? false; |
||||
const wasAlive = (character.newData.health ?? 8) > 0; |
||||
character.newData.health = Math.min((character.newData.health ?? 8) + healing, 8); |
||||
if (stabilize) { |
||||
character.newData.unstable = false |
||||
character.newData.unstable = false; |
||||
} |
||||
const isUnstable = character.newData.unstable ?? false |
||||
const isAlive = character.newData.health > 0 |
||||
description.push(`**${character.name}** healed ${healing} Harm${healing > 0 ? "!" : "."}${ |
||||
!wasAlive && isAlive ? " ***Revived!***" : wasUnstable && !isUnstable ? " ***Stabilized!***" : ""}`)
|
||||
result.push(character) |
||||
const isUnstable = character.newData.unstable ?? false; |
||||
const isAlive = character.newData.health > 0; |
||||
description.push( |
||||
`**${character.name}** healed ${healing} Harm${healing > 0 ? '!' : '.'}${ |
||||
!wasAlive && isAlive ? ' ***Revived!***' : wasUnstable && !isUnstable ? ' ***Stabilized!***' : '' |
||||
}` |
||||
); |
||||
result.push(character); |
||||
} |
||||
return [description.join("\n"), result] |
||||
return [description.join('\n'), result]; |
||||
} |
||||
} |
||||
|
@ -1,66 +1,72 @@ |
||||
import { |
||||
ApplicationCommandType, |
||||
AutocompleteContext, type CommandContext, |
||||
CommandOptionType, Message, |
||||
SlashCommand, |
||||
type SlashCreator |
||||
} from "slash-create"; |
||||
import {type GameCharacter, listCharacters, loadCharacter, saveCharacter} from "../character.js"; |
||||
import {renderStatus} from "../renderStatus.js"; |
||||
import {readFile} from "fs/promises"; |
||||
import {join} from "path"; |
||||
import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create'; |
||||
import { |
||||
AbstractCharacterStatusCommand, |
||||
CharacterOptionTemplate, |
||||
type CharacterStatusOptions, |
||||
type GameCharacterData, |
||||
type LoadedCharacterData |
||||
} from "./base.js"; |
||||
} from './base.js'; |
||||
|
||||
export class LuckCharacterCommand extends AbstractCharacterStatusCommand { |
||||
constructor(creator: SlashCreator) { |
||||
constructor(creator: SlashCreator, opts: CharacterStatusOptions) { |
||||
super(creator, { |
||||
name: "luck", |
||||
description: "Modifies the luck of the given character(s).", |
||||
...opts, |
||||
name: 'luck', |
||||
description: 'Modifies the luck of the given character(s).', |
||||
type: ApplicationCommandType.CHAT_INPUT, |
||||
guildIDs: process.env.DEVELOPMENT_GUILD_ID, |
||||
options: [ |
||||
{ |
||||
...CharacterOptionTemplate, |
||||
name: "character", |
||||
description: "The name of the character(s) to grant luck to or remove luck from.", |
||||
required: true, |
||||
name: 'character', |
||||
description: 'The name of the character(s) to grant luck to or remove luck from.', |
||||
required: true |
||||
}, |
||||
{ |
||||
type: CommandOptionType.INTEGER, |
||||
name: "delta", |
||||
description: "The amount of luck to apply to the character(s) (default -1).", |
||||
name: 'delta', |
||||
description: 'The amount of luck to apply to the character(s) (default -1).', |
||||
max_value: 7, |
||||
min_value: -7, |
||||
required: false, |
||||
}, |
||||
required: false |
||||
} |
||||
] |
||||
}); |
||||
} |
||||
|
||||
async process(ctx: CommandContext, characters: Map<string, readonly GameCharacterData[]>): Promise<readonly [string, LoadedCharacterData[]] | readonly [string, LoadedCharacterData] | readonly LoadedCharacterData[] | LoadedCharacterData | string> { |
||||
const delta: number = ctx.options["delta"] |
||||
const description: string[] = [] |
||||
const result: LoadedCharacterData[] = [] |
||||
for (const character of characters.get("character")!) { |
||||
async process( |
||||
ctx: CommandContext, |
||||
{ characters }: { characters: Map<string, readonly GameCharacterData[]> } |
||||
): Promise< |
||||
| readonly [string, LoadedCharacterData[]] |
||||
| readonly [string, LoadedCharacterData] |
||||
| readonly LoadedCharacterData[] |
||||
| LoadedCharacterData |
||||
| string |
||||
> { |
||||
const delta: number = ctx.options['delta'] ?? -1; |
||||
const description: string[] = []; |
||||
const result: LoadedCharacterData[] = []; |
||||
for (const character of characters.get('character')!) { |
||||
if (!character.success) { |
||||
description.push(`**${character.name}** ${delta > 0 ? "recovered" : "spent"} ${Math.abs(delta)} Luck${delta !== 0 ? "!" : "."}`) |
||||
continue |
||||
description.push( |
||||
`**${character.name}** ${delta > 0 ? 'recovered' : 'spent'} ${Math.abs(delta)} Luck${delta !== 0 ? '!' : '.'}` |
||||
); |
||||
continue; |
||||
} |
||||
character.newData = { |
||||
...(character.newData ?? character.originalData) |
||||
} |
||||
const wasDoomed = (character.newData.luck ?? 7) === 0 |
||||
character.newData.luck = Math.max(0, Math.min((character.newData.luck ?? 7) + delta, 7)) |
||||
const isDoomed = character.newData.luck === 0 |
||||
description.push(`**${character.name}** ${delta > 0 ? "recovered" : "spent"} ${Math.abs(delta)} Luck${delta !== 0 ? "!" : "."}${ |
||||
!wasDoomed && isDoomed ? " ***Doomed...***" : wasDoomed && !isDoomed ? " ***Fate averted!***" : ""}`)
|
||||
result.push(character) |
||||
}; |
||||
const wasDoomed = (character.newData.luck ?? 7) === 0; |
||||
character.newData.luck = Math.max(0, Math.min((character.newData.luck ?? 7) + delta, 7)); |
||||
const isDoomed = character.newData.luck === 0; |
||||
description.push( |
||||
`**${character.name}** ${delta > 0 ? 'recovered' : 'spent'} ${Math.abs(delta)} Luck${delta !== 0 ? '!' : '.'}${ |
||||
!wasDoomed && isDoomed ? ' ***Doomed...***' : wasDoomed && !isDoomed ? ' ***Fate averted!***' : '' |
||||
}` |
||||
); |
||||
result.push(character); |
||||
} |
||||
return [description.join("\n"), result] |
||||
return [description.join('\n'), result]; |
||||
} |
||||
} |
||||
|
@ -0,0 +1,88 @@ |
||||
import { |
||||
AbstractCharacterStatusCommand, |
||||
CharacterOptionTemplate, |
||||
type CharacterStatusOptions, |
||||
type GameCharacterData, |
||||
type LoadedCharacterData |
||||
} from './base.js'; |
||||
import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create'; |
||||
import {type GameParty, saveParty} from '../character.js'; |
||||
|
||||
export class PartyCommand extends AbstractCharacterStatusCommand { |
||||
constructor(creator: SlashCreator, opts: CharacterStatusOptions) { |
||||
super(creator, { |
||||
...opts, |
||||
name: 'party', |
||||
description: 'Gets or sets the current party list.', |
||||
type: ApplicationCommandType.CHAT_INPUT, |
||||
guildIDs: process.env.DEVELOPMENT_GUILD_ID, |
||||
options: [ |
||||
{ |
||||
...CharacterOptionTemplate, |
||||
name: 'characters', |
||||
description: 'The name of the character(s) to become the current party. If not set, shows the party instead.', |
||||
required: false |
||||
}, |
||||
{ |
||||
type: CommandOptionType.BOOLEAN, |
||||
name: 'replace', |
||||
description: |
||||
'If true, the party is being entirely replaced, so show the character list instead of the delta.', |
||||
required: false |
||||
} |
||||
] |
||||
}); |
||||
} |
||||
|
||||
async process( |
||||
ctx: CommandContext, |
||||
{ characters, party }: { characters: Map<string, readonly GameCharacterData[]>; party: GameParty } |
||||
): Promise< |
||||
| readonly [string, LoadedCharacterData[]] |
||||
| readonly [string, LoadedCharacterData] |
||||
| readonly LoadedCharacterData[] |
||||
| LoadedCharacterData |
||||
| string |
||||
> { |
||||
const replace: boolean | undefined = ctx.options.replace; |
||||
const oldPartyMemberNames = party.activeParty; |
||||
const newPartyMemberData = characters.get('party'); |
||||
if (!newPartyMemberData || newPartyMemberData.length === 0) { |
||||
return [ |
||||
`The current active party is:\n **${oldPartyMemberNames.join('**\n **')}**`, |
||||
(await this.loadCharacters(oldPartyMemberNames)).flatMap((c) => (c.success ? [c] : [])) |
||||
]; |
||||
} |
||||
const newPartyMemberNames = newPartyMemberData.map(c => c.name) |
||||
await saveParty(this.dataDir, { |
||||
...party, |
||||
activeParty: newPartyMemberNames, |
||||
}) |
||||
const oldPartySet = new Set(oldPartyMemberNames); |
||||
const newPartySet = new Set(newPartyMemberNames); |
||||
const leftSet = new Set<string>(); |
||||
for (const member of oldPartySet) { |
||||
if (!newPartySet.has(member)) { |
||||
leftSet.add(member); |
||||
} |
||||
} |
||||
const joinedSet = new Set<string>(); |
||||
for (const member of newPartySet) { |
||||
if (!oldPartySet.has(member)) { |
||||
joinedSet.add(member); |
||||
} |
||||
} |
||||
const deltas = [ |
||||
...Array.from(joinedSet).map((c) => `**${c}** joined the party.`), |
||||
...Array.from(leftSet).map((c) => `**${c}** left the party.`) |
||||
]; |
||||
if (replace || deltas.length === 0 || (replace === false && deltas.length > newPartyMemberNames.length)) { |
||||
return [ |
||||
`The current active party is:\n **${newPartyMemberData.map((c) => c.name).join('**\n **')}**`, |
||||
newPartyMemberData.flatMap((c) => (c.success ? [c] : [])) |
||||
]; |
||||
} else { |
||||
return [deltas.join('\n'), newPartyMemberData.flatMap((c) => (c.success ? [c] : []))]; |
||||
} |
||||
} |
||||
} |
@ -1,36 +1,41 @@ |
||||
import { |
||||
ApplicationCommandType, |
||||
type CommandContext, |
||||
CommandOptionType, |
||||
SlashCommand, |
||||
type SlashCreator |
||||
} from "slash-create"; |
||||
import { ApplicationCommandType, type CommandContext, type SlashCreator } from 'slash-create'; |
||||
import { |
||||
CharacterOptionTemplate, |
||||
AbstractCharacterStatusCommand, |
||||
type GameCharacterData, |
||||
type LoadedCharacterData |
||||
} from "./base.js"; |
||||
type LoadedCharacterData, |
||||
type CharacterStatusOptions |
||||
} from './base.js'; |
||||
|
||||
export class CharacterStatusCommand extends AbstractCharacterStatusCommand { |
||||
constructor(creator: SlashCreator) { |
||||
constructor(creator: SlashCreator, opts: CharacterStatusOptions) { |
||||
super(creator, { |
||||
name: "status", |
||||
description: "Gets the status of the given character(s).", |
||||
...opts, |
||||
name: 'status', |
||||
description: 'Gets the status of the given character(s).', |
||||
type: ApplicationCommandType.CHAT_INPUT, |
||||
guildIDs: process.env.DEVELOPMENT_GUILD_ID, |
||||
options: [ |
||||
{ |
||||
...CharacterOptionTemplate, |
||||
name: "character", |
||||
description: "The name of the character(s) to get the status of.", |
||||
required: true, |
||||
}, |
||||
name: 'character', |
||||
description: 'The name of the character(s) to get the status of.', |
||||
required: true |
||||
} |
||||
] |
||||
}); |
||||
} |
||||
|
||||
async process(ctx: CommandContext, characters: Map<string, readonly GameCharacterData[]>): Promise<readonly [string, LoadedCharacterData[]] | readonly [string, LoadedCharacterData] | readonly LoadedCharacterData[] | LoadedCharacterData | string> { |
||||
return characters.get("character")!.flatMap((x) => x.success ? [x] : []); |
||||
async process( |
||||
_ctx: CommandContext, |
||||
{ characters }: { characters: Map<string, readonly GameCharacterData[]> } |
||||
): Promise< |
||||
| readonly [string, LoadedCharacterData[]] |
||||
| readonly [string, LoadedCharacterData] |
||||
| readonly LoadedCharacterData[] |
||||
| LoadedCharacterData |
||||
| string |
||||
> { |
||||
return characters.get('character')!.flatMap((x) => (x.success ? [x] : [])); |
||||
} |
||||
} |
||||
|
@ -1,185 +1,189 @@ |
||||
import {Resvg} from "@resvg/resvg-js"; |
||||
import {JSDOM} from "jsdom"; |
||||
import { Resvg } from '@resvg/resvg-js'; |
||||
import { JSDOM } from 'jsdom'; |
||||
|
||||
export interface Element { |
||||
remove(): void; |
||||
} |
||||
|
||||
export interface GameStatus { |
||||
health: number |
||||
unstable: boolean |
||||
healthDelta: number |
||||
experience: number |
||||
experienceDelta: number |
||||
luck: number |
||||
luckDelta: number |
||||
health: number; |
||||
unstable: boolean; |
||||
healthDelta: number; |
||||
experience: number; |
||||
experienceDelta: number; |
||||
luck: number; |
||||
luckDelta: number; |
||||
} |
||||
|
||||
export interface GameStatusWithPortrait extends GameStatus { |
||||
portrait: Buffer |
||||
portrait: Buffer; |
||||
} |
||||
|
||||
function removeElement(dom: Element): void { |
||||
dom.remove() |
||||
dom.remove(); |
||||
} |
||||
|
||||
function healthBelow(health: number): (status: GameStatus) => boolean { |
||||
return (s) => s.health < health |
||||
return (s) => s.health < health; |
||||
} |
||||
|
||||
function luckBelow(luck: number): (status: GameStatus) => boolean { |
||||
return (s) => s.luck < luck |
||||
return (s) => s.luck < luck; |
||||
} |
||||
|
||||
function experienceBelow(exp: number): (status: GameStatus) => boolean { |
||||
return (s) => s.experience < exp |
||||
return (s) => s.experience < exp; |
||||
} |
||||
|
||||
function healthNotEqual(health: number): (status: GameStatus) => boolean { |
||||
return (s) => s.health !== health |
||||
return (s) => s.health !== health; |
||||
} |
||||
|
||||
function experienceDeltaNotIncludes(index: number): (status: GameStatus) => boolean { |
||||
return (s) => { |
||||
if (s.experienceDelta <= 0) { |
||||
return true |
||||
return true; |
||||
} |
||||
const effectiveMax = Math.min(5, s.experience) |
||||
return !(index <= effectiveMax && index > effectiveMax - s.experienceDelta) |
||||
} |
||||
const effectiveMax = Math.min(5, s.experience); |
||||
return !(index <= effectiveMax && index > effectiveMax - s.experienceDelta); |
||||
}; |
||||
} |
||||
|
||||
function luckDeltaNotIncludes(index: number): (status: GameStatus) => boolean { |
||||
return (s) => { |
||||
if (s.luckDelta === 0) { |
||||
return true |
||||
return true; |
||||
} else if (s.luckDelta > 0) { |
||||
return !((index <= s.luck) && (index > s.luck - s.luckDelta)) |
||||
return !(index <= s.luck && index > s.luck - s.luckDelta); |
||||
} else { |
||||
return !((index > s.luck) && (index <= s.luck - s.luckDelta)) |
||||
return !(index > s.luck && index <= s.luck - s.luckDelta); |
||||
} |
||||
} |
||||
}; |
||||
} |
||||
|
||||
function healthDamageNotIncludes(index: number): (status: GameStatus) => boolean { |
||||
return (s) => { |
||||
if (s.healthDelta >= 0) { |
||||
return true |
||||
return true; |
||||
} |
||||
return !((index > s.health) && (index <= s.health - s.healthDelta)) |
||||
} |
||||
return !(index > s.health && index <= s.health - s.healthDelta); |
||||
}; |
||||
} |
||||
|
||||
function healthRecoveredNotIncludes(index: number): (status: GameStatus) => boolean { |
||||
return (s) => { |
||||
if (s.healthDelta <= 0) { |
||||
return true |
||||
return true; |
||||
} |
||||
return !((index <= s.health) && (index > s.health - s.healthDelta)) |
||||
} |
||||
return !(index <= s.health && index > s.health - s.healthDelta); |
||||
}; |
||||
} |
||||
|
||||
function notStableAlive(): (status: GameStatus) => boolean { |
||||
return (status) => status.unstable || status.health <= 0 |
||||
return (status) => status.unstable || status.health <= 0; |
||||
} |
||||
|
||||
function notUnstableAlive(): (status: GameStatus) => boolean { |
||||
return (status) => !status.unstable || status.health <= 0 |
||||
return (status) => !status.unstable || status.health <= 0; |
||||
} |
||||
|
||||
function notDead(): (status: GameStatus) => boolean { |
||||
return (status) => status.health > 0 |
||||
return (status) => status.health > 0; |
||||
} |
||||
|
||||
function dead(): (status: GameStatus) => boolean { |
||||
return (status) => status.health <= 0 |
||||
return (status) => status.health <= 0; |
||||
} |
||||
|
||||
const mappings: [filter: (status: GameStatus) => boolean, selector: string, hit: (dom: Element) => void][] = [ |
||||
[healthBelow(8), "#health8", removeElement], |
||||
[healthBelow(7), "#health7", removeElement], |
||||
[healthBelow(6), "#health6", removeElement], |
||||
[healthBelow(5), "#health5", removeElement], |
||||
[healthBelow(4), "#health4", removeElement], |
||||
[healthBelow(3), "#health3", removeElement], |
||||
[healthBelow(2), "#health2", removeElement], |
||||
[healthBelow(1), "#health1", removeElement], |
||||
|
||||
[luckBelow(7), "#luck7", removeElement], |
||||
[luckBelow(6), "#luck6", removeElement], |
||||
[luckBelow(5), "#luck5", removeElement], |
||||
[luckBelow(4), "#luck4", removeElement], |
||||
[luckBelow(3), "#luck3", removeElement], |
||||
[luckBelow(2), "#luck2", removeElement], |
||||
[luckBelow(1), "#luck1", removeElement], |
||||
|
||||
[experienceBelow(5), "#experienceLevelUpReady", removeElement], |
||||
[experienceBelow(5), "#experience5", removeElement], |
||||
[experienceBelow(4), "#experience4", removeElement], |
||||
[experienceBelow(3), "#experience3", removeElement], |
||||
[experienceBelow(2), "#experience2", removeElement], |
||||
[experienceBelow(1), "#experience1", removeElement], |
||||
|
||||
[healthDamageNotIncludes(8), "#healthDamage8", removeElement], |
||||
[healthDamageNotIncludes(7), "#healthDamage7", removeElement], |
||||
[healthDamageNotIncludes(6), "#healthDamage6", removeElement], |
||||
[healthDamageNotIncludes(5), "#healthDamage5", removeElement], |
||||
[healthDamageNotIncludes(4), "#healthDamage4", removeElement], |
||||
[healthDamageNotIncludes(3), "#healthDamage3", removeElement], |
||||
[healthDamageNotIncludes(2), "#healthDamage2", removeElement], |
||||
[healthDamageNotIncludes(1), "#healthDamage1", removeElement], |
||||
|
||||
[healthRecoveredNotIncludes(8), "#healthRecovery8", removeElement], |
||||
[healthRecoveredNotIncludes(7), "#healthRecovery7", removeElement], |
||||
[healthRecoveredNotIncludes(6), "#healthRecovery6", removeElement], |
||||
[healthRecoveredNotIncludes(5), "#healthRecovery5", removeElement], |
||||
[healthRecoveredNotIncludes(4), "#healthRecovery4", removeElement], |
||||
[healthRecoveredNotIncludes(3), "#healthRecovery3", removeElement], |
||||
[healthRecoveredNotIncludes(2), "#healthRecovery2", removeElement], |
||||
[healthRecoveredNotIncludes(1), "#healthRecovery1", removeElement], |
||||
|
||||
[healthNotEqual(8), "#healthCounter8", removeElement], |
||||
[healthNotEqual(7), "#healthCounter7", removeElement], |
||||
[healthNotEqual(6), "#healthCounter6", removeElement], |
||||
[healthNotEqual(5), "#healthCounter5", removeElement], |
||||
[healthNotEqual(4), "#healthCounter4", removeElement], |
||||
[healthNotEqual(3), "#healthCounter3", removeElement], |
||||
[healthNotEqual(2), "#healthCounter2", removeElement], |
||||
[healthNotEqual(1), "#healthCounter1", removeElement], |
||||
[healthNotEqual(0), "#healthCounter0", removeElement], |
||||
|
||||
[luckDeltaNotIncludes(7), "#luckShine7", removeElement], |
||||
[luckDeltaNotIncludes(6), "#luckShine6", removeElement], |
||||
[luckDeltaNotIncludes(5), "#luckShine5", removeElement], |
||||
[luckDeltaNotIncludes(4), "#luckShine4", removeElement], |
||||
[luckDeltaNotIncludes(3), "#luckShine3", removeElement], |
||||
[luckDeltaNotIncludes(2), "#luckShine2", removeElement], |
||||
[luckDeltaNotIncludes(1), "#luckShine1", removeElement], |
||||
|
||||
[experienceDeltaNotIncludes(5), "#experienceUp5", removeElement], |
||||
[experienceDeltaNotIncludes(4), "#experienceUp4", removeElement], |
||||
[experienceDeltaNotIncludes(3), "#experienceUp3", removeElement], |
||||
[experienceDeltaNotIncludes(2), "#experienceUp2", removeElement], |
||||
[experienceDeltaNotIncludes(1), "#experienceUp1", removeElement], |
||||
|
||||
[notStableAlive(), "#healthIcon", removeElement], |
||||
[notUnstableAlive(), "#healthLowIcon", removeElement], |
||||
[notDead(), "#healthEmptyIcon", removeElement], |
||||
[notDead(), "#characterDyingFace", removeElement], |
||||
[dead(), "#characterFaceImage", removeElement] |
||||
] |
||||
[healthBelow(8), '#health8', removeElement], |
||||
[healthBelow(7), '#health7', removeElement], |
||||
[healthBelow(6), '#health6', removeElement], |
||||
[healthBelow(5), '#health5', removeElement], |
||||
[healthBelow(4), '#health4', removeElement], |
||||
[healthBelow(3), '#health3', removeElement], |
||||
[healthBelow(2), '#health2', removeElement], |
||||
[healthBelow(1), '#health1', removeElement], |
||||
|
||||
[luckBelow(7), '#luck7', removeElement], |
||||
[luckBelow(6), '#luck6', removeElement], |
||||
[luckBelow(5), '#luck5', removeElement], |
||||
[luckBelow(4), '#luck4', removeElement], |
||||
[luckBelow(3), '#luck3', removeElement], |
||||
[luckBelow(2), '#luck2', removeElement], |
||||
[luckBelow(1), '#luck1', removeElement], |
||||
|
||||
[experienceBelow(5), '#experienceLevelUpReady', removeElement], |
||||
[experienceBelow(5), '#experience5', removeElement], |
||||
[experienceBelow(4), '#experience4', removeElement], |
||||
[experienceBelow(3), '#experience3', removeElement], |
||||
[experienceBelow(2), '#experience2', removeElement], |
||||
[experienceBelow(1), '#experience1', removeElement], |
||||
|
||||
[healthDamageNotIncludes(8), '#healthDamage8', removeElement], |
||||
[healthDamageNotIncludes(7), '#healthDamage7', removeElement], |
||||
[healthDamageNotIncludes(6), '#healthDamage6', removeElement], |
||||
[healthDamageNotIncludes(5), '#healthDamage5', removeElement], |
||||
[healthDamageNotIncludes(4), '#healthDamage4', removeElement], |
||||
[healthDamageNotIncludes(3), '#healthDamage3', removeElement], |
||||
[healthDamageNotIncludes(2), '#healthDamage2', removeElement], |
||||
[healthDamageNotIncludes(1), '#healthDamage1', removeElement], |
||||
|
||||
[healthRecoveredNotIncludes(8), '#healthRecovery8', removeElement], |
||||
[healthRecoveredNotIncludes(7), '#healthRecovery7', removeElement], |
||||
[healthRecoveredNotIncludes(6), '#healthRecovery6', removeElement], |
||||
[healthRecoveredNotIncludes(5), '#healthRecovery5', removeElement], |
||||
[healthRecoveredNotIncludes(4), '#healthRecovery4', removeElement], |
||||
[healthRecoveredNotIncludes(3), '#healthRecovery3', removeElement], |
||||
[healthRecoveredNotIncludes(2), '#healthRecovery2', removeElement], |
||||
[healthRecoveredNotIncludes(1), '#healthRecovery1', removeElement], |
||||
|
||||
[healthNotEqual(8), '#healthCounter8', removeElement], |
||||
[healthNotEqual(7), '#healthCounter7', removeElement], |
||||
[healthNotEqual(6), '#healthCounter6', removeElement], |
||||
[healthNotEqual(5), '#healthCounter5', removeElement], |
||||
[healthNotEqual(4), '#healthCounter4', removeElement], |
||||
[healthNotEqual(3), '#healthCounter3', removeElement], |
||||
[healthNotEqual(2), '#healthCounter2', removeElement], |
||||
[healthNotEqual(1), '#healthCounter1', removeElement], |
||||
[healthNotEqual(0), '#healthCounter0', removeElement], |
||||
|
||||
[luckDeltaNotIncludes(7), '#luckShine7', removeElement], |
||||
[luckDeltaNotIncludes(6), '#luckShine6', removeElement], |
||||
[luckDeltaNotIncludes(5), '#luckShine5', removeElement], |
||||
[luckDeltaNotIncludes(4), '#luckShine4', removeElement], |
||||
[luckDeltaNotIncludes(3), '#luckShine3', removeElement], |
||||
[luckDeltaNotIncludes(2), '#luckShine2', removeElement], |
||||
[luckDeltaNotIncludes(1), '#luckShine1', removeElement], |
||||
|
||||
[experienceDeltaNotIncludes(5), '#experienceUp5', removeElement], |
||||
[experienceDeltaNotIncludes(4), '#experienceUp4', removeElement], |
||||
[experienceDeltaNotIncludes(3), '#experienceUp3', removeElement], |
||||
[experienceDeltaNotIncludes(2), '#experienceUp2', removeElement], |
||||
[experienceDeltaNotIncludes(1), '#experienceUp1', removeElement], |
||||
|
||||
[notStableAlive(), '#healthIcon', removeElement], |
||||
[notUnstableAlive(), '#healthLowIcon', removeElement], |
||||
[notDead(), '#healthEmptyIcon', removeElement], |
||||
[notDead(), '#characterDyingFace', removeElement], |
||||
[dead(), '#characterFaceImage', removeElement] |
||||
]; |
||||
|
||||
export async function renderStatus(svgTemplate: string, status: GameStatusWithPortrait): Promise<Buffer> { |
||||
const dom = new JSDOM(svgTemplate, { |
||||
contentType: "image/svg+xml", |
||||
contentType: 'image/svg+xml', |
||||
pretendToBeVisual: false, |
||||
includeNodeLocations: false, |
||||
url: "https://localhost/status.xml" |
||||
}) |
||||
url: 'https://localhost/status.xml' |
||||
}); |
||||
for (const [filter, selector, action] of mappings) { |
||||
if (filter(status)) { |
||||
for (const el of dom.window.document.querySelectorAll(selector)) { |
||||
action(el) |
||||
action(el); |
||||
} |
||||
} |
||||
} |
||||
const resvg = new Resvg(dom.window.document.documentElement.outerHTML, {}) |
||||
resvg.resolveImage("https://invalid.invalid/face.png", status.portrait) |
||||
return resvg.render().asPng() |
||||
const resvg = new Resvg(dom.window.document.documentElement.outerHTML, {}); |
||||
resvg.resolveImage('https://invalid.invalid/face.png', status.portrait); |
||||
return resvg.render().asPng(); |
||||
} |
||||
|
Loading…
Reference in new issue