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 { 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'; |
||||||
|
|
||||||
export interface GameCharacter { |
export interface GameCharacter { |
||||||
health: number |
health: number; |
||||||
armor?: number |
armor?: number; |
||||||
unstable?: boolean |
unstable?: boolean; |
||||||
luck: number |
luck: number; |
||||||
luckSpecial?: string |
luckSpecial?: string; |
||||||
experience: number |
experience: number; |
||||||
|
|
||||||
Charm?: number |
Charm?: number; |
||||||
Cool?: number |
Cool?: number; |
||||||
Sharp?: number |
Sharp?: number; |
||||||
Tough?: number |
Tough?: number; |
||||||
Weird?: number |
Weird?: number; |
||||||
|
|
||||||
color: number |
color: number; |
||||||
defaultFacePath: string |
defaultFacePath: string; |
||||||
additionalFaces?: GameCharacterFace[] |
additionalFaces?: GameCharacterFace[]; |
||||||
activeFaceSets?: string[] |
activeFaceSets?: string[]; |
||||||
moves?: GameCharacterMove[] |
moves?: GameCharacterMove[]; |
||||||
improvementsTaken?: string[] |
improvementsTaken?: string[]; |
||||||
improvementsAvailable?: string[] |
improvementsAvailable?: string[]; |
||||||
improvementsAdvanced?: string[] |
improvementsAdvanced?: string[]; |
||||||
} |
} |
||||||
|
|
||||||
export enum GameAttribute { |
export enum GameAttribute { |
||||||
Charm = "Charm", |
Charm = 'Charm', |
||||||
Cool = "Cool", |
Cool = 'Cool', |
||||||
Sharp = "Sharp", |
Sharp = 'Sharp', |
||||||
Tough = "Tough", |
Tough = 'Tough', |
||||||
Weird = "Weird", |
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 { |
export interface GameCharacterMoveBase { |
||||||
type?: string |
type?: string; |
||||||
name: string |
name: string; |
||||||
summary?: string |
summary?: string; |
||||||
description?: string |
description?: string; |
||||||
} |
} |
||||||
|
|
||||||
export interface GameCharacterPassiveMove extends GameCharacterMoveBase { |
export interface GameCharacterPassiveMove extends GameCharacterMoveBase { |
||||||
type?: "passive" |
type?: 'passive'; |
||||||
} |
} |
||||||
|
|
||||||
export interface GameCharacterRollableMove extends GameCharacterMoveBase { |
export interface GameCharacterRollableMove extends GameCharacterMoveBase { |
||||||
type: "rollable" |
type: 'rollable'; |
||||||
attribute?: GameAttribute |
attribute?: GameAttribute; |
||||||
bonus?: number |
bonus?: number; |
||||||
advanced?: boolean |
advanced?: boolean; |
||||||
onAdvanced?: string |
onAdvanced?: string; |
||||||
onSuccess?: string |
onSuccess?: string; |
||||||
onMixed?: string |
onMixed?: string; |
||||||
onMiss?: string |
onMiss?: string; |
||||||
} |
} |
||||||
|
|
||||||
export type GameCharacterMove = GameCharacterPassiveMove|GameCharacterRollableMove |
export type GameCharacterMove = GameCharacterPassiveMove | GameCharacterRollableMove; |
||||||
|
|
||||||
export interface FaceConditionBase { |
export interface FaceConditionBase { |
||||||
type: string |
type: string; |
||||||
negated?: boolean |
negated?: boolean; |
||||||
} |
} |
||||||
|
|
||||||
export interface FaceConditionStability extends FaceConditionBase { |
export interface FaceConditionStability extends FaceConditionBase { |
||||||
type: "stable"|"unstable"|"dead" |
type: 'stable' | 'unstable' | 'dead'; |
||||||
} |
} |
||||||
|
|
||||||
export interface FaceConditionHealth extends FaceConditionBase { |
export interface FaceConditionHealth extends FaceConditionBase { |
||||||
type: "hpEq"|"hpGt"|"hpLt"|"hpGtEq"|"hpLtEq" |
type: 'hpEq' | 'hpGt' | 'hpLt' | 'hpGtEq' | 'hpLtEq'; |
||||||
threshold: number |
threshold: number; |
||||||
} |
} |
||||||
|
|
||||||
export interface FaceConditionHealthDelta extends FaceConditionBase { |
export interface FaceConditionHealthDelta extends FaceConditionBase { |
||||||
type: "beingHealed"|"beingDamaged"|"healthSteady" |
type: 'beingHealed' | 'beingDamaged' | 'healthSteady'; |
||||||
} |
} |
||||||
|
|
||||||
export interface FaceConditionSet extends FaceConditionBase { |
export interface FaceConditionSet extends FaceConditionBase { |
||||||
type: "faceSetActive" |
type: 'faceSetActive'; |
||||||
set: string |
set: string; |
||||||
} |
} |
||||||
|
|
||||||
export type FaceCondition = FaceConditionStability|FaceConditionHealth|FaceConditionHealthDelta|FaceConditionSet |
export type FaceCondition = FaceConditionStability | FaceConditionHealth | FaceConditionHealthDelta | FaceConditionSet; |
||||||
|
|
||||||
export interface GameCharacterFace { |
export interface GameCharacterFace { |
||||||
path: string |
path: string; |
||||||
conditions: FaceCondition[] |
conditions: FaceCondition[]; |
||||||
} |
} |
||||||
|
|
||||||
export const FaceSetIdentifier = /^[a-z0-9_]+$/ |
export const FaceSetIdentifier = /^[a-z0-9_]+$/; |
||||||
|
|
||||||
export async function listCharacters(dataDir: string): Promise<string[]> { |
export async function listCharacters(dataDir: string): Promise<string[]> { |
||||||
const list = await readdir(join(dataDir, "characters")) |
const list = await readdir(join(dataDir, 'characters')); |
||||||
return list.filter(s => s.endsWith(".yaml")).map(s => s.substring(0, s.length - 5)) |
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> { |
export async function loadCharacter(dataDir: string, name: string): Promise<GameCharacter> { |
||||||
const contents = await readFile(join(dataDir, "characters", name + ".yaml"), {encoding: "utf-8"}) |
const contents = await readFile(join(dataDir, 'characters', name + '.yaml'), { encoding: 'utf-8' }); |
||||||
return parseYaml(contents) |
return parseYaml(contents); |
||||||
} |
} |
||||||
|
|
||||||
export async function saveCharacter(dataDir: string, name: string, character: GameCharacter): Promise<void> { |
export async function saveCharacter(dataDir: string, name: string, character: GameCharacter): Promise<void> { |
||||||
const contents = stringifyYaml(character) |
const contents = stringifyYaml(character); |
||||||
return writeFile(join(dataDir, "characters", name + ".yaml"), contents) |
return writeFile(join(dataDir, 'characters', name + '.yaml'), contents, { encoding: 'utf-8' }); |
||||||
} |
} |
||||||
|
|
||||||
export interface GameParty { |
export interface GameParty { |
||||||
defaultCharacters: Record<string, string> |
defaultCharacters: Record<string, string>; |
||||||
activeParty: string[] |
activeParty: string[]; |
||||||
keeper: string |
keeper: string; |
||||||
} |
} |
||||||
|
|
||||||
export interface ReadonlyGameParty { |
export interface ReadonlyGameParty { |
||||||
readonly defaultCharacters: Readonly<Record<string, string>> |
readonly defaultCharacters: Readonly<Record<string, string>>; |
||||||
readonly activeParty: readonly string[] |
readonly activeParty: readonly string[]; |
||||||
readonly keeper: string |
readonly keeper: string; |
||||||
} |
} |
||||||
|
|
||||||
export async function loadParty(dataDir: string): Promise<GameParty> { |
export async function loadParty(dataDir: string): Promise<GameParty> { |
||||||
const contents = await readFile(join(dataDir, "party.yaml"), {encoding: "utf-8"}) |
const contents = await readFile(join(dataDir, 'party.yaml'), { encoding: 'utf-8' }); |
||||||
return parseYaml(contents) |
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 { |
import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create'; |
||||||
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 { |
import { |
||||||
AbstractCharacterStatusCommand, |
AbstractCharacterStatusCommand, |
||||||
CharacterOptionTemplate, |
CharacterOptionTemplate, |
||||||
|
type CharacterStatusOptions, |
||||||
type GameCharacterData, |
type GameCharacterData, |
||||||
type LoadedCharacterData |
type LoadedCharacterData |
||||||
} from "./base.js"; |
} from './base.js'; |
||||||
|
|
||||||
export class ExperienceCharacterCommand extends AbstractCharacterStatusCommand { |
export class ExperienceCharacterCommand extends AbstractCharacterStatusCommand { |
||||||
constructor(creator: SlashCreator) { |
constructor(creator: SlashCreator, opts: CharacterStatusOptions) { |
||||||
super(creator, { |
super(creator, { |
||||||
name: "experience", |
...opts, |
||||||
description: "Modifies the EXP total of the given character(s).", |
name: 'experience', |
||||||
|
description: 'Modifies the EXP total of the given character(s).', |
||||||
type: ApplicationCommandType.CHAT_INPUT, |
type: ApplicationCommandType.CHAT_INPUT, |
||||||
guildIDs: process.env.DEVELOPMENT_GUILD_ID, |
guildIDs: process.env.DEVELOPMENT_GUILD_ID, |
||||||
options: [ |
options: [ |
||||||
{ |
{ |
||||||
...CharacterOptionTemplate, |
...CharacterOptionTemplate, |
||||||
name: "character", |
name: 'character', |
||||||
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 |
||||||
}, |
}, |
||||||
{ |
{ |
||||||
type: CommandOptionType.INTEGER, |
type: CommandOptionType.INTEGER, |
||||||
name: "delta", |
name: 'delta', |
||||||
description: "The amount of EXP to apply to the character(s) (default +1).", |
description: 'The amount of EXP to apply to the character(s) (default +1).', |
||||||
max_value: 25, |
max_value: 25, |
||||||
min_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> { |
async process( |
||||||
const delta = ctx.options["delta"] ?? 1 |
ctx: CommandContext, |
||||||
const description: string[] = [] |
{ characters }: { characters: Map<string, readonly GameCharacterData[]> } |
||||||
const result: LoadedCharacterData[] = [] |
): Promise< |
||||||
for (const character of characters.get("character")!) { |
| 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) { |
if (!character.success) { |
||||||
description.push(`**${character.name}** ${delta >= 0 ? "gained" : "lost"} ${Math.abs(delta)} EXP${delta >= 0 ? "!" : "."}`) |
description.push( |
||||||
continue |
`**${character.name}** ${delta >= 0 ? 'gained' : 'lost'} ${Math.abs(delta)} EXP${delta >= 0 ? '!' : '.'}` |
||||||
|
); |
||||||
|
continue; |
||||||
} |
} |
||||||
character.newData = { |
character.newData = { |
||||||
...character.newData ?? character.originalData |
...(character.newData ?? character.originalData) |
||||||
} |
}; |
||||||
const oldLevels = Math.floor(character.newData.experience / 5) |
const oldLevels = Math.floor(character.newData.experience / 5); |
||||||
character.newData.experience = Math.max(0, (character.newData.experience ?? 0) + delta) |
character.newData.experience = Math.max(0, (character.newData.experience ?? 0) + delta); |
||||||
const levels = Math.floor(character.newData.experience / 5) |
const levels = Math.floor(character.newData.experience / 5); |
||||||
result.push(character) |
result.push(character); |
||||||
description.push(`**${character.name}** ${delta >= 0 ? "gained" : "lost"} ${Math.abs(delta)} EXP${delta >= 0 ? "!" : "."}${levels > oldLevels ? " ***Level up!***" : ""}`) |
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 { |
import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create'; |
||||||
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 { |
import { |
||||||
AbstractCharacterStatusCommand, |
AbstractCharacterStatusCommand, |
||||||
CharacterOptionTemplate, |
CharacterOptionTemplate, |
||||||
|
type CharacterStatusOptions, |
||||||
type GameCharacterData, |
type GameCharacterData, |
||||||
type LoadedCharacterData |
type LoadedCharacterData |
||||||
} from "./base.js"; |
} from './base.js'; |
||||||
|
|
||||||
export class HarmCharacterCommand extends AbstractCharacterStatusCommand { |
export class HarmCharacterCommand extends AbstractCharacterStatusCommand { |
||||||
constructor(creator: SlashCreator) { |
constructor(creator: SlashCreator, opts: CharacterStatusOptions) { |
||||||
super(creator, { |
super(creator, { |
||||||
name: "harm", |
...opts, |
||||||
description: "Harms the given character(s).", |
name: 'harm', |
||||||
|
description: 'Harms the given character(s).', |
||||||
type: ApplicationCommandType.CHAT_INPUT, |
type: ApplicationCommandType.CHAT_INPUT, |
||||||
guildIDs: process.env.DEVELOPMENT_GUILD_ID, |
guildIDs: process.env.DEVELOPMENT_GUILD_ID, |
||||||
options: [ |
options: [ |
||||||
{ |
{ |
||||||
...CharacterOptionTemplate, |
...CharacterOptionTemplate, |
||||||
description: "The name of the character(s) to harm.", |
description: 'The name of the character(s) to harm.', |
||||||
required: true, |
required: true |
||||||
}, |
}, |
||||||
{ |
{ |
||||||
type: CommandOptionType.INTEGER, |
type: CommandOptionType.INTEGER, |
||||||
name: "damage", |
name: 'damage', |
||||||
description: "The amount of harm to deal to the character(s).", |
description: 'The amount of harm to deal to the character(s).', |
||||||
max_value: 99, |
max_value: 99, |
||||||
min_value: 0, |
min_value: 0, |
||||||
required: true, |
required: true |
||||||
}, |
}, |
||||||
{ |
{ |
||||||
type: CommandOptionType.BOOLEAN, |
type: CommandOptionType.BOOLEAN, |
||||||
name: "piercing", |
name: 'piercing', |
||||||
description: "If set, ignores any armor the character(s) may have.", |
description: 'If set, ignores any armor the character(s) may have.' |
||||||
}, |
}, |
||||||
{ |
{ |
||||||
type: CommandOptionType.BOOLEAN, |
type: CommandOptionType.BOOLEAN, |
||||||
name: "destabilize", |
name: 'destabilize', |
||||||
description: "True to force unstable, False to never set unstable. Default based on remaining health.", |
description: 'True to force unstable, False to never set unstable. Default based on remaining health.', |
||||||
required: false, |
required: false |
||||||
} |
} |
||||||
] |
] |
||||||
}); |
}); |
||||||
} |
} |
||||||
|
|
||||||
async process(ctx: CommandContext, characters: Map<string, readonly GameCharacterData[]>): Promise<readonly [string, LoadedCharacterData[]] | readonly [string, LoadedCharacterData] | readonly LoadedCharacterData[] | LoadedCharacterData | string> { |
async process( |
||||||
const baseDamage: number = ctx.options["damage"] |
ctx: CommandContext, |
||||||
const piercing: boolean = ctx.options["piercing"] ?? false |
{ characters }: { characters: Map<string, readonly GameCharacterData[]> } |
||||||
const makeUnstable: boolean|null = ctx.options["destabilize"] ?? null |
): Promise< |
||||||
const description: string[] = [] |
| readonly [string, LoadedCharacterData[]] |
||||||
const result: LoadedCharacterData[] = [] |
| readonly [string, LoadedCharacterData] |
||||||
for (const character of characters.get("character")!) { |
| 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) { |
if (!character.success) { |
||||||
description.push(`**${character.name}** took ${baseDamage} Harm${piercing ? " ignore-armour" : ""}${baseDamage > 0 ? "!" : "."}${makeUnstable ? " ***Unstable!***" : ""}`) |
description.push( |
||||||
continue |
`**${character.name}** took ${baseDamage} Harm${piercing ? ' ignore-armour' : ''}${ |
||||||
|
baseDamage > 0 ? '!' : '.' |
||||||
|
}${makeUnstable ? ' ***Unstable!***' : ''}` |
||||||
|
); |
||||||
|
continue; |
||||||
} |
} |
||||||
character.newData = { |
character.newData = { |
||||||
...(character.newData ?? character.originalData) |
...(character.newData ?? character.originalData) |
||||||
} |
}; |
||||||
const effectiveDamage = Math.max(0, baseDamage - (piercing ? 0 : (character.newData.armor ?? 0))) |
const effectiveDamage = Math.max(0, baseDamage - (piercing ? 0 : character.newData.armor ?? 0)); |
||||||
const blocked = Math.max(0, baseDamage - effectiveDamage) |
const blocked = Math.max(0, baseDamage - effectiveDamage); |
||||||
const wasUnstable = character.newData.unstable ?? false |
const wasUnstable = character.newData.unstable ?? false; |
||||||
const wasAlive = (character.newData.health ?? 8) > 0 |
const wasAlive = (character.newData.health ?? 8) > 0; |
||||||
character.newData.health = Math.max(0, (character.newData.health ?? 8) - effectiveDamage) |
character.newData.health = Math.max(0, (character.newData.health ?? 8) - effectiveDamage); |
||||||
if ((makeUnstable === null && character.newData.health <= 4) || makeUnstable) { |
if ((makeUnstable === null && character.newData.health <= 4) || makeUnstable) { |
||||||
character.newData.unstable = true |
character.newData.unstable = true; |
||||||
} |
} |
||||||
const isUnstable = character.newData.unstable ?? false |
const isUnstable = character.newData.unstable ?? false; |
||||||
const isAlive = character.newData.health > 0 |
const isAlive = character.newData.health > 0; |
||||||
description.push(`**${character.name}** took ${effectiveDamage} Harm${ |
description.push( |
||||||
piercing ? " ignore-armour" : blocked > 0 ? ` (${blocked} blocked)` : ""}${effectiveDamage > 0 ? "!" : "."}${ |
`**${character.name}** took ${effectiveDamage} Harm${ |
||||||
wasAlive && !isAlive ? " ***Defeated...***" : !wasUnstable && isUnstable ? " ***Unstable!***" : ""}`)
|
piercing ? ' ignore-armour' : blocked > 0 ? ` (${blocked} blocked)` : '' |
||||||
result.push(character) |
}${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 { |
import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create'; |
||||||
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 { |
import { |
||||||
AbstractCharacterStatusCommand, |
AbstractCharacterStatusCommand, |
||||||
CharacterOptionTemplate, |
CharacterOptionTemplate, |
||||||
|
type CharacterStatusOptions, |
||||||
type GameCharacterData, |
type GameCharacterData, |
||||||
type LoadedCharacterData |
type LoadedCharacterData |
||||||
} from "./base.js"; |
} from './base.js'; |
||||||
|
|
||||||
export class HealCharacterCommand extends AbstractCharacterStatusCommand { |
export class HealCharacterCommand extends AbstractCharacterStatusCommand { |
||||||
constructor(creator: SlashCreator) { |
constructor(creator: SlashCreator, opts: CharacterStatusOptions) { |
||||||
super(creator, { |
super(creator, { |
||||||
name: "heal", |
...opts, |
||||||
description: "Heals the given character(s).", |
name: 'heal', |
||||||
|
description: 'Heals the given character(s).', |
||||||
type: ApplicationCommandType.CHAT_INPUT, |
type: ApplicationCommandType.CHAT_INPUT, |
||||||
guildIDs: process.env.DEVELOPMENT_GUILD_ID, |
guildIDs: process.env.DEVELOPMENT_GUILD_ID, |
||||||
options: [ |
options: [ |
||||||
{ |
{ |
||||||
...CharacterOptionTemplate, |
...CharacterOptionTemplate, |
||||||
name: "character", |
name: 'character', |
||||||
description: "The name of the character(s) to heal.", |
description: 'The name of the character(s) to heal.', |
||||||
required: true, |
required: true |
||||||
}, |
}, |
||||||
{ |
{ |
||||||
type: CommandOptionType.INTEGER, |
type: CommandOptionType.INTEGER, |
||||||
name: "healing", |
name: 'healing', |
||||||
description: "The amount of healing to apply to the character(s).", |
description: 'The amount of healing to apply to the character(s).', |
||||||
max_value: 99, |
max_value: 99, |
||||||
min_value: 0, |
min_value: 0, |
||||||
required: true, |
required: true |
||||||
}, |
}, |
||||||
{ |
{ |
||||||
type: CommandOptionType.BOOLEAN, |
type: CommandOptionType.BOOLEAN, |
||||||
name: "stabilize", |
name: 'stabilize', |
||||||
description: "If true, repairs the unstable status of the character(s).", |
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> { |
async process( |
||||||
const healing: number = ctx.options["healing"] |
ctx: CommandContext, |
||||||
const stabilize: boolean = ctx.options["stabilize"] ?? false |
{ characters }: { characters: Map<string, readonly GameCharacterData[]> } |
||||||
const description: string[] = [] |
): Promise< |
||||||
const result: LoadedCharacterData[] = [] |
| readonly [string, LoadedCharacterData[]] |
||||||
for (const character of characters.get("character")!) { |
| 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) { |
if (!character.success) { |
||||||
description.push(`**${character.name}** healed ${healing} Harm${healing > 0 ? "!" : "."}${stabilize ? " ***Stabilized!***" : ""}`) |
description.push( |
||||||
continue |
`**${character.name}** healed ${healing} Harm${healing > 0 ? '!' : '.'}${ |
||||||
|
stabilize ? ' ***Stabilized!***' : '' |
||||||
|
}` |
||||||
|
); |
||||||
|
continue; |
||||||
} |
} |
||||||
character.newData = { |
character.newData = { |
||||||
...(character.newData ?? character.originalData) |
...(character.newData ?? character.originalData) |
||||||
} |
}; |
||||||
const wasUnstable = character.newData.unstable ?? false |
const wasUnstable = character.newData.unstable ?? false; |
||||||
const wasAlive = (character.newData.health ?? 8) > 0 |
const wasAlive = (character.newData.health ?? 8) > 0; |
||||||
character.newData.health = Math.min((character.newData.health ?? 8) + healing, 8) |
character.newData.health = Math.min((character.newData.health ?? 8) + healing, 8); |
||||||
if (stabilize) { |
if (stabilize) { |
||||||
character.newData.unstable = false |
character.newData.unstable = false; |
||||||
} |
} |
||||||
const isUnstable = character.newData.unstable ?? false |
const isUnstable = character.newData.unstable ?? false; |
||||||
const isAlive = character.newData.health > 0 |
const isAlive = character.newData.health > 0; |
||||||
description.push(`**${character.name}** healed ${healing} Harm${healing > 0 ? "!" : "."}${ |
description.push( |
||||||
!wasAlive && isAlive ? " ***Revived!***" : wasUnstable && !isUnstable ? " ***Stabilized!***" : ""}`)
|
`**${character.name}** healed ${healing} Harm${healing > 0 ? '!' : '.'}${ |
||||||
result.push(character) |
!wasAlive && isAlive ? ' ***Revived!***' : wasUnstable && !isUnstable ? ' ***Stabilized!***' : '' |
||||||
|
}` |
||||||
|
); |
||||||
|
result.push(character); |
||||||
} |
} |
||||||
return [description.join("\n"), result] |
return [description.join('\n'), result]; |
||||||
} |
} |
||||||
} |
} |
||||||
|
@ -1,66 +1,72 @@ |
|||||||
import { |
import { ApplicationCommandType, type CommandContext, CommandOptionType, type SlashCreator } from 'slash-create'; |
||||||
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 { |
import { |
||||||
AbstractCharacterStatusCommand, |
AbstractCharacterStatusCommand, |
||||||
CharacterOptionTemplate, |
CharacterOptionTemplate, |
||||||
|
type CharacterStatusOptions, |
||||||
type GameCharacterData, |
type GameCharacterData, |
||||||
type LoadedCharacterData |
type LoadedCharacterData |
||||||
} from "./base.js"; |
} from './base.js'; |
||||||
|
|
||||||
export class LuckCharacterCommand extends AbstractCharacterStatusCommand { |
export class LuckCharacterCommand extends AbstractCharacterStatusCommand { |
||||||
constructor(creator: SlashCreator) { |
constructor(creator: SlashCreator, opts: CharacterStatusOptions) { |
||||||
super(creator, { |
super(creator, { |
||||||
name: "luck", |
...opts, |
||||||
description: "Modifies the luck of the given character(s).", |
name: 'luck', |
||||||
|
description: 'Modifies the luck of the given character(s).', |
||||||
type: ApplicationCommandType.CHAT_INPUT, |
type: ApplicationCommandType.CHAT_INPUT, |
||||||
guildIDs: process.env.DEVELOPMENT_GUILD_ID, |
guildIDs: process.env.DEVELOPMENT_GUILD_ID, |
||||||
options: [ |
options: [ |
||||||
{ |
{ |
||||||
...CharacterOptionTemplate, |
...CharacterOptionTemplate, |
||||||
name: "character", |
name: 'character', |
||||||
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 |
||||||
}, |
}, |
||||||
{ |
{ |
||||||
type: CommandOptionType.INTEGER, |
type: CommandOptionType.INTEGER, |
||||||
name: "delta", |
name: 'delta', |
||||||
description: "The amount of luck to apply to the character(s) (default -1).", |
description: 'The amount of luck to apply to the character(s) (default -1).', |
||||||
max_value: 7, |
max_value: 7, |
||||||
min_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> { |
async process( |
||||||
const delta: number = ctx.options["delta"] |
ctx: CommandContext, |
||||||
const description: string[] = [] |
{ characters }: { characters: Map<string, readonly GameCharacterData[]> } |
||||||
const result: LoadedCharacterData[] = [] |
): Promise< |
||||||
for (const character of characters.get("character")!) { |
| 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) { |
if (!character.success) { |
||||||
description.push(`**${character.name}** ${delta > 0 ? "recovered" : "spent"} ${Math.abs(delta)} Luck${delta !== 0 ? "!" : "."}`) |
description.push( |
||||||
continue |
`**${character.name}** ${delta > 0 ? 'recovered' : 'spent'} ${Math.abs(delta)} Luck${delta !== 0 ? '!' : '.'}` |
||||||
|
); |
||||||
|
continue; |
||||||
} |
} |
||||||
character.newData = { |
character.newData = { |
||||||
...(character.newData ?? character.originalData) |
...(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 |
return [description.join('\n'), result]; |
||||||
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] |
|
||||||
} |
} |
||||||
} |
} |
||||||
|
@ -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 { |
import { ApplicationCommandType, type CommandContext, type SlashCreator } from 'slash-create'; |
||||||
ApplicationCommandType, |
|
||||||
type CommandContext, |
|
||||||
CommandOptionType, |
|
||||||
SlashCommand, |
|
||||||
type SlashCreator |
|
||||||
} from "slash-create"; |
|
||||||
import { |
import { |
||||||
CharacterOptionTemplate, |
CharacterOptionTemplate, |
||||||
AbstractCharacterStatusCommand, |
AbstractCharacterStatusCommand, |
||||||
type GameCharacterData, |
type GameCharacterData, |
||||||
type LoadedCharacterData |
type LoadedCharacterData, |
||||||
} from "./base.js"; |
type CharacterStatusOptions |
||||||
|
} from './base.js'; |
||||||
|
|
||||||
export class CharacterStatusCommand extends AbstractCharacterStatusCommand { |
export class CharacterStatusCommand extends AbstractCharacterStatusCommand { |
||||||
constructor(creator: SlashCreator) { |
constructor(creator: SlashCreator, opts: CharacterStatusOptions) { |
||||||
super(creator, { |
super(creator, { |
||||||
name: "status", |
...opts, |
||||||
description: "Gets the status of the given character(s).", |
name: 'status', |
||||||
|
description: 'Gets the status of the given character(s).', |
||||||
type: ApplicationCommandType.CHAT_INPUT, |
type: ApplicationCommandType.CHAT_INPUT, |
||||||
guildIDs: process.env.DEVELOPMENT_GUILD_ID, |
guildIDs: process.env.DEVELOPMENT_GUILD_ID, |
||||||
options: [ |
options: [ |
||||||
{ |
{ |
||||||
...CharacterOptionTemplate, |
...CharacterOptionTemplate, |
||||||
name: "character", |
name: 'character', |
||||||
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 |
||||||
}, |
} |
||||||
] |
] |
||||||
}); |
}); |
||||||
} |
} |
||||||
|
|
||||||
async process(ctx: CommandContext, characters: Map<string, readonly GameCharacterData[]>): Promise<readonly [string, LoadedCharacterData[]] | readonly [string, LoadedCharacterData] | readonly LoadedCharacterData[] | LoadedCharacterData | string> { |
async process( |
||||||
return characters.get("character")!.flatMap((x) => x.success ? [x] : []); |
_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 { Resvg } from '@resvg/resvg-js'; |
||||||
import {JSDOM} from "jsdom"; |
import { JSDOM } from 'jsdom'; |
||||||
|
|
||||||
|
export interface Element { |
||||||
|
remove(): void; |
||||||
|
} |
||||||
|
|
||||||
export interface GameStatus { |
export interface GameStatus { |
||||||
health: number |
health: number; |
||||||
unstable: boolean |
unstable: boolean; |
||||||
healthDelta: number |
healthDelta: number; |
||||||
experience: number |
experience: number; |
||||||
experienceDelta: number |
experienceDelta: number; |
||||||
luck: number |
luck: number; |
||||||
luckDelta: number |
luckDelta: number; |
||||||
} |
} |
||||||
|
|
||||||
export interface GameStatusWithPortrait extends GameStatus { |
export interface GameStatusWithPortrait extends GameStatus { |
||||||
portrait: Buffer |
portrait: Buffer; |
||||||
} |
} |
||||||
|
|
||||||
function removeElement(dom: Element): void { |
function removeElement(dom: Element): void { |
||||||
dom.remove() |
dom.remove(); |
||||||
} |
} |
||||||
|
|
||||||
function healthBelow(health: number): (status: GameStatus) => boolean { |
function healthBelow(health: number): (status: GameStatus) => boolean { |
||||||
return (s) => s.health < health |
return (s) => s.health < health; |
||||||
} |
} |
||||||
|
|
||||||
function luckBelow(luck: number): (status: GameStatus) => boolean { |
function luckBelow(luck: number): (status: GameStatus) => boolean { |
||||||
return (s) => s.luck < luck |
return (s) => s.luck < luck; |
||||||
} |
} |
||||||
|
|
||||||
function experienceBelow(exp: number): (status: GameStatus) => boolean { |
function experienceBelow(exp: number): (status: GameStatus) => boolean { |
||||||
return (s) => s.experience < exp |
return (s) => s.experience < exp; |
||||||
} |
} |
||||||
|
|
||||||
function healthNotEqual(health: number): (status: GameStatus) => boolean { |
function healthNotEqual(health: number): (status: GameStatus) => boolean { |
||||||
return (s) => s.health !== health |
return (s) => s.health !== health; |
||||||
} |
} |
||||||
|
|
||||||
function experienceDeltaNotIncludes(index: number): (status: GameStatus) => boolean { |
function experienceDeltaNotIncludes(index: number): (status: GameStatus) => boolean { |
||||||
return (s) => { |
return (s) => { |
||||||
if (s.experienceDelta <= 0) { |
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 { |
function luckDeltaNotIncludes(index: number): (status: GameStatus) => boolean { |
||||||
return (s) => { |
return (s) => { |
||||||
if (s.luckDelta === 0) { |
if (s.luckDelta === 0) { |
||||||
return true |
return true; |
||||||
} else if (s.luckDelta > 0) { |
} else if (s.luckDelta > 0) { |
||||||
return !((index <= s.luck) && (index > s.luck - s.luckDelta)) |
return !(index <= s.luck && index > s.luck - s.luckDelta); |
||||||
} else { |
} 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 { |
function healthDamageNotIncludes(index: number): (status: GameStatus) => boolean { |
||||||
return (s) => { |
return (s) => { |
||||||
if (s.healthDelta >= 0) { |
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 { |
function healthRecoveredNotIncludes(index: number): (status: GameStatus) => boolean { |
||||||
return (s) => { |
return (s) => { |
||||||
if (s.healthDelta <= 0) { |
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 { |
function notStableAlive(): (status: GameStatus) => boolean { |
||||||
return (status) => status.unstable || status.health <= 0 |
return (status) => status.unstable || status.health <= 0; |
||||||
} |
} |
||||||
|
|
||||||
function notUnstableAlive(): (status: GameStatus) => boolean { |
function notUnstableAlive(): (status: GameStatus) => boolean { |
||||||
return (status) => !status.unstable || status.health <= 0 |
return (status) => !status.unstable || status.health <= 0; |
||||||
} |
} |
||||||
|
|
||||||
function notDead(): (status: GameStatus) => boolean { |
function notDead(): (status: GameStatus) => boolean { |
||||||
return (status) => status.health > 0 |
return (status) => status.health > 0; |
||||||
} |
} |
||||||
|
|
||||||
function dead(): (status: GameStatus) => boolean { |
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][] = [ |
const mappings: [filter: (status: GameStatus) => boolean, selector: string, hit: (dom: Element) => void][] = [ |
||||||
[healthBelow(8), "#health8", removeElement], |
[healthBelow(8), '#health8', removeElement], |
||||||
[healthBelow(7), "#health7", removeElement], |
[healthBelow(7), '#health7', removeElement], |
||||||
[healthBelow(6), "#health6", removeElement], |
[healthBelow(6), '#health6', removeElement], |
||||||
[healthBelow(5), "#health5", removeElement], |
[healthBelow(5), '#health5', removeElement], |
||||||
[healthBelow(4), "#health4", removeElement], |
[healthBelow(4), '#health4', removeElement], |
||||||
[healthBelow(3), "#health3", removeElement], |
[healthBelow(3), '#health3', removeElement], |
||||||
[healthBelow(2), "#health2", removeElement], |
[healthBelow(2), '#health2', removeElement], |
||||||
[healthBelow(1), "#health1", removeElement], |
[healthBelow(1), '#health1', removeElement], |
||||||
|
|
||||||
[luckBelow(7), "#luck7", removeElement], |
[luckBelow(7), '#luck7', removeElement], |
||||||
[luckBelow(6), "#luck6", removeElement], |
[luckBelow(6), '#luck6', removeElement], |
||||||
[luckBelow(5), "#luck5", removeElement], |
[luckBelow(5), '#luck5', removeElement], |
||||||
[luckBelow(4), "#luck4", removeElement], |
[luckBelow(4), '#luck4', removeElement], |
||||||
[luckBelow(3), "#luck3", removeElement], |
[luckBelow(3), '#luck3', removeElement], |
||||||
[luckBelow(2), "#luck2", removeElement], |
[luckBelow(2), '#luck2', removeElement], |
||||||
[luckBelow(1), "#luck1", removeElement], |
[luckBelow(1), '#luck1', removeElement], |
||||||
|
|
||||||
[experienceBelow(5), "#experienceLevelUpReady", removeElement], |
[experienceBelow(5), '#experienceLevelUpReady', removeElement], |
||||||
[experienceBelow(5), "#experience5", removeElement], |
[experienceBelow(5), '#experience5', removeElement], |
||||||
[experienceBelow(4), "#experience4", removeElement], |
[experienceBelow(4), '#experience4', removeElement], |
||||||
[experienceBelow(3), "#experience3", removeElement], |
[experienceBelow(3), '#experience3', removeElement], |
||||||
[experienceBelow(2), "#experience2", removeElement], |
[experienceBelow(2), '#experience2', removeElement], |
||||||
[experienceBelow(1), "#experience1", removeElement], |
[experienceBelow(1), '#experience1', removeElement], |
||||||
|
|
||||||
[healthDamageNotIncludes(8), "#healthDamage8", removeElement], |
[healthDamageNotIncludes(8), '#healthDamage8', removeElement], |
||||||
[healthDamageNotIncludes(7), "#healthDamage7", removeElement], |
[healthDamageNotIncludes(7), '#healthDamage7', removeElement], |
||||||
[healthDamageNotIncludes(6), "#healthDamage6", removeElement], |
[healthDamageNotIncludes(6), '#healthDamage6', removeElement], |
||||||
[healthDamageNotIncludes(5), "#healthDamage5", removeElement], |
[healthDamageNotIncludes(5), '#healthDamage5', removeElement], |
||||||
[healthDamageNotIncludes(4), "#healthDamage4", removeElement], |
[healthDamageNotIncludes(4), '#healthDamage4', removeElement], |
||||||
[healthDamageNotIncludes(3), "#healthDamage3", removeElement], |
[healthDamageNotIncludes(3), '#healthDamage3', removeElement], |
||||||
[healthDamageNotIncludes(2), "#healthDamage2", removeElement], |
[healthDamageNotIncludes(2), '#healthDamage2', removeElement], |
||||||
[healthDamageNotIncludes(1), "#healthDamage1", removeElement], |
[healthDamageNotIncludes(1), '#healthDamage1', removeElement], |
||||||
|
|
||||||
[healthRecoveredNotIncludes(8), "#healthRecovery8", removeElement], |
[healthRecoveredNotIncludes(8), '#healthRecovery8', removeElement], |
||||||
[healthRecoveredNotIncludes(7), "#healthRecovery7", removeElement], |
[healthRecoveredNotIncludes(7), '#healthRecovery7', removeElement], |
||||||
[healthRecoveredNotIncludes(6), "#healthRecovery6", removeElement], |
[healthRecoveredNotIncludes(6), '#healthRecovery6', removeElement], |
||||||
[healthRecoveredNotIncludes(5), "#healthRecovery5", removeElement], |
[healthRecoveredNotIncludes(5), '#healthRecovery5', removeElement], |
||||||
[healthRecoveredNotIncludes(4), "#healthRecovery4", removeElement], |
[healthRecoveredNotIncludes(4), '#healthRecovery4', removeElement], |
||||||
[healthRecoveredNotIncludes(3), "#healthRecovery3", removeElement], |
[healthRecoveredNotIncludes(3), '#healthRecovery3', removeElement], |
||||||
[healthRecoveredNotIncludes(2), "#healthRecovery2", removeElement], |
[healthRecoveredNotIncludes(2), '#healthRecovery2', removeElement], |
||||||
[healthRecoveredNotIncludes(1), "#healthRecovery1", removeElement], |
[healthRecoveredNotIncludes(1), '#healthRecovery1', removeElement], |
||||||
|
|
||||||
[healthNotEqual(8), "#healthCounter8", removeElement], |
[healthNotEqual(8), '#healthCounter8', removeElement], |
||||||
[healthNotEqual(7), "#healthCounter7", removeElement], |
[healthNotEqual(7), '#healthCounter7', removeElement], |
||||||
[healthNotEqual(6), "#healthCounter6", removeElement], |
[healthNotEqual(6), '#healthCounter6', removeElement], |
||||||
[healthNotEqual(5), "#healthCounter5", removeElement], |
[healthNotEqual(5), '#healthCounter5', removeElement], |
||||||
[healthNotEqual(4), "#healthCounter4", removeElement], |
[healthNotEqual(4), '#healthCounter4', removeElement], |
||||||
[healthNotEqual(3), "#healthCounter3", removeElement], |
[healthNotEqual(3), '#healthCounter3', removeElement], |
||||||
[healthNotEqual(2), "#healthCounter2", removeElement], |
[healthNotEqual(2), '#healthCounter2', removeElement], |
||||||
[healthNotEqual(1), "#healthCounter1", removeElement], |
[healthNotEqual(1), '#healthCounter1', removeElement], |
||||||
[healthNotEqual(0), "#healthCounter0", removeElement], |
[healthNotEqual(0), '#healthCounter0', removeElement], |
||||||
|
|
||||||
[luckDeltaNotIncludes(7), "#luckShine7", removeElement], |
[luckDeltaNotIncludes(7), '#luckShine7', removeElement], |
||||||
[luckDeltaNotIncludes(6), "#luckShine6", removeElement], |
[luckDeltaNotIncludes(6), '#luckShine6', removeElement], |
||||||
[luckDeltaNotIncludes(5), "#luckShine5", removeElement], |
[luckDeltaNotIncludes(5), '#luckShine5', removeElement], |
||||||
[luckDeltaNotIncludes(4), "#luckShine4", removeElement], |
[luckDeltaNotIncludes(4), '#luckShine4', removeElement], |
||||||
[luckDeltaNotIncludes(3), "#luckShine3", removeElement], |
[luckDeltaNotIncludes(3), '#luckShine3', removeElement], |
||||||
[luckDeltaNotIncludes(2), "#luckShine2", removeElement], |
[luckDeltaNotIncludes(2), '#luckShine2', removeElement], |
||||||
[luckDeltaNotIncludes(1), "#luckShine1", removeElement], |
[luckDeltaNotIncludes(1), '#luckShine1', removeElement], |
||||||
|
|
||||||
[experienceDeltaNotIncludes(5), "#experienceUp5", removeElement], |
[experienceDeltaNotIncludes(5), '#experienceUp5', removeElement], |
||||||
[experienceDeltaNotIncludes(4), "#experienceUp4", removeElement], |
[experienceDeltaNotIncludes(4), '#experienceUp4', removeElement], |
||||||
[experienceDeltaNotIncludes(3), "#experienceUp3", removeElement], |
[experienceDeltaNotIncludes(3), '#experienceUp3', removeElement], |
||||||
[experienceDeltaNotIncludes(2), "#experienceUp2", removeElement], |
[experienceDeltaNotIncludes(2), '#experienceUp2', removeElement], |
||||||
[experienceDeltaNotIncludes(1), "#experienceUp1", removeElement], |
[experienceDeltaNotIncludes(1), '#experienceUp1', removeElement], |
||||||
|
|
||||||
[notStableAlive(), "#healthIcon", removeElement], |
[notStableAlive(), '#healthIcon', removeElement], |
||||||
[notUnstableAlive(), "#healthLowIcon", removeElement], |
[notUnstableAlive(), '#healthLowIcon', removeElement], |
||||||
[notDead(), "#healthEmptyIcon", removeElement], |
[notDead(), '#healthEmptyIcon', removeElement], |
||||||
[notDead(), "#characterDyingFace", removeElement], |
[notDead(), '#characterDyingFace', removeElement], |
||||||
[dead(), "#characterFaceImage", removeElement] |
[dead(), '#characterFaceImage', removeElement] |
||||||
] |
]; |
||||||
|
|
||||||
export async function renderStatus(svgTemplate: string, status: GameStatusWithPortrait): Promise<Buffer> { |
export async function renderStatus(svgTemplate: string, status: GameStatusWithPortrait): Promise<Buffer> { |
||||||
const dom = new JSDOM(svgTemplate, { |
const dom = new JSDOM(svgTemplate, { |
||||||
contentType: "image/svg+xml", |
contentType: 'image/svg+xml', |
||||||
pretendToBeVisual: false, |
pretendToBeVisual: false, |
||||||
includeNodeLocations: false, |
includeNodeLocations: false, |
||||||
url: "https://localhost/status.xml" |
url: 'https://localhost/status.xml' |
||||||
}) |
}); |
||||||
for (const [filter, selector, action] of mappings) { |
for (const [filter, selector, action] of mappings) { |
||||||
if (filter(status)) { |
if (filter(status)) { |
||||||
for (const el of dom.window.document.querySelectorAll(selector)) { |
for (const el of dom.window.document.querySelectorAll(selector)) { |
||||||
action(el) |
action(el); |
||||||
} |
} |
||||||
} |
} |
||||||
} |
} |
||||||
const resvg = new Resvg(dom.window.document.documentElement.outerHTML, {}) |
const resvg = new Resvg(dom.window.document.documentElement.outerHTML, {}); |
||||||
resvg.resolveImage("https://invalid.invalid/face.png", status.portrait) |
resvg.resolveImage('https://invalid.invalid/face.png', status.portrait); |
||||||
return resvg.render().asPng() |
return resvg.render().asPng(); |
||||||
} |
} |
||||||
|
Loading…
Reference in new issue