import {Resvg} from "@resvg/resvg-js"; import {JSDOM} from "jsdom"; export interface GameStatus { health: number unstable: boolean healthDelta: number experience: number experienceDelta: number luck: number luckDelta: number } export interface GameStatusWithPortrait extends GameStatus { portrait: Buffer } function removeElement(dom: Element): void { dom.remove() } function healthBelow(health: number): (status: GameStatus) => boolean { return (s) => s.health < health } function luckBelow(luck: number): (status: GameStatus) => boolean { return (s) => s.luck < luck } function experienceBelow(exp: number): (status: GameStatus) => boolean { return (s) => s.experience < exp } function healthNotEqual(health: number): (status: GameStatus) => boolean { return (s) => s.health !== health } function experienceDeltaNotIncludes(index: number): (status: GameStatus) => boolean { return (s) => { if (s.experienceDelta <= 0) { return true } 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 } else if (s.luckDelta > 0) { return !((index <= s.luck) && (index > s.luck - s.luckDelta)) } else { 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 !((index > s.health) && (index <= s.health - s.healthDelta)) } } function healthRecoveredNotIncludes(index: number): (status: GameStatus) => boolean { return (s) => { if (s.healthDelta <= 0) { return true } return !((index <= s.health) && (index > s.health - s.healthDelta)) } } function notStableAlive(): (status: GameStatus) => boolean { return (status) => status.unstable || status.health <= 0 } function notUnstableAlive(): (status: GameStatus) => boolean { return (status) => !status.unstable || status.health <= 0 } function notDead(): (status: GameStatus) => boolean { return (status) => status.health > 0 } function dead(): (status: GameStatus) => boolean { 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] ] export async function renderStatus(svgTemplate: string, status: GameStatusWithPortrait): Promise { const dom = new JSDOM(svgTemplate, { contentType: "image/svg+xml", pretendToBeVisual: false, includeNodeLocations: false, 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) } } } const resvg = new Resvg(dom.window.document.documentElement.outerHTML, {}) resvg.resolveImage("https://invalid.invalid/face.png", status.portrait) return resvg.render().asPng() }