683 lines
25 KiB
JavaScript
683 lines
25 KiB
JavaScript
const fetch = require('node-fetch');
|
|
const path = require('path');
|
|
const { createCanvas, loadImage, registerFont } = require('canvas');
|
|
|
|
const createIlvlFilter = require('./create-ilvl-filter');
|
|
|
|
function absolute(relativePath) {
|
|
return path.join(__dirname, relativePath);
|
|
}
|
|
|
|
registerFont(absolute('./resources/SourceSansPro-Regular.ttf'), { family: 'Source Sans Pro', style: 'Regular' });
|
|
registerFont(absolute('./resources/SourceSansPro-SemiBold.ttf'), { family: 'Source Sans Pro', style: 'SemiBold' });
|
|
|
|
const primary = 'rgba(178, 214, 249, 1)';
|
|
const white = 'rgba(255, 255, 255,1)';
|
|
const grey = '#868686';
|
|
const black = 'rgba(0,0,0,0.5)';
|
|
const copyright = '11px "Source Sans Pro"';
|
|
const small = '18px "Source Sans Pro"';
|
|
const med = '30px "Source Sans Pro"';
|
|
const smed = '25px "Source Sans Pro"';
|
|
const large = '45px "Source Sans Pro SemiBold"';
|
|
|
|
const jobsRowTextStartX = 495;
|
|
const jobsRowTextSize = 30;
|
|
const jobsRowTextSpacer = jobsRowTextSize * 2;
|
|
|
|
const rectHeightRow1 = 120; // Title, Name, World
|
|
const rectHeightRow2 = 40; // Mounts, Minions
|
|
const rectHeightRow3 = 215; // Info
|
|
const rectHeightRow4 = 120; // Jobs
|
|
const rectHeightRow5 = 175; // Jobs
|
|
|
|
const rectSpacing = 8;
|
|
const rectHalfWidthSpacing = 10;
|
|
|
|
const rectFullWidth = 400;
|
|
const rectHalfWidth = (rectFullWidth / 2) - (rectHalfWidthSpacing / 2);
|
|
|
|
const rectStartX = 464;
|
|
const rectStartXHalf = rectStartX + rectHalfWidth + rectHalfWidthSpacing;
|
|
|
|
const rectStartRow1Y = 0;
|
|
const rectStartRow2Y = rectStartRow1Y + rectHeightRow1 + rectSpacing;
|
|
const rectStartRow3Y = rectStartRow2Y + rectHeightRow2 + rectSpacing;
|
|
const rectStartRow4Y = rectStartRow3Y + rectHeightRow3 + rectSpacing;
|
|
const rectStartRow5Y = rectStartRow4Y + rectHeightRow4 + rectSpacing;
|
|
|
|
const jobsStartSpacing = 10;
|
|
const jobsRowSpacing = 8;
|
|
|
|
const jobsRowIcon1Y = rectStartRow5Y + jobsStartSpacing;
|
|
const jobsRowText1Y = jobsRowIcon1Y + 45;
|
|
|
|
const jobsRowIcon2Y = jobsRowText1Y + jobsRowSpacing;
|
|
const jobsRowText2Y = jobsRowIcon2Y + 45;
|
|
|
|
const jobsRowIcon3Y = jobsRowText2Y + jobsRowSpacing;
|
|
const jobsRowText3Y = jobsRowIcon3Y + 45;
|
|
|
|
const textTitleY = rectStartRow1Y + 34;
|
|
const textServerY = rectStartRow1Y + 104;
|
|
const textNameNoTitleY = rectStartRow1Y + 59;
|
|
const textNameTitleY = rectStartRow1Y + 79;
|
|
|
|
const textMountMinionY = rectStartRow2Y + 28;
|
|
const iconMountMinionY = rectStartRow2Y + 5;
|
|
|
|
const deityIconY = rectStartRow3Y + 69;
|
|
const deityIconX = 805;
|
|
|
|
const gcRankIconY = rectStartRow3Y + 110;
|
|
const gcRankIconX = 799;
|
|
|
|
const fcCrestScale = 38;
|
|
const fcCrestY = rectStartRow3Y + 162;
|
|
const fcCrestX = 800;
|
|
|
|
const infoTextStartSpacing = 22;
|
|
const infoTextSmallStartY = rectStartRow3Y + infoTextStartSpacing;
|
|
const infoTextBigStartY = infoTextSmallStartY + 25;
|
|
const infoTextSpacing = 50;
|
|
|
|
const languageStrings = {
|
|
en: {
|
|
raceAndClan: 'Race & Clan',
|
|
guardian: 'Guardian',
|
|
grandCompany: 'Grand Company',
|
|
freeCompany: 'Free Company',
|
|
elementalLevel: 'Elemental Level',
|
|
eurekaLevel: 'Level',
|
|
resistanceRank: 'Resistance Rank',
|
|
bozjaRank: 'Rank',
|
|
mounts: 'Mounts',
|
|
minions: 'Minions',
|
|
},
|
|
de: {
|
|
raceAndClan: 'Volk & Stamm',
|
|
guardian: 'Schutzgott',
|
|
grandCompany: 'Staatliche Gesellschaft',
|
|
freeCompany: 'Freie Gesellschaft',
|
|
elementalLevel: 'Das Verbotene Land Eureka',
|
|
eurekaLevel: 'Elementarstufe',
|
|
resistanceRank: 'Bozja-Südfront',
|
|
bozjaRank: 'Widerstandsstufe',
|
|
mounts: 'Reittiere',
|
|
minions: 'Begleiter',
|
|
},
|
|
};
|
|
|
|
class CardCreator {
|
|
/**
|
|
* Creates a new card creator.
|
|
* @constructor
|
|
* @param {string} [xivApiKey] The API key for the XIV API to be used in all requests.
|
|
*/
|
|
constructor(xivApiKey = undefined) {
|
|
this.xivApiKey = typeof xivApiKey === 'string' && xivApiKey !== '' ? xivApiKey : undefined;
|
|
this.initPromise = null;
|
|
}
|
|
|
|
/**
|
|
* @typedef {Object} CardCreator~CanvasDimensions
|
|
* @property {number} width The width of the canvas.
|
|
* @property {number} height The height of the canvas.
|
|
*/
|
|
|
|
/**
|
|
* The canvas's dimensions.
|
|
* @type {CardCreator~CanvasDimensions}
|
|
*/
|
|
get canvasSize() {
|
|
return {
|
|
width: 890,
|
|
height: 720,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Ensures that the instance is ready to generate character cards.
|
|
* This function must be resolved before using character card
|
|
* generation methods.
|
|
* @returns {Promise} A promise representing the initialization state of this generator.
|
|
*/
|
|
async ensureInit() {
|
|
if (this.initPromise == null) this.initPromise = this.init();
|
|
|
|
await this.initPromise;
|
|
}
|
|
|
|
async init() {
|
|
this.copyrightYear = new Date().getFullYear();
|
|
|
|
this.bgImage = await loadImage(absolute('./resources/background.png'));
|
|
|
|
this.imgMinion = await loadImage(absolute('./resources/minion.png'));
|
|
this.imgMount = await loadImage(absolute('./resources/mount.png'));
|
|
this.imgIlvl = await loadImage(absolute('./resources/ilvl-icon.png'));
|
|
this.imgShadow = await loadImage(absolute('./resources/shadow.png'));
|
|
|
|
this.imgAlchemist = await loadImage(absolute('./resources/class-jobs-icons/alchemist.png'));
|
|
this.imgArmorer = await loadImage(absolute('./resources/class-jobs-icons/armorer.png'));
|
|
this.imgBlacksmith = await loadImage(absolute('./resources/class-jobs-icons/blacksmith.png'));
|
|
this.imgCarpenter = await loadImage(absolute('./resources/class-jobs-icons/carpenter.png'));
|
|
this.imgCulinarian = await loadImage(absolute('./resources/class-jobs-icons/culinarian.png'));
|
|
this.imgGoldsmith = await loadImage(absolute('./resources/class-jobs-icons/goldsmith.png'));
|
|
this.imgLeatherworker = await loadImage(absolute('./resources/class-jobs-icons/leatherworker.png'));
|
|
this.imgWeaver = await loadImage(absolute('./resources/class-jobs-icons/weaver.png'));
|
|
|
|
this.imgBotanist = await loadImage(absolute('./resources/class-jobs-icons/botanist.png'));
|
|
this.imgFisher = await loadImage(absolute('./resources/class-jobs-icons/fisher.png'));
|
|
this.imgMiner = await loadImage(absolute('./resources/class-jobs-icons/miner.png'));
|
|
|
|
this.imgGladiator = await loadImage(absolute('./resources/class-jobs-icons/gladiator.png'));
|
|
this.imgPaladin = await loadImage(absolute('./resources/class-jobs-icons/paladin.png'));
|
|
this.imgMarauder = await loadImage(absolute('./resources/class-jobs-icons/marauder.png'));
|
|
this.imgWarrior = await loadImage(absolute('./resources/class-jobs-icons/warrior.png'));
|
|
this.imgDarkKnight = await loadImage(absolute('./resources/class-jobs-icons/darkknight.png'));
|
|
this.imgGunbreaker = await loadImage(absolute('./resources/class-jobs-icons/gunbreaker.png'));
|
|
|
|
this.imgConjurer = await loadImage(absolute('./resources/class-jobs-icons/conjurer.png'));
|
|
this.imgWhitemage = await loadImage(absolute('./resources/class-jobs-icons/whitemage.png'));
|
|
this.imgScholar = await loadImage(absolute('./resources/class-jobs-icons/scholar.png'));
|
|
this.imgAstrologian = await loadImage(absolute('./resources/class-jobs-icons/astrologian.png'));
|
|
|
|
this.imgArcher = await loadImage(absolute('./resources/class-jobs-icons/archer.png'));
|
|
this.imgBard = await loadImage(absolute('./resources/class-jobs-icons/bard.png'));
|
|
this.imgMachinist = await loadImage(absolute('./resources/class-jobs-icons/machinist.png'));
|
|
this.imgDancer = await loadImage(absolute('./resources/class-jobs-icons/dancer.png'));
|
|
|
|
this.imgLancer = await loadImage(absolute('./resources/class-jobs-icons/lancer.png'));
|
|
this.imgDragoon = await loadImage(absolute('./resources/class-jobs-icons/dragoon.png'));
|
|
this.imgPugilist = await loadImage(absolute('./resources/class-jobs-icons/pugilist.png'));
|
|
this.imgMonk = await loadImage(absolute('./resources/class-jobs-icons/monk.png'));
|
|
this.imgRogue = await loadImage(absolute('./resources/class-jobs-icons/rogue.png'));
|
|
this.imgNinja = await loadImage(absolute('./resources/class-jobs-icons/ninja.png'));
|
|
this.imgSamurai = await loadImage(absolute('./resources/class-jobs-icons/samurai.png'));
|
|
|
|
this.imgThaumaturge = await loadImage(absolute('./resources/class-jobs-icons/thaumaturge.png'));
|
|
this.imgBlackmage = await loadImage(absolute('./resources/class-jobs-icons/blackmage.png'));
|
|
this.imgArcanist = await loadImage(absolute('./resources/class-jobs-icons/arcanist.png'));
|
|
this.imgSummoner = await loadImage(absolute('./resources/class-jobs-icons/summoner.png'));
|
|
this.imgRedmage = await loadImage(absolute('./resources/class-jobs-icons/redmage.png'));
|
|
|
|
this.imgBluemage = await loadImage(absolute('./resources/class-jobs-icons/bluemage.png'));
|
|
|
|
this.imgJobBg = {};
|
|
for (let i = 1; i <= 38; i++) {
|
|
this.imgJobBg[i] = await loadImage(absolute(`./resources/class-jobs-backgrounds/${i}.png`));
|
|
}
|
|
|
|
this.ilvlFilterIds = await createIlvlFilter(this.xivApiKey);
|
|
|
|
await this.countMountsMinions();
|
|
}
|
|
|
|
async countMountsMinions() {
|
|
const minionResponse = await fetch(`https://ffxivcollect.com/api/minions/`);
|
|
const minionData = await minionResponse.json();
|
|
|
|
this.countMinion = minionData.count;
|
|
|
|
const mountResponse = await fetch(`https://ffxivcollect.com/api/mounts/`);
|
|
const mountData = await mountResponse.json();
|
|
|
|
this.countMount = mountData.count;
|
|
|
|
console.log(`Refreshed counts: ${this.countMinion} - ${this.countMount}`);
|
|
}
|
|
|
|
async createCrest(crestAry) {
|
|
const canvas = createCanvas(128, 128);
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
if (crestAry.length == 0)
|
|
return null;
|
|
|
|
for (let i = 0; i < crestAry.length; i++) {
|
|
const crestLayer = await loadImage(crestAry[i]);
|
|
ctx.drawImage(crestLayer, 0, 0, 128, 128);
|
|
}
|
|
|
|
const imgd = ctx.getImageData(0, 0, 128, 128),
|
|
pix = imgd.data,
|
|
newColor = { r: 0, g: 0, b: 0, a: 0 };
|
|
|
|
for (let i = 0, n = pix.length; i < n; i += 4) {
|
|
const r = pix[i],
|
|
g = pix[i + 1],
|
|
b = pix[i + 2];
|
|
|
|
// If its white then change it
|
|
if (r == 64 && g == 64 && b == 64) {
|
|
// Change the white to whatever.
|
|
pix[i] = newColor.r;
|
|
pix[i + 1] = newColor.g;
|
|
pix[i + 2] = newColor.b;
|
|
pix[i + 3] = newColor.a;
|
|
}
|
|
}
|
|
|
|
ctx.putImageData(imgd, 0, 0);
|
|
|
|
return canvas;
|
|
}
|
|
|
|
getItemLevel(gearset) {
|
|
let ilvl = 0;
|
|
let cnt = 0;
|
|
let mainHandLvl = 0;
|
|
let hasOffHand = false;
|
|
|
|
for (const [key, piece] of Object.entries(gearset)) {
|
|
if (key == 'SoulCrystal')
|
|
continue;
|
|
|
|
if (key == 'MainHand')
|
|
mainHandLvl = piece.Item.LevelItem;
|
|
|
|
if (key == 'OffHand')
|
|
hasOffHand = true;
|
|
|
|
if (this.ilvlFilterIds.includes(piece.Item.ID) == true) {
|
|
ilvl += 1;
|
|
} else {
|
|
ilvl += piece.Item.LevelItem;
|
|
}
|
|
|
|
cnt++;
|
|
}
|
|
|
|
if (!hasOffHand) {
|
|
ilvl += mainHandLvl;
|
|
cnt++;
|
|
}
|
|
|
|
if (cnt == 0)
|
|
return 0;
|
|
|
|
// ilvl division is always out of 13 items
|
|
// mainhand counts twice if there's no offhand
|
|
// job stones are ignored
|
|
return this.pad(Math.floor(ilvl / 13), 4);
|
|
}
|
|
|
|
pad(num, size) {
|
|
num = num.toString();
|
|
while (num.length < size) num = '0' + num;
|
|
return num;
|
|
}
|
|
|
|
/**
|
|
* Creates a character card for a character.
|
|
* @param {number | string} charaId The Lodestone ID of the character to generate a card for.
|
|
* @param {string | Buffer | null | undefined} customImage Optional parameter providing a custom
|
|
* image to be drawn between the background of the character card and the black information boxes.
|
|
* The image should be the same resolution as the default image. The default image size can be
|
|
* retrieved with {@link CardCreator#canvasSize}. May be a URL, `data: `URI, a local file path,
|
|
* or a Buffer instance.
|
|
* @param {string} [language] The language that the cards should be in use for the request
|
|
* @example
|
|
* const fs = require('fs');
|
|
*
|
|
* const card = new CardCreator();
|
|
* const lodestoneId = '13821878';
|
|
*
|
|
* await card.ensureInit();
|
|
* const png = await card.createCard(lodestoneId);
|
|
*
|
|
* fs.writeFile('./test.png', png, err => {
|
|
* if (err) console.error(err);
|
|
* });
|
|
* @returns {Promise<Buffer>} A promise representating the construction of the card's image data.
|
|
*/
|
|
async createCard(charaId, customImage, language = 'en') {
|
|
const strings = Object.keys(languageStrings).includes(language) ? languageStrings[language] : languageStrings.en;
|
|
|
|
const characterInfoUrl = `https://xivapi.com/character/${charaId}?language=${language}&extended=1&data=FC,mimo`;
|
|
let response = await fetch(characterInfoUrl);
|
|
if (!response.ok) {
|
|
// Retry once if the request fails
|
|
response = await fetch(characterInfoUrl);
|
|
}
|
|
const data = await response.json();
|
|
|
|
const canvasSize = this.canvasSize;
|
|
const canvas = createCanvas(canvasSize.width, canvasSize.height);
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
const portrait = await loadImage(data.Character.Portrait);
|
|
|
|
ctx.drawImage(this.bgImage, 0, 0, canvasSize.width, canvasSize.height + 2);
|
|
|
|
ctx.drawImage(portrait, 0, 120, 441, 600);
|
|
|
|
if (customImage != null) {
|
|
const bg = await loadImage(customImage);
|
|
|
|
ctx.drawImage(bg, 0, 0, canvasSize.width, canvasSize.height);
|
|
}
|
|
|
|
ctx.strokeStyle = white;
|
|
ctx.fillStyle = black;
|
|
ctx.beginPath();
|
|
// Name, Title, Server Rect
|
|
ctx.fillRect(25, 10, 840, 100);
|
|
|
|
// BLU returns a null UnlockedState.ID so we can't use it to pick the job image.
|
|
if (data.Character.ActiveClassJob.UnlockedState.ID != null) {
|
|
ctx.drawImage(this.imgJobBg[data.Character.ActiveClassJob.UnlockedState.ID], 450, 4, rectFullWidth, 110);
|
|
} else {
|
|
ctx.drawImage(this.imgJobBg[36], 450, 4, rectFullWidth, 110);
|
|
}
|
|
|
|
ctx.fillRect(rectStartX, rectStartRow2Y, rectHalfWidth, rectHeightRow2);
|
|
ctx.fillRect(rectStartXHalf, rectStartRow2Y, rectHalfWidth, rectHeightRow2);
|
|
|
|
ctx.fillRect(rectStartX, rectStartRow3Y, rectFullWidth, rectHeightRow3); //info
|
|
ctx.fillRect(rectStartX, rectStartRow4Y, rectFullWidth, rectHeightRow4); // bozja
|
|
ctx.fillRect(rectStartX, rectStartRow5Y, rectFullWidth, rectHeightRow5);
|
|
ctx.stroke();
|
|
|
|
ctx.textAlign = 'center';
|
|
ctx.font = med;
|
|
ctx.fillStyle = primary;
|
|
|
|
if (data.Character.Title.Name !== undefined)
|
|
ctx.fillText(data.Character.Title.Name, 450, 40);
|
|
|
|
ctx.font = small;
|
|
ctx.fillText(`${data.Character.Server} (${data.Character.DC})`, 450, 100);
|
|
|
|
// Race, Clan, Guardian, GC, FC Titles
|
|
ctx.font = small;
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText(strings.raceAndClan, 480, infoTextSmallStartY);
|
|
ctx.fillText(strings.guardian, 480, infoTextSmallStartY + infoTextSpacing);
|
|
if (data.Character.GrandCompany.Company != null) {
|
|
ctx.fillText(strings.grandCompany, 480, infoTextSmallStartY + infoTextSpacing * 2);
|
|
}
|
|
if (data.Character.FreeCompanyName != null) {
|
|
ctx.fillText(strings.freeCompany, 480, infoTextSmallStartY + infoTextSpacing * 3);
|
|
}
|
|
ctx.fillText(strings.elementalLevel, 480, 425);
|
|
ctx.fillText(strings.resistanceRank, 480, 475);
|
|
|
|
|
|
ctx.fillStyle = grey;
|
|
ctx.font = smed;
|
|
|
|
const ilvl = this.getItemLevel(data.Character.GearSet.Gear);
|
|
ctx.drawImage(this.imgShadow, 441 - 143, 110, 170, 90);
|
|
ctx.drawImage(this.imgIlvl, 441 - 92, 132, 24, 27);
|
|
ctx.fillText(ilvl, 441 - 65, 155);
|
|
|
|
ctx.fillStyle = white;
|
|
ctx.font = large;
|
|
|
|
ctx.textAlign = 'center';
|
|
// Chara Name
|
|
if (data.Character.Title === undefined || data.Character.Title.Name == null || data.Character.Title.Name == '') {
|
|
ctx.fillText(data.Character.Name, 450, 80);
|
|
} else {
|
|
ctx.fillText(data.Character.Name, 450, 80);
|
|
}
|
|
// Race, Clan, Guardian, GC, FC Info
|
|
ctx.font = smed;
|
|
ctx.textAlign = 'left';
|
|
ctx.fillText(`${data.Character.Race.Name}, ${data.Character.Tribe.Name}`, 480, infoTextBigStartY);
|
|
|
|
ctx.fillText(data.Character.GuardianDeity.Name, 480, infoTextBigStartY + infoTextSpacing);
|
|
const deityIcon = await loadImage('https://xivapi.com/' + data.Character.GuardianDeity.Icon);
|
|
ctx.drawImage(deityIcon, deityIconX, deityIconY, 28, 28);
|
|
|
|
if (data.Character.GrandCompany.Company != null) {
|
|
ctx.fillText(data.Character.GrandCompany.Company.Name.replace('[p]', ''), 480, infoTextBigStartY + infoTextSpacing * 2);
|
|
|
|
const gcRankIcon = await loadImage('https://xivapi.com/' + data.Character.GrandCompany.Rank.Icon);
|
|
ctx.drawImage(gcRankIcon, gcRankIconX, gcRankIconY, 40, 40);
|
|
}
|
|
|
|
if (data.Character.FreeCompanyName != null) {
|
|
const crestImage = await this.createCrest(data.FreeCompany.Crest);
|
|
|
|
if (crestImage !== null)
|
|
ctx.drawImage(crestImage, fcCrestX, fcCrestY, fcCrestScale, fcCrestScale);
|
|
|
|
|
|
const fcMeasure = ctx.measureText(data.Character.FreeCompanyName);
|
|
ctx.fillText(data.Character.FreeCompanyName, 480, infoTextBigStartY + infoTextSpacing * 3);
|
|
|
|
ctx.fillStyle = grey;
|
|
ctx.font = small;
|
|
ctx.fillText(`«${data.FreeCompany.Tag}»`, 480 + fcMeasure.width + 10, infoTextBigStartY + infoTextSpacing * 3);
|
|
}
|
|
|
|
ctx.font = smed;
|
|
ctx.fillStyle = white;
|
|
|
|
if (data.Character.ClassJobsElemental.Level != null) {
|
|
ctx.fillText(`${strings.eurekaLevel} ${data.Character.ClassJobsElemental.Level}`, 480, 450);
|
|
} else {
|
|
ctx.fillText(`${strings.eurekaLevel} 0`, 480, 450);
|
|
}
|
|
|
|
if (data.Character.ClassJobsBozjan.Level != null) {
|
|
ctx.fillText(`${strings.bozjaRank} ${data.Character.ClassJobsBozjan.Level}`, 480, 500);
|
|
} else {
|
|
ctx.fillText(`${strings.bozjaRank} 0`, 480, 500);
|
|
}
|
|
|
|
// Minion & Mount percentages
|
|
let mountsPct = '0';
|
|
if (data.Mounts !== null) {
|
|
mountsPct = Math.ceil((data.Mounts.length / this.countMount) * 100);
|
|
}
|
|
|
|
let minionsPct = '0';
|
|
if (data.Minions !== null) {
|
|
minionsPct = Math.ceil((data.Minions.length / this.countMinion) * 100);
|
|
}
|
|
|
|
const mountsMeasure = ctx.measureText(`${mountsPct}%`);
|
|
const minionsMeasure = ctx.measureText(`${minionsPct}%`);
|
|
|
|
ctx.fillText(`${mountsPct}%`, 480, textMountMinionY);
|
|
ctx.fillText(`${minionsPct}%`, 685, textMountMinionY);
|
|
|
|
ctx.fillStyle = grey;
|
|
ctx.font = small;
|
|
|
|
ctx.fillText(strings.mounts, 480 + mountsMeasure.width + 5, textMountMinionY);
|
|
ctx.fillText(strings.minions, 685 + minionsMeasure.width + 5, textMountMinionY);
|
|
|
|
ctx.drawImage(this.imgMount, 620, iconMountMinionY, 32, 32);
|
|
ctx.drawImage(this.imgMinion, 834, iconMountMinionY, 19, 32);
|
|
|
|
ctx.fillStyle = white;
|
|
|
|
|
|
// Why are there so many fucking jobs in this game?
|
|
// Crafting
|
|
ctx.textAlign = 'center';
|
|
|
|
let cJobsRowTextX = jobsRowTextStartX;
|
|
ctx.drawImage(this.imgAlchemist, 480, jobsRowIcon3Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[24].Level, cJobsRowTextX, jobsRowText3Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgArmorer, 510, jobsRowIcon3Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[20].Level, cJobsRowTextX, jobsRowText3Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgBlacksmith, 540, jobsRowIcon3Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[19].Level, cJobsRowTextX, jobsRowText3Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgCarpenter, 570, jobsRowIcon3Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[18].Level, cJobsRowTextX, jobsRowText3Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgCulinarian, 600, jobsRowIcon3Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[25].Level, cJobsRowTextX, jobsRowText3Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgGoldsmith, 630, jobsRowIcon3Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[21].Level, cJobsRowTextX, jobsRowText3Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgLeatherworker, 660, jobsRowIcon3Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[22].Level, cJobsRowTextX, jobsRowText3Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgWeaver, 690, jobsRowIcon3Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[23].Level, cJobsRowTextX, jobsRowText3Y);
|
|
cJobsRowTextX += jobsRowTextSpacer;
|
|
|
|
// Gathering
|
|
ctx.drawImage(this.imgBotanist, 750, jobsRowIcon3Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[27].Level, cJobsRowTextX, jobsRowText3Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgFisher, 780, jobsRowIcon3Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[28].Level, cJobsRowTextX, jobsRowText3Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgMiner, 810, jobsRowIcon3Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[26].Level, cJobsRowTextX, jobsRowText3Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
// Tanks
|
|
cJobsRowTextX = jobsRowTextStartX;
|
|
|
|
if (data.Character.ClassJobs[0].UnlockedState.ID == 19) {
|
|
ctx.drawImage(this.imgPaladin, 480, jobsRowIcon1Y, 30, 30);
|
|
} else {
|
|
ctx.drawImage(this.imgGladiator, 480, jobsRowIcon1Y, 30, 30);
|
|
}
|
|
ctx.fillText(data.Character.ClassJobs[0].Level, cJobsRowTextX, jobsRowText1Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
if (data.Character.ClassJobs[1].UnlockedState.ID == 21) {
|
|
ctx.drawImage(this.imgWarrior, 510, jobsRowIcon1Y, 30, 30);
|
|
} else {
|
|
ctx.drawImage(this.imgMarauder, 510, jobsRowIcon1Y, 30, 30);
|
|
}
|
|
ctx.fillText(data.Character.ClassJobs[1].Level, cJobsRowTextX, jobsRowText1Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgDarkKnight, 540, jobsRowIcon1Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[2].Level, cJobsRowTextX, jobsRowText1Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgGunbreaker, 570, jobsRowIcon1Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[3].Level, cJobsRowTextX, jobsRowText1Y);
|
|
cJobsRowTextX += jobsRowTextSpacer;
|
|
|
|
// Healers
|
|
if (data.Character.ClassJobs[8].UnlockedState.ID == 24) {
|
|
ctx.drawImage(this.imgWhitemage, 630, jobsRowIcon1Y, 30, 30);
|
|
} else {
|
|
ctx.drawImage(this.imgConjurer, 630, jobsRowIcon1Y, 30, 30);
|
|
}
|
|
ctx.fillText(data.Character.ClassJobs[8].Level, cJobsRowTextX, jobsRowText1Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgScholar, 660, jobsRowIcon1Y, 30, 30);
|
|
if (data.Character.ClassJobs[9].Level >= 30) {
|
|
ctx.fillText(data.Character.ClassJobs[9].Level, cJobsRowTextX, jobsRowText1Y);
|
|
} else {
|
|
ctx.fillText('0', cJobsRowTextX, jobsRowText1Y);
|
|
}
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgAstrologian, 690, jobsRowIcon1Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[10].Level, cJobsRowTextX, jobsRowText1Y);
|
|
cJobsRowTextX += jobsRowTextSpacer;
|
|
|
|
// DPS
|
|
// Ranged
|
|
if (data.Character.ClassJobs[11].UnlockedState.ID == 23) {
|
|
ctx.drawImage(this.imgBard, 750, jobsRowIcon1Y, 30, 30);
|
|
} else {
|
|
ctx.drawImage(this.imgArcher, 750, jobsRowIcon1Y, 30, 30);
|
|
}
|
|
ctx.fillText(data.Character.ClassJobs[11].Level, cJobsRowTextX, jobsRowText1Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgMachinist, 780, jobsRowIcon1Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[12].Level, cJobsRowTextX, jobsRowText1Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgDancer, 810, jobsRowIcon1Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[13].Level, cJobsRowTextX, jobsRowText1Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
// Melee
|
|
cJobsRowTextX = jobsRowTextStartX;
|
|
|
|
if (data.Character.ClassJobs[5].UnlockedState.ID == 22) {
|
|
ctx.drawImage(this.imgDragoon, 480, jobsRowIcon2Y, 30, 30);
|
|
} else {
|
|
ctx.drawImage(this.imgLancer, 480, jobsRowIcon2Y, 30, 30);
|
|
}
|
|
ctx.fillText(data.Character.ClassJobs[5].Level, cJobsRowTextX, jobsRowText2Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
if (data.Character.ClassJobs[4].UnlockedState.ID == 20) {
|
|
ctx.drawImage(this.imgMonk, 510, jobsRowIcon2Y, 30, 30);
|
|
} else {
|
|
ctx.drawImage(this.imgPugilist, 510, jobsRowIcon2Y, 30, 30);
|
|
}
|
|
ctx.fillText(data.Character.ClassJobs[4].Level, cJobsRowTextX, jobsRowText2Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
if (data.Character.ClassJobs[6].UnlockedState.ID == 30) {
|
|
ctx.drawImage(this.imgNinja, 540, jobsRowIcon2Y, 30, 30);
|
|
} else {
|
|
ctx.drawImage(this.imgRogue, 540, jobsRowIcon2Y, 30, 30);
|
|
}
|
|
ctx.fillText(data.Character.ClassJobs[6].Level, cJobsRowTextX, jobsRowText2Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgSamurai, 570, jobsRowIcon2Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[7].Level, cJobsRowTextX, jobsRowText2Y);
|
|
cJobsRowTextX += jobsRowTextSpacer;
|
|
|
|
// Caster
|
|
if (data.Character.ClassJobs[14].UnlockedState.ID == 25) {
|
|
ctx.drawImage(this.imgBlackmage, 630, jobsRowIcon2Y, 30, 30);
|
|
} else {
|
|
ctx.drawImage(this.imgThaumaturge, 630, jobsRowIcon2Y, 30, 30);
|
|
}
|
|
ctx.fillText(data.Character.ClassJobs[14].Level, cJobsRowTextX, jobsRowText2Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
if (data.Character.ClassJobs[15].UnlockedState.ID == 27) {
|
|
ctx.drawImage(this.imgSummoner, 660, jobsRowIcon2Y, 30, 30);
|
|
} else {
|
|
ctx.drawImage(this.imgArcanist, 660, jobsRowIcon2Y, 30, 30);
|
|
}
|
|
ctx.fillText(data.Character.ClassJobs[15].Level, cJobsRowTextX, jobsRowText2Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
ctx.drawImage(this.imgRedmage, 690, jobsRowIcon2Y, 30, 30);
|
|
ctx.fillText(data.Character.ClassJobs[16].Level, cJobsRowTextX, jobsRowText2Y);
|
|
cJobsRowTextX += jobsRowTextSize;
|
|
|
|
// Limited
|
|
ctx.drawImage(this.imgBluemage, 780, jobsRowIcon2Y, 33, 33);
|
|
ctx.fillText(data.Character.ClassJobs[17].Level, 796, jobsRowText2Y);
|
|
|
|
ctx.textAlign = 'left';
|
|
ctx.fillStyle = black;
|
|
ctx.font = copyright;
|
|
|
|
ctx.fillText(`© 2010 - ${this.copyrightYear} SQUARE ENIX CO., LTD. All Rights Reserved`, rectStartX, 720 - 5);
|
|
|
|
return canvas.toBuffer();
|
|
}
|
|
}
|
|
|
|
exports.CardCreator = CardCreator;
|