Updates
This commit is contained in:
parent
ddcb0e9ac8
commit
26d138b6e6
3 changed files with 605 additions and 528 deletions
154
CardCreator.html
154
CardCreator.html
|
@ -28,7 +28,7 @@
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
|
|
||||||
<h2><span class="attribs"><span class="type-signature"></span></span>CardCreator<span class="signature">()</span><span class="type-signature"></span></h2>
|
<h2><span class="attribs"><span class="type-signature"></span></span>CardCreator<span class="signature">(xivApiKey<span class="signature-attributes">opt</span>)</span><span class="type-signature"></span></h2>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<h4 class="name" id="CardCreator"><span class="type-signature"></span>new CardCreator<span class="signature">()</span><span class="type-signature"></span></h4>
|
<h4 class="name" id="CardCreator"><span class="type-signature"></span>new CardCreator<span class="signature">(xivApiKey<span class="signature-attributes">opt</span>)</span><span class="type-signature"></span></h4>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,6 +60,67 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<h5>Parameters:</h5>
|
||||||
|
|
||||||
|
|
||||||
|
<table class="params">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<th>Name</th>
|
||||||
|
|
||||||
|
|
||||||
|
<th>Type</th>
|
||||||
|
|
||||||
|
|
||||||
|
<th>Attributes</th>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<th class="last">Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<td class="name"><code>xivApiKey</code></td>
|
||||||
|
|
||||||
|
|
||||||
|
<td class="type">
|
||||||
|
|
||||||
|
|
||||||
|
<span class="param-type">string</span>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
<td class="attributes">
|
||||||
|
|
||||||
|
<optional><br>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<td class="description last">The API key for the XIV API to be used in all requests.</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -93,7 +154,7 @@
|
||||||
|
|
||||||
<dt class="tag-source">Source:</dt>
|
<dt class="tag-source">Source:</dt>
|
||||||
<dd class="tag-source"><ul class="dummy"><li>
|
<dd class="tag-source"><ul class="dummy"><li>
|
||||||
<a href="create-card.js.html">create-card.js</a>, <a href="create-card.js.html#line87">line 87</a>
|
<a href="create-card.js.html">create-card.js</a>, <a href="create-card.js.html#line118">line 118</a>
|
||||||
</li></ul></dd>
|
</li></ul></dd>
|
||||||
|
|
||||||
|
|
||||||
|
@ -197,7 +258,7 @@
|
||||||
|
|
||||||
<dt class="tag-source">Source:</dt>
|
<dt class="tag-source">Source:</dt>
|
||||||
<dd class="tag-source"><ul class="dummy"><li>
|
<dd class="tag-source"><ul class="dummy"><li>
|
||||||
<a href="create-card.js.html">create-card.js</a>, <a href="create-card.js.html#line101">line 101</a>
|
<a href="create-card.js.html">create-card.js</a>, <a href="create-card.js.html#line133">line 133</a>
|
||||||
</li></ul></dd>
|
</li></ul></dd>
|
||||||
|
|
||||||
|
|
||||||
|
@ -225,7 +286,7 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<h4 class="name" id="createCard"><span class="type-signature">(async) </span>createCard<span class="signature">(charaId, customImage)</span><span class="type-signature"> → {Promise.<Buffer>}</span></h4>
|
<h4 class="name" id="createCard"><span class="type-signature">(async) </span>createCard<span class="signature">(characterId, customImage, language<span class="signature-attributes">opt</span>)</span><span class="type-signature"> → {Promise.<Buffer>}</span></h4>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -257,9 +318,13 @@
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
|
|
||||||
|
|
||||||
|
<th>Attributes</th>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<th>Default</th>
|
||||||
|
|
||||||
|
|
||||||
<th class="last">Description</th>
|
<th class="last">Description</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -269,7 +334,7 @@
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
|
|
||||||
<td class="name"><code>charaId</code></td>
|
<td class="name"><code>characterId</code></td>
|
||||||
|
|
||||||
|
|
||||||
<td class="type">
|
<td class="type">
|
||||||
|
@ -285,9 +350,21 @@
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
<td class="attributes">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<td class="default">
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
<td class="description last">The Lodestone ID of the character to generate a card for.</td>
|
<td class="description last">The Lodestone ID of the character to generate a card for.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
@ -317,9 +394,21 @@
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
<td class="attributes">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<td class="default">
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
<td class="description last">Optional parameter providing a custom
|
<td class="description last">Optional parameter providing a custom
|
||||||
image to be drawn between the background of the character card and the black information boxes.
|
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
|
The image should be the same resolution as the default image. The default image size can be
|
||||||
|
@ -328,6 +417,45 @@ or a Buffer instance.</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<td class="name"><code>language</code></td>
|
||||||
|
|
||||||
|
|
||||||
|
<td class="type">
|
||||||
|
|
||||||
|
|
||||||
|
<span class="param-type">string</span>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
<td class="attributes">
|
||||||
|
|
||||||
|
<optional><br>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<td class="default">
|
||||||
|
|
||||||
|
en
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
|
||||||
|
<td class="description last">The language that the cards should be in use for the request</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -365,7 +493,7 @@ or a Buffer instance.</td>
|
||||||
|
|
||||||
<dt class="tag-source">Source:</dt>
|
<dt class="tag-source">Source:</dt>
|
||||||
<dd class="tag-source"><ul class="dummy"><li>
|
<dd class="tag-source"><ul class="dummy"><li>
|
||||||
<a href="create-card.js.html">create-card.js</a>, <a href="create-card.js.html#line302">line 302</a>
|
<a href="create-card.js.html">create-card.js</a>, <a href="create-card.js.html#line307">line 307</a>
|
||||||
</li></ul></dd>
|
</li></ul></dd>
|
||||||
|
|
||||||
|
|
||||||
|
@ -418,15 +546,15 @@ or a Buffer instance.</td>
|
||||||
|
|
||||||
<h5>Example</h5>
|
<h5>Example</h5>
|
||||||
|
|
||||||
<pre class="prettyprint"><code>const fs = require("fs");
|
<pre class="prettyprint"><code>const fs = require('fs');
|
||||||
|
|
||||||
const card = new CardCreator();
|
const card = new CardCreator();
|
||||||
const lodestoneId = "13821878";
|
const lodestoneId = '13821878';
|
||||||
|
|
||||||
await card.ensureInit();
|
await card.ensureInit();
|
||||||
const png = await card.createCard(lodestoneId);
|
const png = await card.createCard(lodestoneId);
|
||||||
|
|
||||||
fs.writeFile("./test.png", png, err => {
|
fs.writeFile('./test.png', png, err => {
|
||||||
if (err) console.error(err);
|
if (err) console.error(err);
|
||||||
});</code></pre>
|
});</code></pre>
|
||||||
|
|
||||||
|
@ -492,7 +620,7 @@ generation methods.
|
||||||
|
|
||||||
<dt class="tag-source">Source:</dt>
|
<dt class="tag-source">Source:</dt>
|
||||||
<dd class="tag-source"><ul class="dummy"><li>
|
<dd class="tag-source"><ul class="dummy"><li>
|
||||||
<a href="create-card.js.html">create-card.js</a>, <a href="create-card.js.html#line114">line 114</a>
|
<a href="create-card.js.html">create-card.js</a>, <a href="create-card.js.html#line146">line 146</a>
|
||||||
</li></ul></dd>
|
</li></ul></dd>
|
||||||
|
|
||||||
|
|
||||||
|
@ -677,7 +805,7 @@ generation methods.
|
||||||
|
|
||||||
<dt class="tag-source">Source:</dt>
|
<dt class="tag-source">Source:</dt>
|
||||||
<dd class="tag-source"><ul class="dummy"><li>
|
<dd class="tag-source"><ul class="dummy"><li>
|
||||||
<a href="create-card.js.html">create-card.js</a>, <a href="create-card.js.html#line91">line 91</a>
|
<a href="create-card.js.html">create-card.js</a>, <a href="create-card.js.html#line123">line 123</a>
|
||||||
</li></ul></dd>
|
</li></ul></dd>
|
||||||
|
|
||||||
|
|
||||||
|
@ -713,7 +841,7 @@ generation methods.
|
||||||
<br class="clear">
|
<br class="clear">
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.7</a> on Sat Aug 21 2021 20:28:44 GMT+0000 (Coordinated Universal Time)
|
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.7</a> on Sat Nov 06 2021 20:00:23 GMT+0000 (Coordinated Universal Time)
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script> prettyPrint(); </script>
|
<script> prettyPrint(); </script>
|
||||||
|
|
|
@ -26,23 +26,25 @@
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<article>
|
<article>
|
||||||
<pre class="prettyprint source linenums"><code>const fetch = require("node-fetch");
|
<pre class="prettyprint source linenums"><code>const fetch = require('node-fetch');
|
||||||
const path = require("path");
|
const path = require('path');
|
||||||
const { createCanvas, loadImage, registerFont } = require("canvas");
|
const { createCanvas, loadImage, registerFont } = require('canvas');
|
||||||
|
|
||||||
|
const createIlvlFilter = require('./create-ilvl-filter');
|
||||||
|
|
||||||
function absolute(relativePath) {
|
function absolute(relativePath) {
|
||||||
return path.join(__dirname, relativePath);
|
return path.join(__dirname, relativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerFont(absolute('SourceSansPro-Regular.ttf'), { family: 'Source Sans Pro', style: 'Regular' });
|
registerFont(absolute('./resources/SourceSansPro-Regular.ttf'), { family: 'Source Sans Pro', style: 'Regular' });
|
||||||
registerFont(absolute('SourceSansPro-SemiBold.ttf'), { family: 'Source Sans Pro', style: 'SemiBold' });
|
registerFont(absolute('./resources/SourceSansPro-SemiBold.ttf'), { family: 'Source Sans Pro', style: 'SemiBold' });
|
||||||
|
|
||||||
const primary = "rgba(178, 214, 249, 1)";
|
const primary = 'rgba(178, 214, 249, 1)';
|
||||||
const white = "rgba(255, 255, 255,1)";
|
const white = 'rgba(255, 255, 255,1)';
|
||||||
const grey = "#868686";
|
const grey = '#868686';
|
||||||
const black = "rgba(0,0,0,0.5)";
|
const black = 'rgba(0,0,0,0.5)';
|
||||||
const copyright = '"11px "Source Sans Pro"';
|
const copyright = '11px "Source Sans Pro"';
|
||||||
const small = '"18px "Source Sans Pro"';
|
const small = '18px "Source Sans Pro"';
|
||||||
const med = '30px "Source Sans Pro"';
|
const med = '30px "Source Sans Pro"';
|
||||||
const smed = '25px "Source Sans Pro"';
|
const smed = '25px "Source Sans Pro"';
|
||||||
const large = '45px "Source Sans Pro SemiBold"';
|
const large = '45px "Source Sans Pro SemiBold"';
|
||||||
|
@ -107,13 +109,43 @@ const infoTextSmallStartY = rectStartRow3Y + infoTextStartSpacing;
|
||||||
const infoTextBigStartY = infoTextSmallStartY + 25;
|
const infoTextBigStartY = infoTextSmallStartY + 25;
|
||||||
const infoTextSpacing = 50;
|
const infoTextSpacing = 50;
|
||||||
|
|
||||||
|
const xivApiSupportedLanguages = ['en', 'ja', 'de', 'fr'];
|
||||||
|
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 {
|
class CardCreator {
|
||||||
/**
|
/**
|
||||||
* Creates a new card creator.
|
* Creates a new card creator.
|
||||||
* @constructor
|
* @constructor
|
||||||
|
* @param {string} [xivApiKey] The API key for the XIV API to be used in all requests.
|
||||||
*/
|
*/
|
||||||
constructor() {
|
constructor(xivApiKey = undefined) {
|
||||||
this.isInit = false;
|
this.xivApiKey = typeof xivApiKey === 'string' && xivApiKey !== '' ? xivApiKey : undefined;
|
||||||
|
this.initPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -140,535 +172,459 @@ class CardCreator {
|
||||||
* @returns {Promise} A promise representing the initialization state of this generator.
|
* @returns {Promise} A promise representing the initialization state of this generator.
|
||||||
*/
|
*/
|
||||||
async ensureInit() {
|
async ensureInit() {
|
||||||
if (this.isInit) {
|
if (this.initPromise == null) this.initPromise = this.init();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.init();
|
await this.initPromise;
|
||||||
this.isInit = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
var d = new Date();
|
const commonImagesPromise = Promise.all([
|
||||||
this.copyrightYear = d.getFullYear();
|
loadImage(absolute('./resources/background.png')),
|
||||||
|
loadImage(absolute('./resources/minion.png')),
|
||||||
|
loadImage(absolute('./resources/mount.png')),
|
||||||
|
loadImage(absolute('./resources/ilvl-icon.png')),
|
||||||
|
loadImage(absolute('./resources/shadow.png')),
|
||||||
|
]).then(([background, minion, mount, ilvl, shadow]) => {
|
||||||
|
this.images = {
|
||||||
|
background, minion, mount, ilvl, shadow,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
this.bgImage = await loadImage(absolute("./chara_top.png"));
|
const classJobs = [
|
||||||
|
'alchemist', 'armorer', 'blacksmith', 'carpenter', 'culinarian', 'goldsmith', 'leatherworker', 'weaver',
|
||||||
|
'botanist', 'fisher', 'miner',
|
||||||
|
'gladiator', 'paladin', 'marauder', 'warrior', 'darkknight', 'gunbreaker',
|
||||||
|
'conjurer', 'whitemage', 'scholar', 'astrologian',
|
||||||
|
'archer', 'bard', 'machinist', 'dancer',
|
||||||
|
'lancer', 'dragoon', 'pugilist', 'monk', 'rogue', 'ninja', 'samurai',
|
||||||
|
'thaumaturge', 'blackmage', 'arcanist', 'summoner', 'redmage',
|
||||||
|
'bluemage',
|
||||||
|
];
|
||||||
|
|
||||||
this.imgMinion = await loadImage(absolute("./minion.png"));
|
const classJobIconsPromise = Promise.all(
|
||||||
this.imgMount = await loadImage(absolute("./mount.png"));
|
classJobs.map(name => loadImage(absolute(`./resources/class-jobs-icons/${name}.png`)))
|
||||||
this.imgIlvl = await loadImage(absolute("./ilvl_n.png"));
|
).then(images => {
|
||||||
this.imgShadow = await loadImage(absolute("./shadow.png"));
|
this.cjIcons = {};
|
||||||
|
images.forEach((image, index) => this.cjIcons[classJobs[index]] = image);
|
||||||
|
});
|
||||||
|
|
||||||
this.imgAlchemist = await loadImage(absolute("./cj/1/alchemist.png"));
|
const jobBackgroundsPromise = Promise.all(
|
||||||
this.imgArmorer = await loadImage(absolute("./cj/1/armorer.png"));
|
Array.from({ length: 38 }, (_, index) => loadImage(absolute(`./resources/class-jobs-backgrounds/${index + 1}.png`)))
|
||||||
this.imgBlacksmith = await loadImage(absolute("./cj/1/blacksmith.png"));
|
).then(images => this.jobBackgrounds = images);
|
||||||
this.imgCarpenter = await loadImage(absolute("./cj/1/carpenter.png"));
|
|
||||||
this.imgCulinarian = await loadImage(absolute("./cj/1/culinarian.png"));
|
|
||||||
this.imgGoldsmith = await loadImage(absolute("./cj/1/goldsmith.png"));
|
|
||||||
this.imgLeatherworker = await loadImage(absolute("./cj/1/leatherworker.png"));
|
|
||||||
this.imgWeaver = await loadImage(absolute("./cj/1/weaver.png"));
|
|
||||||
|
|
||||||
this.imgBotanist = await loadImage(absolute("./cj/1/botanist.png"));
|
const ilevelFilterPromise = createIlvlFilter(this.xivApiKey).then(filterIds => this.ilvlFilterIds = filterIds);
|
||||||
this.imgFisher = await loadImage(absolute("./cj/1/fisher.png"));
|
|
||||||
this.imgMiner = await loadImage(absolute("./cj/1/miner.png"));
|
|
||||||
|
|
||||||
this.imgGladiator = await loadImage(absolute("./cj/1/gladiator.png"));
|
const minionCountPromise = fetch(`https://ffxivcollect.com/api/minions/`)
|
||||||
this.imgPaladin = await loadImage(absolute("./cj/1/paladin.png"));
|
.then(response => response.json())
|
||||||
this.imgMarauder = await loadImage(absolute("./cj/1/marauder.png"));
|
.then(data => this.minionCount = data.count);
|
||||||
this.imgWarrior = await loadImage(absolute("./cj/1/warrior.png"));
|
|
||||||
this.imgDarkKnight = await loadImage(absolute("./cj/1/darkknight.png"));
|
|
||||||
this.imgGunbreaker = await loadImage(absolute("./cj/1/gunbreaker.png"));
|
|
||||||
|
|
||||||
this.imgConjurer = await loadImage(absolute("./cj/1/conjurer.png"));
|
const mountCountPromise = fetch(`https://ffxivcollect.com/api/mounts/`)
|
||||||
this.imgWhitemage = await loadImage(absolute("./cj/1/whitemage.png"));
|
.then(response => response.json())
|
||||||
this.imgScholar = await loadImage(absolute("./cj/1/scholar.png"));
|
.then(data => this.mountCount = data.count);
|
||||||
this.imgAstrologian = await loadImage(absolute("./cj/1/astrologian.png"));
|
|
||||||
|
|
||||||
this.imgArcher = await loadImage(absolute("./cj/1/archer.png"));
|
await Promise.all([
|
||||||
this.imgBard = await loadImage(absolute("./cj/1/bard.png"));
|
commonImagesPromise,
|
||||||
this.imgMachinist = await loadImage(absolute("./cj/1/machinist.png"));
|
classJobIconsPromise,
|
||||||
this.imgDancer = await loadImage(absolute("./cj/1/dancer.png"));
|
jobBackgroundsPromise,
|
||||||
|
ilevelFilterPromise,
|
||||||
this.imgLancer = await loadImage(absolute("./cj/1/lancer.png"));
|
minionCountPromise,
|
||||||
this.imgDragoon = await loadImage(absolute("./cj/1/dragoon.png"));
|
mountCountPromise,
|
||||||
this.imgPugilist = await loadImage(absolute("./cj/1/pugilist.png"));
|
]);
|
||||||
this.imgMonk = await loadImage(absolute("./cj/1/monk.png"));
|
|
||||||
this.imgRogue = await loadImage(absolute("./cj/1/rogue.png"));
|
|
||||||
this.imgNinja = await loadImage(absolute("./cj/1/ninja.png"));
|
|
||||||
this.imgSamurai = await loadImage(absolute("./cj/1/samurai.png"));
|
|
||||||
|
|
||||||
this.imgThaumaturge = await loadImage(absolute("./cj/1/thaumaturge.png"));
|
|
||||||
this.imgBlackmage = await loadImage(absolute("./cj/1/blackmage.png"));
|
|
||||||
this.imgArcanist = await loadImage(absolute("./cj/1/arcanist.png"));
|
|
||||||
this.imgSummoner = await loadImage(absolute("./cj/1/summoner.png"));
|
|
||||||
this.imgRedmage = await loadImage(absolute("./cj/1/redmage.png"));
|
|
||||||
|
|
||||||
this.imgBluemage = await loadImage(absolute('./cj/1/bluemage.png'));
|
|
||||||
|
|
||||||
this.imgJobBg = {};
|
|
||||||
for (var i = 1; i <= 38; i++) {
|
|
||||||
this.imgJobBg[i] = await loadImage(absolute(`./cj/bg/${i}.png`));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.countMountsMinions();
|
async createCrest(crests) {
|
||||||
}
|
if (!Array.isArray(crests) || crests.length == 0) return null;
|
||||||
|
|
||||||
async countMountsMinions() {
|
|
||||||
var response = await fetch(`https://ffxivcollect.com/api/minions/`);
|
|
||||||
var data = await response.json();
|
|
||||||
|
|
||||||
this.countMinion = data.count;
|
|
||||||
|
|
||||||
var response = await fetch(`https://ffxivcollect.com/api/mounts/`);
|
|
||||||
var data = await response.json();
|
|
||||||
|
|
||||||
this.countMount = data.count;
|
|
||||||
|
|
||||||
console.log(`Refreshed counts: ${this.countMinion} - ${this.countMount}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createCrest(crestAry) {
|
|
||||||
const canvas = createCanvas(128, 128);
|
const canvas = createCanvas(128, 128);
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
if (crestAry.length == 0)
|
const crestLayers = await Promise.all(crests.map(crest => loadImage(crest)));
|
||||||
return null;
|
|
||||||
|
|
||||||
for (var i = 0; i < crestAry.length; i++) {
|
for (const layer of crestLayers) {
|
||||||
var crestLayer = await loadImage(crestAry[i]);
|
ctx.drawImage(layer, 0, 0, 128, 128);
|
||||||
ctx.drawImage(crestLayer, 0, 0, 128, 128);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var imgd = ctx.getImageData(0, 0, 128, 128),
|
const imageData = ctx.getImageData(0, 0, 128, 128);
|
||||||
pix = imgd.data,
|
const pixelData = imageData.data;
|
||||||
newColor = { r: 0, g: 0, b: 0, a: 0 };
|
|
||||||
|
|
||||||
for (var i = 0, n = pix.length; i < n; i += 4) {
|
// Iterate over all pixels, where one consists of 4 numbers: r, g, b and a
|
||||||
var r = pix[i],
|
for (let index = 0; index < pixelData.length; index += 4) {
|
||||||
g = pix[i + 1],
|
const [r, g, b] = pixelData.slice(index, index + 3);
|
||||||
b = pix[i + 2];
|
|
||||||
|
|
||||||
// If its white then change it
|
// If the pixel is a special grey, change it to be transparent (a = 0)
|
||||||
if (r == 64 && g == 64 && b == 64) {
|
if (r == 64 && g == 64 && b == 64) {
|
||||||
// Change the white to whatever.
|
pixelData[index] = 0;
|
||||||
pix[i] = newColor.r;
|
pixelData[index + 1] = 0;
|
||||||
pix[i + 1] = newColor.g;
|
pixelData[index + 2] = 0;
|
||||||
pix[i + 2] = newColor.b;
|
pixelData[index + 3] = 0;
|
||||||
pix[i + 3] = newColor.a;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.putImageData(imgd, 0, 0);
|
ctx.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemLevel(gearset) {
|
getItemLevel(gearset) {
|
||||||
var ilvl = 0;
|
let itemLevelSum = 0;
|
||||||
var cnt = 0;
|
|
||||||
var mainHandLvl = 0;
|
|
||||||
var hasOffHand = false;
|
|
||||||
|
|
||||||
for (var key in gearset) {
|
for (const [key, piece] of Object.entries(gearset)) {
|
||||||
var piece = gearset[key];
|
|
||||||
|
|
||||||
if (key == 'SoulCrystal')
|
// Exclude SoulCrystal from item level sum
|
||||||
continue;
|
if (key !== 'SoulCrystal') {
|
||||||
|
|
||||||
if (key == 'MainHand')
|
// If this item is a special one, increase the total item level by only 1
|
||||||
mainHandLvl = piece.Item.LevelItem;
|
if (this.ilvlFilterIds.includes(piece.Item.ID) == true) {
|
||||||
|
itemLevelSum += 1;
|
||||||
if (key == 'OffHand')
|
} else {
|
||||||
hasOffHand = true;
|
itemLevelSum += piece.Item.LevelItem;
|
||||||
|
}
|
||||||
ilvl += piece.Item.LevelItem;
|
}
|
||||||
cnt++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasOffHand) {
|
// If there is no OffHand, the MainHand item level counts twice
|
||||||
ilvl += mainHandLvl;
|
if (gearset.Offhand != null && typeof gearset.MainHand != 'number') {
|
||||||
cnt++;
|
const piece = gearset.MainHand;
|
||||||
|
|
||||||
|
// If this item is a special one, increase the total item level by only 1
|
||||||
|
if (this.ilvlFilterIds.includes(piece.Item.ID) == true) {
|
||||||
|
itemLevelSum += 1;
|
||||||
|
} else {
|
||||||
|
itemLevelSum += piece.Item.LevelItem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cnt == 0)
|
// Average item level computation is always for 13 items
|
||||||
return 0;
|
// Job stones are ignored
|
||||||
|
return this.pad(Math.floor(itemLevelSum / 13), 4);
|
||||||
// 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) {
|
pad(number, size) {
|
||||||
num = num.toString();
|
const string = String(number);
|
||||||
while (num.length < size) num = "0" + num;
|
const paddingCount = Math.max(size - string.length, 0);
|
||||||
return num;
|
return '0'.repeat(paddingCount) + string;
|
||||||
|
}
|
||||||
|
|
||||||
|
classOrJobIcon(classJob, unlockId, className, jobName) {
|
||||||
|
if (classJob?.UnlockedState?.ID === unlockId) return this.cjIcons[jobName];
|
||||||
|
else return this.cjIcons[className];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a character card for a character.
|
* Creates a character card for a character.
|
||||||
* @param {number | string} charaId The Lodestone ID of the character to generate a card for.
|
* @param {number | string} characterId The Lodestone ID of the character to generate a card for.
|
||||||
* @param {string | Buffer | null | undefined} customImage Optional parameter providing a custom
|
* @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.
|
* 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
|
* 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,
|
* retrieved with {@link CardCreator#canvasSize}. May be a URL, `data: `URI, a local file path,
|
||||||
* or a Buffer instance.
|
* or a Buffer instance.
|
||||||
|
* @param {string} [language] The language that the cards should be in use for the request
|
||||||
* @example
|
* @example
|
||||||
* const fs = require("fs");
|
* const fs = require('fs');
|
||||||
*
|
*
|
||||||
* const card = new CardCreator();
|
* const card = new CardCreator();
|
||||||
* const lodestoneId = "13821878";
|
* const lodestoneId = '13821878';
|
||||||
*
|
*
|
||||||
* await card.ensureInit();
|
* await card.ensureInit();
|
||||||
* const png = await card.createCard(lodestoneId);
|
* const png = await card.createCard(lodestoneId);
|
||||||
*
|
*
|
||||||
* fs.writeFile("./test.png", png, err => {
|
* fs.writeFile('./test.png', png, err => {
|
||||||
* if (err) console.error(err);
|
* if (err) console.error(err);
|
||||||
* });
|
* });
|
||||||
* @returns {Promise<Buffer>} A promise representating the construction of the card's image data.
|
* @returns {Promise<Buffer>} A promise representating the construction of the card's image data.
|
||||||
*/
|
*/
|
||||||
async createCard(charaId, customImage) {
|
async createCard(characterId, customImage, language = 'en') {
|
||||||
const characterInfoUrl = `https://xivapi.com/character/${charaId}?extended=1&data=FC,mimo`;
|
const supportedLanguage = xivApiSupportedLanguages.includes(language) ? language : 'en';
|
||||||
let response = await fetch(characterInfoUrl);
|
const strings = Object.keys(languageStrings).includes(supportedLanguage) ? languageStrings[supportedLanguage] : languageStrings.en;
|
||||||
if (!response.ok) {
|
|
||||||
|
// Request all API data as early as possible
|
||||||
|
const neededFields = [
|
||||||
|
'Character.ActiveClassJob.UnlockedState.ID', 'Character.ClassJobs.*.Level', 'Character.ClassJobs.*.UnlockedState.ID', 'Character.ClassJobsBozjan.Level', 'Character.ClassJobsElemental.Level',
|
||||||
|
'Character.DC', 'Character.FreeCompanyName', 'Character.GearSet.Gear', 'Character.GrandCompany.Company.Name', 'Character.GrandCompany.Rank.Icon', 'Character.GuardianDeity.Name',
|
||||||
|
'Character.GuardianDeity.Icon', 'Character.Name', 'Character.Portrait', 'Character.Race.Name', 'Character.Tribe.Name', 'Character.Server', 'Character.Title.Name',
|
||||||
|
'FreeCompany.Crest', 'FreeCompany.Tag', 'Minions.*.dummy', 'Mounts.*.dummy',
|
||||||
|
];
|
||||||
|
|
||||||
|
const characterInfoUrl = new URL(`https://xivapi.com/character/${encodeURIComponent(characterId)}`)
|
||||||
|
characterInfoUrl.searchParams.set('language', supportedLanguage);
|
||||||
|
characterInfoUrl.searchParams.set('extended', '1');
|
||||||
|
characterInfoUrl.searchParams.set('data', 'FC,MIMO');
|
||||||
|
characterInfoUrl.searchParams.set('columns', neededFields.join(','));
|
||||||
|
if (typeof this.xivApiKey === 'string' && this.xivApiKey !== '') url.searchParams.set('private_key', this.xivApiKey);
|
||||||
|
|
||||||
|
const dataPromise = fetch(characterInfoUrl)
|
||||||
// Retry once if the request fails
|
// Retry once if the request fails
|
||||||
response = await fetch(characterInfoUrl);
|
.then(response => response.ok ? response : fetch(characterInfoUrl))
|
||||||
}
|
.then(response => response.json());
|
||||||
|
|
||||||
const data = await response.json();
|
const customImagePromise = customImage != null ? loadImage(customImage) : Promise.resolve();
|
||||||
|
const portraitPromise = dataPromise.then(data => loadImage(data.Character.Portrait));
|
||||||
|
const deityPromise = dataPromise.then(data => loadImage(`https://xivapi.com/${data.Character.GuardianDeity.Icon}`));
|
||||||
|
const gcRankPromise = dataPromise.then(data => data.Character.GrandCompany.Company != null ? loadImage(`https://xivapi.com/${data.Character.GrandCompany.Rank.Icon}`) : null);
|
||||||
|
const fcCrestPromise = dataPromise.then(data => data.Character.FreeCompanyName != null ? this.createCrest(data.FreeCompany.Crest) : null);
|
||||||
|
|
||||||
|
// Build canvas and only await data, when actually needed
|
||||||
const canvasSize = this.canvasSize;
|
const canvasSize = this.canvasSize;
|
||||||
const canvas = createCanvas(canvasSize.width, canvasSize.height);
|
const canvas = createCanvas(canvasSize.width, canvasSize.height);
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
const portrait = await loadImage(data.Character.Portrait);
|
// Draw background
|
||||||
|
ctx.drawImage(this.images.background, 0, 0, canvasSize.width, canvasSize.height + 2);
|
||||||
|
|
||||||
ctx.drawImage(this.bgImage, 0, 0, canvasSize.width, canvasSize.height + 2);
|
// Draw custom background image
|
||||||
|
const customLoadedImage = await customImagePromise;
|
||||||
ctx.drawImage(portrait, 0, 120, 441, 600);
|
if (customLoadedImage != null) {
|
||||||
|
ctx.drawImage(customLoadedImage, 0, 0, canvasSize.width, canvasSize.height);
|
||||||
if (customImage != null) {
|
|
||||||
const bg = await loadImage(customImage);
|
|
||||||
|
|
||||||
ctx.drawImage(bg, 0, 0, canvasSize.width, canvasSize.height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.strokeStyle = white;
|
// Draw dark background boxes
|
||||||
ctx.fillStyle = black;
|
ctx.fillStyle = black;
|
||||||
ctx.beginPath();
|
ctx.fillRect(25, 10, 840, 100); // Name, Title, Server
|
||||||
// Name, Title, Server Rect
|
ctx.fillRect(rectStartX, rectStartRow2Y, rectHalfWidth, rectHeightRow2); // Mounts
|
||||||
ctx.fillRect(25, 10, 840, 100);
|
ctx.fillRect(rectStartXHalf, rectStartRow2Y, rectHalfWidth, rectHeightRow2); // Minions
|
||||||
|
ctx.fillRect(rectStartX, rectStartRow3Y, rectFullWidth, rectHeightRow3); // Character information
|
||||||
|
ctx.fillRect(rectStartX, rectStartRow4Y, rectFullWidth, rectHeightRow4); // Eureka & Bozja
|
||||||
|
ctx.fillRect(rectStartX, rectStartRow5Y, rectFullWidth, rectHeightRow5); // Classes & Jobs
|
||||||
|
ctx.restore(); ctx.save();
|
||||||
|
|
||||||
// BLU returns a null UnlockedState.ID so we can't use it to pick the job image.
|
// Draw non data dependent text
|
||||||
if (data.Character.ActiveClassJob.UnlockedState.ID != null) {
|
ctx.textAlign = 'left';
|
||||||
ctx.drawImage(this.imgJobBg[data.Character.ActiveClassJob.UnlockedState.ID], 450, 4, rectFullWidth, 110);
|
ctx.font = small;
|
||||||
} else {
|
ctx.fillStyle = primary;
|
||||||
ctx.drawImage(this.imgJobBg[36], 450, 4, rectFullWidth, 110);
|
ctx.fillText(strings.raceAndClan, 480, infoTextSmallStartY); // Race & Clan
|
||||||
|
ctx.fillText(strings.guardian, 480, infoTextSmallStartY + infoTextSpacing); // Guardian
|
||||||
|
ctx.fillText(strings.elementalLevel, 480, 425); // Elemental level
|
||||||
|
ctx.fillText(strings.resistanceRank, 480, 475); // Resistance rank
|
||||||
|
|
||||||
|
ctx.font = copyright;
|
||||||
|
ctx.fillStyle = black;
|
||||||
|
ctx.fillText(`© 2010 - ${new Date().getFullYear()} SQUARE ENIX CO., LTD. All Rights Reserved`, rectStartX, 720 - 5); // Copyright
|
||||||
|
ctx.restore(); ctx.save();
|
||||||
|
|
||||||
|
// Draw non data dependent images
|
||||||
|
ctx.drawImage(this.images.shadow, 441 - 143, 110, 170, 90); // Item level shadow
|
||||||
|
ctx.drawImage(this.images.ilvl, 441 - 92, 132, 24, 27); // Item level icon
|
||||||
|
ctx.drawImage(this.images.mount, 620, iconMountMinionY, 32, 32); // Mount icon
|
||||||
|
ctx.drawImage(this.images.minion, 834, iconMountMinionY, 19, 32); // Minion icon
|
||||||
|
|
||||||
|
// Draw non data dependent job icons
|
||||||
|
{
|
||||||
|
ctx.drawImage(this.cjIcons.darkknight, 540, jobsRowIcon1Y, 30, 30); // Darkknight
|
||||||
|
ctx.drawImage(this.cjIcons.gunbreaker, 570, jobsRowIcon1Y, 30, 30); // Gunbreaker
|
||||||
|
ctx.drawImage(this.cjIcons.scholar, 660, jobsRowIcon1Y, 30, 30); // Scholar
|
||||||
|
ctx.drawImage(this.cjIcons.astrologian, 690, jobsRowIcon1Y, 30, 30); // Astrologian
|
||||||
|
|
||||||
|
ctx.drawImage(this.cjIcons.machinist, 780, jobsRowIcon1Y, 30, 30); // Machinist
|
||||||
|
ctx.drawImage(this.cjIcons.dancer, 810, jobsRowIcon1Y, 30, 30); // Dancer
|
||||||
|
ctx.drawImage(this.cjIcons.samurai, 570, jobsRowIcon2Y, 30, 30); // Samurai
|
||||||
|
ctx.drawImage(this.cjIcons.redmage, 690, jobsRowIcon2Y, 30, 30); // Redmage
|
||||||
|
ctx.drawImage(this.cjIcons.bluemage, 780, jobsRowIcon2Y, 33, 33); // Bluemage
|
||||||
|
|
||||||
|
ctx.drawImage(this.cjIcons.carpenter, 480, jobsRowIcon3Y, 30, 30); // Carpenter
|
||||||
|
ctx.drawImage(this.cjIcons.blacksmith, 510, jobsRowIcon3Y, 30, 30); // Blacksmith
|
||||||
|
ctx.drawImage(this.cjIcons.armorer, 540, jobsRowIcon3Y, 30, 30); // Armorer
|
||||||
|
ctx.drawImage(this.cjIcons.goldsmith, 570, jobsRowIcon3Y, 30, 30); // Goldsmith
|
||||||
|
ctx.drawImage(this.cjIcons.leatherworker, 600, jobsRowIcon3Y, 30, 30); // Leatherworker
|
||||||
|
ctx.drawImage(this.cjIcons.weaver, 630, jobsRowIcon3Y, 30, 30); // Weaver
|
||||||
|
ctx.drawImage(this.cjIcons.alchemist, 660, jobsRowIcon3Y, 30, 30); // Alchemist
|
||||||
|
ctx.drawImage(this.cjIcons.culinarian, 690, jobsRowIcon3Y, 30, 30); // Culinarian
|
||||||
|
ctx.drawImage(this.cjIcons.miner, 750, jobsRowIcon3Y, 30, 30); // Miner
|
||||||
|
ctx.drawImage(this.cjIcons.botanist, 780, jobsRowIcon3Y, 30, 30); // Botanist
|
||||||
|
ctx.drawImage(this.cjIcons.fisher, 810, jobsRowIcon3Y, 30, 30); // Fisher
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.fillRect(rectStartX, rectStartRow2Y, rectHalfWidth, rectHeightRow2);
|
// Draw info from character data
|
||||||
ctx.fillRect(rectStartXHalf, rectStartRow2Y, rectHalfWidth, rectHeightRow2);
|
const { Character, FreeCompany, Mounts, Minions } = await dataPromise;
|
||||||
|
|
||||||
ctx.fillRect(rectStartX, rectStartRow3Y, rectFullWidth, rectHeightRow3); //info
|
// Header
|
||||||
ctx.fillRect(rectStartX, rectStartRow4Y, rectFullWidth, rectHeightRow4); // bozja
|
{
|
||||||
ctx.fillRect(rectStartX, rectStartRow5Y, rectFullWidth, rectHeightRow5);
|
const activeClassJob = Character.ActiveClassJob.UnlockedState.ID ?? 36; // BLU returns a null UnlockedState.ID so we can't use it to pick the job image
|
||||||
ctx.stroke();
|
ctx.drawImage(this.jobBackgrounds[activeClassJob - 1], 450, 4, rectFullWidth, 110); // Current class/job background
|
||||||
|
|
||||||
ctx.textAlign = "center";
|
ctx.textAlign = 'center';
|
||||||
ctx.font = med;
|
ctx.font = med;
|
||||||
ctx.fillStyle = primary;
|
ctx.fillStyle = primary;
|
||||||
|
if (Character.Title.Name != null) ctx.fillText(Character.Title.Name, 450, 40); // Character title
|
||||||
if (data.Character.Title.Name !== undefined)
|
|
||||||
ctx.fillText(data.Character.Title.Name, 450, 40);
|
|
||||||
|
|
||||||
ctx.font = small;
|
ctx.font = small;
|
||||||
ctx.fillText(`${data.Character.Server} (${data.Character.DC})`, 450, 100);
|
ctx.fillText(`${Character.Server} (${Character.DC})`, 450, 100); // Character service & DC
|
||||||
|
|
||||||
// Race, Clan, Guardian, GC, FC Titles
|
|
||||||
ctx.font = small;
|
|
||||||
ctx.textAlign = "left";
|
|
||||||
ctx.fillText("Race & Clan", 480, infoTextSmallStartY);
|
|
||||||
ctx.fillText("Guardian", 480, infoTextSmallStartY + infoTextSpacing);
|
|
||||||
if (data.Character.GrandCompany.Company != null) {
|
|
||||||
ctx.fillText("Grand Company", 480, infoTextSmallStartY + infoTextSpacing * 2);
|
|
||||||
}
|
|
||||||
if (data.Character.FreeCompanyName != null) {
|
|
||||||
ctx.fillText("Free Company", 480, infoTextSmallStartY + infoTextSpacing * 3);
|
|
||||||
}
|
|
||||||
ctx.fillText("Elemental Level", 480, 425);
|
|
||||||
ctx.fillText("Resistance Rank", 480, 475);
|
|
||||||
|
|
||||||
|
|
||||||
ctx.fillStyle = grey;
|
|
||||||
ctx.font = smed;
|
|
||||||
|
|
||||||
var 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.font = large;
|
||||||
|
ctx.fillStyle = white;
|
||||||
ctx.textAlign = "center";
|
ctx.fillText(Character.Name, 450, 80); // Character name
|
||||||
// Chara Name
|
ctx.restore(); ctx.save();
|
||||||
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
|
|
||||||
|
// Item level
|
||||||
|
{
|
||||||
ctx.font = smed;
|
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);
|
|
||||||
var 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, 480, infoTextBigStartY + infoTextSpacing * 2);
|
|
||||||
|
|
||||||
var gcRankIcon = await loadImage('https://xivapi.com/' + data.Character.GrandCompany.Rank.Icon);
|
|
||||||
ctx.drawImage(gcRankIcon, gcRankIconX, gcRankIconY, 40, 40);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.Character.FreeCompanyName != null) {
|
|
||||||
var 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.fillStyle = grey;
|
||||||
ctx.font = small;
|
ctx.fillText(this.getItemLevel(Character.GearSet.Gear), 441 - 65, 155); // Item level
|
||||||
ctx.fillText(`«${data.FreeCompany.Tag}»`, 480 + fcMeasure.width + 10, infoTextBigStartY + infoTextSpacing * 3);
|
ctx.restore(); ctx.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mounts & Minions
|
||||||
|
{
|
||||||
|
const mountsPercentage = Math.ceil(((Mounts.length ?? 0) / this.mountCount) * 100);
|
||||||
|
const minionsPercentage = Math.ceil(((Minions.length ?? 0) / this.minionCount) * 100);
|
||||||
|
|
||||||
ctx.font = smed;
|
ctx.font = smed;
|
||||||
ctx.fillStyle = white;
|
ctx.fillStyle = white;
|
||||||
|
const mountsMeasure = ctx.measureText(`${mountsPercentage}%`);
|
||||||
|
const minionsMeasure = ctx.measureText(`${minionsPercentage}%`);
|
||||||
|
ctx.fillText(`${mountsPercentage}%`, 480, textMountMinionY); // Mounts percentage
|
||||||
|
ctx.fillText(`${minionsPercentage}%`, 685, textMountMinionY); // Minions percentage
|
||||||
|
|
||||||
if (data.Character.ClassJobsElemental.Level != null) {
|
|
||||||
ctx.fillText(`Level ${data.Character.ClassJobsElemental.Level}`, 480, 450);
|
|
||||||
} else {
|
|
||||||
ctx.fillText(`Level 0`, 480, 450);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.Character.ClassJobsBozjan.Level != null) {
|
|
||||||
ctx.fillText(`Rank ${data.Character.ClassJobsBozjan.Level}`, 480, 500);
|
|
||||||
} else {
|
|
||||||
ctx.fillText(`Rank 0`, 480, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minion & Mount percentages
|
|
||||||
var mountsPct = '0';
|
|
||||||
if (data.Mounts !== null) {
|
|
||||||
mountsPct = Math.ceil((data.Mounts.length / this.countMount) * 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
var 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.font = small;
|
||||||
|
ctx.fillStyle = grey;
|
||||||
|
ctx.fillText(strings.mounts, 480 + mountsMeasure.width + 5, textMountMinionY); // Mounts
|
||||||
|
ctx.fillText(strings.minions, 685 + minionsMeasure.width + 5, textMountMinionY); // Minions
|
||||||
|
ctx.restore(); ctx.save();
|
||||||
|
}
|
||||||
|
|
||||||
ctx.fillText("Mounts", 480 + mountsMeasure.width + 5, textMountMinionY);
|
// Character information
|
||||||
ctx.fillText("Minions", 685 + minionsMeasure.width + 5, textMountMinionY);
|
{
|
||||||
|
ctx.font = smed;
|
||||||
|
ctx.fillStyle = white;
|
||||||
|
ctx.fillText(`${Character.Race.Name}, ${Character.Tribe.Name}`, 480, infoTextBigStartY); // Race & Clan
|
||||||
|
ctx.fillText(Character.GuardianDeity.Name, 480, infoTextBigStartY + infoTextSpacing); // Guardian
|
||||||
|
|
||||||
ctx.drawImage(this.imgMount, 620, iconMountMinionY, 32, 32);
|
if (Character.GrandCompany.Company != null) {
|
||||||
ctx.drawImage(this.imgMinion, 834, iconMountMinionY, 19, 32);
|
ctx.font = small;
|
||||||
|
ctx.fillStyle = primary;
|
||||||
|
ctx.fillText(strings.grandCompany, 480, infoTextSmallStartY + infoTextSpacing * 2); // Grand Company
|
||||||
|
|
||||||
|
ctx.font = smed;
|
||||||
|
ctx.fillStyle = white;
|
||||||
|
ctx.fillText(Character.GrandCompany.Company.Name.replace('[p]', ''), 480, infoTextBigStartY + infoTextSpacing * 2); // Grand Company name
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Character.FreeCompanyName != null) {
|
||||||
|
ctx.font = small;
|
||||||
|
ctx.fillStyle = primary;
|
||||||
|
ctx.fillText(strings.freeCompany, 480, infoTextSmallStartY + infoTextSpacing * 3); // Free Company
|
||||||
|
|
||||||
|
ctx.font = smed;
|
||||||
|
ctx.fillStyle = white;
|
||||||
|
ctx.fillText(Character.FreeCompanyName, 480, infoTextBigStartY + infoTextSpacing * 3); // Free Company name
|
||||||
|
|
||||||
|
const nameMeasure = ctx.measureText(Character.FreeCompanyName);
|
||||||
|
ctx.font = small;
|
||||||
|
ctx.fillStyle = grey;
|
||||||
|
ctx.fillText(`«${FreeCompany.Tag}»`, 480 + nameMeasure.width + 10, infoTextBigStartY + infoTextSpacing * 3); // Free Company tag
|
||||||
|
}
|
||||||
|
ctx.restore(); ctx.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eureka & Bozja
|
||||||
|
{
|
||||||
|
ctx.font = smed;
|
||||||
|
ctx.fillStyle = white;
|
||||||
|
ctx.fillText(`${strings.eurekaLevel} ${Character.ClassJobsElemental.Level ?? 0}`, 480, 450); // Elemental level
|
||||||
|
ctx.fillText(`${strings.bozjaRank} ${Character.ClassJobsBozjan.Level ?? 0}`, 480, 500); // Resistance rank
|
||||||
|
ctx.restore(); ctx.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classes & Jobs - data dependant job or class icons
|
||||||
|
{
|
||||||
|
const { ClassJobs } = Character;
|
||||||
|
ctx.drawImage(this.classOrJobIcon(ClassJobs[0], 19, 'gladiator', 'paladin'), 480, jobsRowIcon1Y, 30, 30); // Gladiator/Paladin
|
||||||
|
ctx.drawImage(this.classOrJobIcon(ClassJobs[1], 21, 'marauder', 'warrior'), 510, jobsRowIcon1Y, 30, 30); // Marauder/Warrior
|
||||||
|
ctx.drawImage(this.classOrJobIcon(ClassJobs[8], 24, 'conjurer', 'whitemage'), 630, jobsRowIcon1Y, 30, 30); // Conjurer/Whitemage
|
||||||
|
ctx.drawImage(this.classOrJobIcon(ClassJobs[11], 23, 'archer', 'bard'), 750, jobsRowIcon1Y, 30, 30); // Archer/Bard
|
||||||
|
ctx.drawImage(this.classOrJobIcon(ClassJobs[5], 22, 'lancer', 'dragoon'), 480, jobsRowIcon2Y, 30, 30); // Lancer/Dragoon
|
||||||
|
ctx.drawImage(this.classOrJobIcon(ClassJobs[4], 20, 'pugilist', 'monk'), 510, jobsRowIcon2Y, 30, 30); // Monk/Pugilist
|
||||||
|
ctx.drawImage(this.classOrJobIcon(ClassJobs[6], 30, 'rogue', 'ninja'), 540, jobsRowIcon2Y, 30, 30); // Ninja/Rogue
|
||||||
|
ctx.drawImage(this.classOrJobIcon(ClassJobs[14], 25, 'thaumaturge', 'blackmage'), 630, jobsRowIcon2Y, 30, 30); // Thaumaturge/Blackmage
|
||||||
|
ctx.drawImage(this.classOrJobIcon(ClassJobs[15], 27, 'arcanist', 'summoner'), 660, jobsRowIcon2Y, 30, 30); // Summoner/Arcanist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classes & Jobs - levels
|
||||||
|
{
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.font = small;
|
||||||
ctx.fillStyle = white;
|
ctx.fillStyle = white;
|
||||||
|
|
||||||
|
const { ClassJobs } = Character;
|
||||||
|
|
||||||
// Why are there so many fucking jobs in this game?
|
// First row
|
||||||
// Crafting
|
let rowTextX = jobsRowTextStartX;
|
||||||
ctx.textAlign = "center";
|
ctx.fillText(ClassJobs[0].Level, rowTextX, jobsRowText1Y); // Gladiator/Paladin
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
|
ctx.fillText(ClassJobs[1].Level, rowTextX, jobsRowText1Y); // Marauder/Warrior
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
|
ctx.fillText(ClassJobs[2].Level, rowTextX, jobsRowText1Y); // Darkknight
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
|
ctx.fillText(ClassJobs[3].Level, rowTextX, jobsRowText1Y); // Gunbreaker
|
||||||
|
rowTextX += jobsRowTextSpacer;
|
||||||
|
ctx.fillText(ClassJobs[8].Level, rowTextX, jobsRowText1Y); // Conjurer/Whitemage
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
|
ctx.fillText(ClassJobs[9].Level >= 30 ? ClassJobs[9].Level : '0', rowTextX, jobsRowText1Y); // Scholar
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
|
ctx.fillText(ClassJobs[10].Level, rowTextX, jobsRowText1Y); // Astrologian
|
||||||
|
rowTextX += jobsRowTextSpacer;
|
||||||
|
ctx.fillText(ClassJobs[11].Level, rowTextX, jobsRowText1Y); // Archer/Bard
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
|
ctx.fillText(ClassJobs[12].Level, rowTextX, jobsRowText1Y); // Machinist
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
|
ctx.fillText(ClassJobs[13].Level, rowTextX, jobsRowText1Y); // Dancer
|
||||||
|
|
||||||
var cJobsRowTextX = jobsRowTextStartX;
|
// Second row
|
||||||
ctx.drawImage(this.imgAlchemist, 480, jobsRowIcon3Y, 30, 30);
|
rowTextX = jobsRowTextStartX;
|
||||||
ctx.fillText(data.Character.ClassJobs[24].Level, cJobsRowTextX, jobsRowText3Y);
|
ctx.fillText(ClassJobs[5].Level, rowTextX, jobsRowText2Y); // Lancer/Dragoon
|
||||||
cJobsRowTextX += jobsRowTextSize;
|
rowTextX += jobsRowTextSize;
|
||||||
|
ctx.fillText(ClassJobs[4].Level, rowTextX, jobsRowText2Y); // Monk/Pugilist
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
|
ctx.fillText(ClassJobs[6].Level, rowTextX, jobsRowText2Y); // Ninja/Rogue
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
|
ctx.fillText(ClassJobs[7].Level, rowTextX, jobsRowText2Y); // Samurai
|
||||||
|
rowTextX += jobsRowTextSpacer;
|
||||||
|
ctx.fillText(ClassJobs[14].Level, rowTextX, jobsRowText2Y); // Thaumaturge/Blackmage
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
|
ctx.fillText(ClassJobs[15].Level, rowTextX, jobsRowText2Y); // Summoner/Arcanist
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
|
ctx.fillText(ClassJobs[16].Level, rowTextX, jobsRowText2Y); // Redmage
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
|
ctx.fillText(ClassJobs[17].Level, 796, jobsRowText2Y); // Bluemage
|
||||||
|
|
||||||
ctx.drawImage(this.imgArmorer, 510, jobsRowIcon3Y, 30, 30);
|
// Third row
|
||||||
ctx.fillText(data.Character.ClassJobs[20].Level, cJobsRowTextX, jobsRowText3Y);
|
rowTextX = jobsRowTextStartX;
|
||||||
cJobsRowTextX += jobsRowTextSize;
|
ctx.fillText(ClassJobs[18].Level, rowTextX, jobsRowText3Y); // Carpenter
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
ctx.drawImage(this.imgBlacksmith, 540, jobsRowIcon3Y, 30, 30);
|
ctx.fillText(ClassJobs[19].Level, rowTextX, jobsRowText3Y); // Blacksmith
|
||||||
ctx.fillText(data.Character.ClassJobs[19].Level, cJobsRowTextX, jobsRowText3Y);
|
rowTextX += jobsRowTextSize;
|
||||||
cJobsRowTextX += jobsRowTextSize;
|
ctx.fillText(ClassJobs[20].Level, rowTextX, jobsRowText3Y); // Armorer
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
ctx.drawImage(this.imgCarpenter, 570, jobsRowIcon3Y, 30, 30);
|
ctx.fillText(ClassJobs[21].Level, rowTextX, jobsRowText3Y); // Goldsmith
|
||||||
ctx.fillText(data.Character.ClassJobs[18].Level, cJobsRowTextX, jobsRowText3Y);
|
rowTextX += jobsRowTextSize;
|
||||||
cJobsRowTextX += jobsRowTextSize;
|
ctx.fillText(ClassJobs[22].Level, rowTextX, jobsRowText3Y); // Leatherworker
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
ctx.drawImage(this.imgCulinarian, 600, jobsRowIcon3Y, 30, 30);
|
ctx.fillText(ClassJobs[23].Level, rowTextX, jobsRowText3Y); // Weaver
|
||||||
ctx.fillText(data.Character.ClassJobs[25].Level, cJobsRowTextX, jobsRowText3Y);
|
rowTextX += jobsRowTextSize;
|
||||||
cJobsRowTextX += jobsRowTextSize;
|
ctx.fillText(ClassJobs[24].Level, rowTextX, jobsRowText3Y); // Alchemist
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
ctx.drawImage(this.imgGoldsmith, 630, jobsRowIcon3Y, 30, 30);
|
ctx.fillText(ClassJobs[25].Level, rowTextX, jobsRowText3Y); // Culinarian
|
||||||
ctx.fillText(data.Character.ClassJobs[21].Level, cJobsRowTextX, jobsRowText3Y);
|
rowTextX += jobsRowTextSpacer;
|
||||||
cJobsRowTextX += jobsRowTextSize;
|
ctx.fillText(ClassJobs[26].Level, rowTextX, jobsRowText3Y); // Miner
|
||||||
|
rowTextX += jobsRowTextSize;
|
||||||
ctx.drawImage(this.imgLeatherworker, 660, jobsRowIcon3Y, 30, 30);
|
ctx.fillText(ClassJobs[27].Level, rowTextX, jobsRowText3Y); // Botanist
|
||||||
ctx.fillText(data.Character.ClassJobs[22].Level, cJobsRowTextX, jobsRowText3Y);
|
rowTextX += jobsRowTextSize;
|
||||||
cJobsRowTextX += jobsRowTextSize;
|
ctx.fillText(ClassJobs[28].Level, rowTextX, jobsRowText3Y); // Fisher
|
||||||
|
|
||||||
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) {
|
// Remaining asynchronous drawing
|
||||||
ctx.drawImage(this.imgWarrior, 510, jobsRowIcon1Y, 30, 30);
|
{
|
||||||
} else {
|
await Promise.all([
|
||||||
ctx.drawImage(this.imgMarauder, 510, jobsRowIcon1Y, 30, 30);
|
portraitPromise.then(portrait => ctx.drawImage(portrait, 0, 120, 441, 600)),
|
||||||
|
deityPromise.then(deityIcon => ctx.drawImage(deityIcon, deityIconX, deityIconY, 28, 28)),
|
||||||
|
gcRankPromise.then(gcRankIcon => {
|
||||||
|
if (gcRankIcon != null) ctx.drawImage(gcRankIcon, gcRankIconX, gcRankIconY, 40, 40);
|
||||||
|
}),
|
||||||
|
fcCrestPromise.then(fcCrestIcon => {
|
||||||
|
if (fcCrestIcon != null) ctx.drawImage(fcCrestIcon, fcCrestX, fcCrestY, fcCrestScale, fcCrestScale);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
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();
|
return canvas.toBuffer();
|
||||||
}
|
}
|
||||||
|
@ -691,7 +647,7 @@ exports.CardCreator = CardCreator;
|
||||||
<br class="clear">
|
<br class="clear">
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.7</a> on Sat Aug 21 2021 20:28:44 GMT+0000 (Coordinated Universal Time)
|
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.7</a> on Sat Nov 06 2021 20:00:23 GMT+0000 (Coordinated Universal Time)
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script> prettyPrint(); </script>
|
<script> prettyPrint(); </script>
|
||||||
|
|
81
index.html
81
index.html
|
@ -46,61 +46,54 @@
|
||||||
<article><h1>XIV Character Cards</h1>
|
<article><h1>XIV Character Cards</h1>
|
||||||
<p><img src="https://img.shields.io/npm/v/xiv-character-cards" alt="npm Version">
|
<p><img src="https://img.shields.io/npm/v/xiv-character-cards" alt="npm Version">
|
||||||
<a href="https://xivapi.github.io/XIV-Character-Cards/"><img src="https://img.shields.io/badge/docs-JSDoc-orange" alt="Documentation"></a></p>
|
<a href="https://xivapi.github.io/XIV-Character-Cards/"><img src="https://img.shields.io/badge/docs-JSDoc-orange" alt="Documentation"></a></p>
|
||||||
<p>API to create fancy cards for FFXIV characters based on their Lodestone data, hosted at https://ffxiv-character-cards.herokuapp.com.</p>
|
<p>Library and API to create fancy cards for FFXIV characters based on their Lodestone data, powered by <a href="https://xivapi.com/">xivapi.com</a> and hosted at <a href="https://ffxiv-character-cards.herokuapp.com">https://ffxiv-character-cards.herokuapp.com</a>.</p>
|
||||||
<p><img src="https://ffxiv-character-cards.herokuapp.com/characters/id/9575452.png" alt="Demo image"></p>
|
<p><img src="https://ffxiv-character-cards.herokuapp.com/characters/id/9575452.png" alt="Demo image"></p>
|
||||||
<h2>Endpoints</h2>
|
<h2>API</h2>
|
||||||
<h3>Getting images</h3>
|
<p>All API calls support the <code>lang</code> query parameter to create a character card with information in the specified language. The supported languages are the same as <a href="https://xivapi.com/docs/Common-Features#language">xivapi.com</a>, which are English (en), Japanese (ja), German (de) and French (fr).</p>
|
||||||
<p><code>https://ffxiv-character-cards.herokuapp.com/characters/id/<LODESTONE ID>.png</code>
|
<p>E.g. a request for a german character card would look like this: <code>https://ffxiv-character-cards.herokuapp.com/characters/id/<LODESTONE ID>.png?lang=de</code></p>
|
||||||
<br>Get the PNG for a character by its Lodestone ID.</p>
|
<h3>Getting a card for a character by its Lodestone ID</h3>
|
||||||
|
<p><code>GET https://ffxiv-character-cards.herokuapp.com/characters/id/<LODESTONE ID>.png</code></p>
|
||||||
|
<h3>Getting card for a character by its world and name</h3>
|
||||||
|
<p><code>GET https://ffxiv-character-cards.herokuapp.com/characters/name/<WORLD>/<CHARACTER NAME>.png</code>
|
||||||
|
<br><strong>Note:</strong> This is considerably slower than the creation by ID, since the character has to be looked up in the Lodestone first.</p>
|
||||||
<br>
|
<br>
|
||||||
<p><code>https://ffxiv-character-cards.herokuapp.com/characters/name/<WORLD>/<CHARACTER NAME>.png</code>
|
<p>If you are using this API together with an application that requires the API to respond very quickly, like Discord, you may need to ask it to "prepare" the card image for a character beforehand. The API will reply with its status, and in case of success, the URL to the final image.
|
||||||
<br>Get the PNG for a character by its world and name.</p>
|
|
||||||
<h3>Requesting images to be cached</h3>
|
|
||||||
<p>If you are using this API together with an application that requires the API to respond very quickly, like Discord, you need to ask it to "prepare" the image for a character beforehand.</p>
|
|
||||||
<p><code>https://ffxiv-character-cards.herokuapp.com/prepare/id/<LODESTONE ID></code>
|
|
||||||
<br>Request a character image to be cached by its Lodestone ID.</p>
|
|
||||||
<br>
|
|
||||||
<p><code>https://ffxiv-character-cards.herokuapp.com/prepare/name/<WORLD>/<CHARACTER NAME></code>
|
|
||||||
<br>Request a character image to be cached by its world and name.</p>
|
|
||||||
<p>The API will reply with its status, and in case of success, the URL to the final image.
|
|
||||||
<code>{"status":"ok","url":"/characters/id/123456789.png"}</code></p>
|
<code>{"status":"ok","url":"/characters/id/123456789.png"}</code></p>
|
||||||
<h2>Using in your application</h2>
|
<h3>Requesting a card to be cached for a character by its Lodestone ID</h3>
|
||||||
<pre class="prettyprint source"><code>yarn add xiv-character-cards
|
<p><code>GET https://ffxiv-character-cards.herokuapp.com/prepare/id/<LODESTONE ID></code></p>
|
||||||
|
<h3>Requesting a card to be cached for a character by its world and name</h3>
|
||||||
|
<p><code>GET https://ffxiv-character-cards.herokuapp.com/prepare/name/<WORLD>/<CHARACTER NAME></code></p>
|
||||||
|
<h2>Library</h2>
|
||||||
|
<p>To use the card creator as a library in your Node.JS application, first install it as a dependency with:</p>
|
||||||
|
<pre class="prettyprint source lang-sh"><code>yarn add xiv-character-cards
|
||||||
# or
|
# or
|
||||||
npm i xiv-character-cards
|
npm i xiv-character-cards
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<p>You will receive a PNG-buffer for you to use in your bot or application.<br>Check <code>index.js</code> for other usage examples.</p>
|
<p>You can then instantiate the class <code>CardCreator</code> from the library, call the asynchronous <code>insureInit()</code> function to make sure all resources are loaded and then use the asynchronous <code>createCard()</code> function with your characters Lodestone ID. You will receive a promise that resolves to a <code>Buffer</code> of the PNG image of your card, that you can use in your bot or application.</p>
|
||||||
<h3>Example</h3>
|
<p>Check the <a href="https://xivapi.github.io/XIV-Character-Cards/">library documentation</a> for more details.</p>
|
||||||
|
<blockquote>
|
||||||
|
<p><strong>Note:</strong> The API server is not published as an NPM package, so if you want to host it yourself, clone the <a href="https://github.com/xivapi/XIV-Character-Cards">Github repository</a> and put the Express.JS webserver defined in the <code>index.js</code> file behind a reverse proxy.</p>
|
||||||
|
</blockquote>
|
||||||
|
<h3>Library example</h3>
|
||||||
<pre class="prettyprint source lang-js"><code>const { CardCreator } = require("xiv-character-cards");
|
<pre class="prettyprint source lang-js"><code>const { CardCreator } = require("xiv-character-cards");
|
||||||
const fs = require("fs");
|
const { writeFileSync } = require("fs");
|
||||||
|
|
||||||
const card = new CardCreator();
|
const creator = new CardCreator();
|
||||||
const lodestoneid = "13821878";
|
const lodestoneId = "13821878";
|
||||||
|
|
||||||
function example(cb) {
|
async function example() {
|
||||||
card.ensureInit()
|
await creator.ensureInit();
|
||||||
.then(
|
return creator.createCard(lodestoneId);
|
||||||
() => card.createCard(lodestoneid),
|
|
||||||
(reason) => cb("Init failed: " + reason, null)
|
|
||||||
)
|
|
||||||
.then((image) =>
|
|
||||||
cb(null, {
|
|
||||||
binary: {
|
|
||||||
image: image,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch((reason) => cb("createCard failed: " + reason, null));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
example((err, response) => {
|
example()
|
||||||
const buffer = response.binary.image;
|
.then(card => {
|
||||||
fs.writeFileSync(`./${lodestoneid}.png`, response.binary.image, (err) => {
|
writeFileSync(`./${lodestoneId}.png`, card);
|
||||||
if (err) {
|
})
|
||||||
console.log(err);
|
.catch(error => {
|
||||||
}
|
console.error('Creator initialization or card creation failed!');
|
||||||
|
console.error(error);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
</code></pre></article>
|
</code></pre></article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@ -118,7 +111,7 @@ example((err, response) => {
|
||||||
<br class="clear">
|
<br class="clear">
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.7</a> on Sat Aug 21 2021 20:28:44 GMT+0000 (Coordinated Universal Time)
|
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.7</a> on Sat Nov 06 2021 20:00:23 GMT+0000 (Coordinated Universal Time)
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script> prettyPrint(); </script>
|
<script> prettyPrint(); </script>
|
||||||
|
|
Loading…
Reference in a new issue