Files
saddbot/utils/UpgradeRenderer.js

215 lines
7.3 KiB
JavaScript

const { createCanvas } = require('canvas');
const config = require('../config.json');
class UpgradeRenderer {
constructor() {
this.padding = 24;
this.maxWidth = 1100;
this.fontFamily = 'Sans';
this.baseColors = {
core: config.upgradeColors.core,
peace: config.upgradeColors.peace,
peaceDim: config.upgradeColors.peaceDim,
war: config.upgradeColors.war,
warDim: config.upgradeColors.warDim,
background: config.upgradeColors.background
};
this.fontSizes = { 0: 26, 1: 22, 2: 18 };
this.lineHeightFactor = 1.3;
}
/**
* Renders the upgrades data to a PNG buffer.
* @param {Object} data The data returned from torn.faction.upgrades()
* @returns {Buffer} The PNG image buffer
*/
render(data) {
const lines = this.buildLines(data);
const state = (data.state || '').toLowerCase();
const dimMode = (state === 'peace' || state === 'war') ? 'opacity' : 'desaturate';
const inactiveGroup = state === 'peace' ? 'war' : (state === 'war' ? 'peace' : null);
// Measurement Canvas
const measureCanvas = createCanvas(10, 10);
const measureCtx = measureCanvas.getContext('2d');
const visualLines = this.measureAndWrapLines(measureCtx, lines);
const { width, height } = this.calculateCanvasDimensions(visualLines);
// Final Canvas
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
this.drawBackground(ctx, width, height);
this.drawText(ctx, visualLines, width, dimMode, inactiveGroup);
return canvas.toBuffer('image/png');
}
buildLines(data) {
const lines = []; // { text: string, group: 'core'|'peace'|'war'|null }
// Core
lines.push({ text: 'Core Upgrades', group: 'core' });
const armoryNames = [];
if (data.upgrades.core.upgrades) {
for (const upgrade of data.upgrades.core.upgrades) {
if (upgrade.name && String(upgrade.name).toLowerCase().includes('armory')) {
armoryNames.push(upgrade.name.replace(/\s+armory$/i, ''));
} else {
lines.push({ text: ` ${upgrade.name} - ${upgrade.ability}`, group: 'core' });
}
}
}
if (armoryNames.length) {
lines.push({ text: ` Armory: ${armoryNames.join(', ')}`, group: 'core' });
}
lines.push({ text: '', group: null });
// Peace
lines.push({ text: 'Peace Upgrades', group: 'peace' });
if (data.upgrades.peace) {
for (const branch of data.upgrades.peace) {
lines.push({ text: ` ${branch.name}`, group: 'peace' });
for (const upgrade of branch.upgrades) {
lines.push({ text: ` ${upgrade.name} - ${upgrade.ability}`, group: 'peace' });
}
}
}
lines.push({ text: '', group: null });
// War
lines.push({ text: 'War Upgrades', group: 'war' });
if (data.upgrades.war) {
for (const branch of data.upgrades.war) {
lines.push({ text: ` ${branch.name}`, group: 'war' });
for (const upgrade of branch.upgrades) {
lines.push({ text: ` ${upgrade.name} - ${upgrade.ability}`, group: 'war' });
}
}
}
return lines;
}
measureAndWrapLines(ctx, lines) {
const visualLines = [];
const textMaxWidth = this.maxWidth - this.padding * 2;
for (const ln of lines) {
if (!ln.text) {
visualLines.push({
text: '',
group: null,
level: 0,
width: 0,
fontSize: this.fontSizes[0],
lineHeight: Math.round(this.fontSizes[0] * this.lineHeightFactor)
});
continue;
}
const leading = (ln.text.match(/^ */) || [''])[0].length;
let level = Math.min(2, Math.floor(leading / 2));
if (ln.group === 'core' && level === 1) level = 2;
const fsz = this.fontSizes[level];
ctx.font = `${fsz}px ${this.fontFamily}`;
const rawText = ln.text.trim();
const wrapped = this.wrapLine(ctx, rawText, textMaxWidth);
for (const wln of wrapped) {
const w = Math.ceil(ctx.measureText(wln).width);
visualLines.push({
text: wln,
group: ln.group,
level,
width: w,
fontSize: fsz,
lineHeight: Math.round(fsz * this.lineHeightFactor)
});
}
}
return visualLines;
}
wrapLine(ctx, text, maxW) {
const words = text.split(' ');
const wrapped = [];
let line = '';
for (const word of words) {
const test = line ? `${line} ${word}` : word;
const w = ctx.measureText(test).width;
if (w > maxW && line) {
wrapped.push(line);
line = word;
} else {
line = test;
}
}
if (line) wrapped.push(line);
return wrapped;
}
calculateCanvasDimensions(visualLines) {
let maxW = 0;
let totalH = 0;
for (const line of visualLines) {
maxW = Math.max(maxW, line.width);
totalH += line.lineHeight;
}
return {
width: Math.min(this.maxWidth, maxW + this.padding * 2),
height: this.padding * 2 + totalH
};
}
drawBackground(ctx, width, height) {
const cornerRadius = 24;
ctx.fillStyle = this.baseColors.background;
const r = Math.max(0, Math.min(cornerRadius, Math.min(width / 2, height / 2)));
ctx.beginPath();
ctx.moveTo(r, 0);
ctx.lineTo(width - r, 0);
ctx.quadraticCurveTo(width, 0, width, r);
ctx.lineTo(width, height - r);
ctx.quadraticCurveTo(width, height, width - r, height);
ctx.lineTo(r, height);
ctx.quadraticCurveTo(0, height, 0, height - r);
ctx.lineTo(0, r);
ctx.quadraticCurveTo(0, 0, r, 0);
ctx.closePath();
ctx.fill();
}
drawText(ctx, visualLines, width, dimMode, inactiveGroup) {
ctx.textBaseline = 'top';
let y = this.padding;
for (const vln of visualLines) {
ctx.font = `${vln.fontSize}px ${this.fontFamily}`;
ctx.fillStyle = this.getColorForGroup(vln.group, dimMode, inactiveGroup);
const x = Math.round((width - vln.width) / 2);
ctx.fillText(vln.text, x, y);
y += vln.lineHeight;
}
}
getColorForGroup(group, dimMode, inactiveGroup) {
if (!group) return '#ffffff';
if (group === 'core') return this.baseColors.core;
if (dimMode === 'opacity') {
if (group === inactiveGroup) return group === 'peace' ? this.baseColors.peaceDim : this.baseColors.warDim;
return group === 'peace' ? this.baseColors.peace : this.baseColors.war;
} else {
return group === 'peace' ? this.baseColors.peaceDim : this.baseColors.warDim;
}
}
}
module.exports = new UpgradeRenderer();