You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
motw-tracker/src/renderStatus.ts

194 lines
6.8 KiB

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;
}
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, fontDir: string): Promise<Buffer> {
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, {
font: {
fontDirs: [fontDir],
loadSystemFonts: false,
}
});
resvg.resolveImage('https://invalid.invalid/face.png', status.portrait);
return resvg.render().asPng();
}