This commit is contained in:
Duda 2022-10-24 01:35:40 -07:00
parent 33afc8084e
commit 00c8a6c97f
20 changed files with 5527 additions and 1 deletions

View file

@ -0,0 +1,41 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
const pino = require('express-pino-logger')();
var cookieParser = require('cookie-parser');
var logger = require('morgan');
// var indexRouter = require('./routes/index');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(pino);
app.use('/', require('./routes/index'));
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;

View file

@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('exportion:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

View file

@ -0,0 +1,13 @@
const mysql = require("mysql");
//Database connection
const pool = mysql.createPool({
connectionLimit : 7,
host : 'localhost',
user : 'root',
password : 'root',
database : 'wahapedia',
charset : "utf8mb4_unicode_ci"
});
module.exports = pool;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,23 @@
{
"name": "exportion",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "nodemon ./bin/www"
},
"dependencies": {
"body-parser": "^1.19.0",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"express": "~4.16.1",
"express-pino-logger": "^4.0.0",
"http-errors": "~1.6.3",
"jade": "~1.11.0",
"morgan": "~1.9.1",
"mysql": "^2.18.1",
"nodemon": "^1.19.4",
"pino-colada": "^1.6.0",
"sqlstring": "^2.3.1",
"string-similarity": "^4.0.4"
}
}

View file

@ -0,0 +1,794 @@
// TODO: Error reporting
// TODO: Fix incrementation guess
const subFacNames = {
AS: 'Order',
AdM: 'Forge World',
AE: 'Craftworld',
AM: 'Regiment',
CD: 'Allegiance',
QT: 'Dread Household', // Questor Traitoris
CSM: 'Legion',
DG: 'Plague Company',
DRU: 'Kabal', // Wych Cult, Haemunculous Coven
GC: 'Cult',
GK: 'Brotherhood',
QI: 'Noble Household', // Questor Allegiance
NEC: 'Dynasty',
ORK: 'Clan',
SM: 'Chapter',
TAU: 'Sept',
TS: 'Great Cult',
TYR: 'Hive Fleet',
}
processInfo = (data,factionKey) => {
let factionName = data.factions.find(faction => faction.faction_id == factionKey).name
let info = {
name: factionName,
game: 'Warhammer 40,000',
genre: 'sci-fi',
publisher: 'GW',
url: 'https://warhammer40000.com/',
notes: 'This manifest is provided for the purposes of testing the features of *Rosterizer* and is not intended for distribution.',
revision: '0.0.1',
dependencies: [
{
slug: "123456",
name: "40k9e",
game: "Warhammer 40,000"
}
],
manifest: {},
}
return info
}
processItems = (data) => {
let assetCatalog = {'Roster§Army': {}};
processModels(data,assetCatalog);
processAbilities(data,assetCatalog);
processWargear(data,assetCatalog);
processPsychicPowers(data,assetCatalog);
processWarlordTraits(data,assetCatalog);
processUnits(data,assetCatalog);
return assetCatalog
}
processClasses = data => {
let assetTaxonomy = {};
processFactions(data,assetTaxonomy);
processPsychicClasses(data,assetTaxonomy);
return assetTaxonomy
}
processModels = (data,assetCatalog) => {
data.models.forEach((model,i,a) => {
a[i].duplicated = data.models.filter(dataModel => dataModel.name === a[i].name).length - 1
a[i].itemKey = 'Model§' + a[i].name;
if(a[i].duplicated){
let unitName = data.datasheets.filter(datasheet => datasheet.datasheet_id == model.datasheet_id)[0].name;
a[i].itemKey += ` (${unitName})`;
}
});
let modelList = Array.from(new Set(data.models.map(model => model.itemKey)));
modelList.forEach(modelItemKey => {
let dupModels = data.models.filter(model => model.itemKey === modelItemKey);
let tempItem = dedupModels(dupModels);
if(dupModels[0].duplicated){
tempItem.rules = {
'rename me': {
evals:[],
failState: 'pass',
evaluate: 'AND',
actions: [
{
paths: [
[
'{self}',
'designation'
]
],
actionType: 'set',
value: dupModels[0].name,
iterations: 1
}
]
}
}
}
assetCatalog[modelItemKey] = tempItem;
// console.log(modelItemKey)
let tempStatline = JSON.parse(JSON.stringify(tempItem));
delete tempStatline.stats.Points;
assetCatalog[modelItemKey.replace('Model§','Statline§')] = tempStatline;
});
}
dedupModels = (dupModels) => {
let deduped = dupModels[0];
let props = ['attacks','ballistic_skill','base_size','cost','leadership','movement','save','strength','toughness','weapon_skill','wounds'];
props.forEach(prop => {
let arr = dupModels.map(model => model[prop]);
deduped[prop] = findMode(arr);
});
return {
stats: {
Points: {value: deduped.cost},
M: {value: deduped.movement},
WS: {value: deduped.weapon_skill},
BS: {value: deduped.ballistic_skill},
S: {value: deduped.strength},
T: {value: deduped.toughness},
W: {value: deduped.wounds},
A: {value: deduped.attacks},
Ld: {value: deduped.leadership},
Sv: {value: deduped.save},
Base: {value: deduped.base_size},
}
}
}
findMode = (arr) => {
if (arr.length == 0) return null;
var modeMap = {},
maxEl = [arr[0]],
maxCount = 1;
for (var i = 0; i < arr.length; i++) {
var el = arr[i];
if (modeMap[el] == null) modeMap[el] = 1;
else modeMap[el]++;
if (modeMap[el] > maxCount) {
maxEl = [el];
maxCount = modeMap[el];
} else if (modeMap[el] == maxCount) {
maxEl.push(el);
maxCount = modeMap[el];
}
}
// console.log(maxEl)
let val = maxEl.sort((a,b) => {
if(typeof a === 'string' && typeof b === 'string') return b.localeCompare(a);
else if(typeof a === 'number' && typeof b === 'number') return a-b
})[0];
return val
}
ordinalize = (n,keepNumber = true) => {
const ordinals = ['th','st','nd','rd'];
let v = n % 100;
return (keepNumber?n:'') + (ordinals[(v-20)%10]||ordinals[v]||ordinals[0]);
}
processAbilities = (data,assetCatalog) => {
data.abilities.abilities.forEach((ability,i,a) => {
// console.log(ability.name)
// console.log(formatText(ability.description))
let shouldLog = false;// = ability.name === 'Tactical Precision (Aura)';
let tempAbility = {text: formatText(ability.description,shouldLog)};
let itemKey;
if(!ability.is_other_wargear){
itemKey = 'Ability§' + ability.name;
if(!assetCatalog[itemKey]) assetCatalog[itemKey] = tempAbility;
else assetCatalog[itemKey].text += '\n\nERROR: The following text was found on another ability with the same name.\n\n' + formatText(ability.description,shouldLog);
}else{
itemKey = 'Wargear§' + ability.name;
let abilityCostArr = data.abilities.datasheets_abilities.filter(datasheets_ability => datasheets_ability.ability_id === ability.ability_id).map(datasheets_ability => datasheets_ability.cost);
let costMode = findMode(abilityCostArr);
if(costMode) tempAbility.stats = {Points: {value: costMode}};
if(!assetCatalog[itemKey]) assetCatalog[itemKey] = tempAbility;
else assetCatalog[itemKey].text += '\n\nERROR: The following text was found on another wargear with the same name.\n\n' + formatText(ability.description,shouldLog);
}
let subFactTest = new RegExp(`&lt;${data.factCurrent}&gt;`, 'gi');
if(subFactTest.test(assetCatalog[itemKey].text)){
let ruleText = assetCatalog[itemKey].text.replace(subFactTest,'{v}')
assetCatalog[itemKey].text = '';
// console.log(ruleText)
assetCatalog[itemKey].stats = assetCatalog[itemKey].stats || {};
assetCatalog[itemKey].stats[data.factCurrent] = {
statType: 'term',
value: `&lt;${data.factCurrent.toUpperCase()}&gt;`,
text: ruleText,
visibility: 'hidden',
dynamic: true
}
}
a[i].itemKey = itemKey;
data.text = data.text || '';
data.text += assetCatalog[itemKey].text;
});
data.abilities.composed = [];
data.abilities.datasheets_abilities.forEach((element) => {
data.abilities.composed.push({
...element,
...data.abilities.abilities.find(e=>e.ability_id===element.ability_id)
});
});
}
processWargear = (data,assetCatalog) => {
data.wargear.wargear_list.forEach(wargear => {
let weapName = wargear.name.replace(/(1: |2: |3: )/,'').replace(/в/g,'d');
let tempWeapon = {stats:{
AP: {value: wargear.armor_piercing},
D: {value: wargear.damage},
S: {value: wargear.strength},
Type: {value: wargear.type},
Range: {value: wargear.weapon_range},
}};
if(wargear.abilities) tempWeapon.text = formatText(wargear.abilities);
let wargearArr = data.wargear.wargear_list.filter(wargear_list => wargear_list.wargear_id == wargear.wargear_id).map(wargear => 'Weapon§' + wargear.name);
let costArr = data.wargear.datasheets_wargear.filter(datasheets_wargear => datasheets_wargear.wargear_id == wargear.wargear_id).map(wargear => wargear.cost);
let cost = findMode(costArr);
// console.log(wargear.wargear_id,cost,costArr)
if(wargearArr.length == 1 && cost){
tempWeapon.stats.Points = {value: cost};
}
assetCatalog['Weapon§' + weapName] = tempWeapon;
if(wargearArr.length > 1){
// console.log(wargearArr,weapName)
let itemKey = 'Wargear§' + data.wargear.wargear.filter(gear => gear.wargear_id == wargear.wargear_id)[0].name;
assetCatalog[itemKey] = assetCatalog[itemKey] || {};
assetCatalog[itemKey].assets = assetCatalog[itemKey].assets || {};
assetCatalog[itemKey].assets.traits = assetCatalog[itemKey].assets.traits || [];
assetCatalog[itemKey].assets.traits.push('Weapon§' + weapName);
if(cost){
assetCatalog[itemKey].stats = {Points: {value: cost}};
}
}
});
let shootingMelee = data.wargear.wargear_list.filter(wargear => wargear.name.includes('(shooting)') || wargear.name.includes('(melee)'));
let shooting = shootingMelee.filter(wargear => wargear.name.includes('(shooting)'));
let melee = shootingMelee.filter(wargear => wargear.name.includes('(melee)'));
shooting.forEach(shooter => {
let bareName = shooter.name.replace(' (shooting)','');
if(melee.filter(meleer => meleer.name.includes(bareName))){
assetCatalog['Wargear§'+bareName] = {
assets: {traits:[
'Weapon§'+bareName+' (melee)',
'Weapon§'+bareName+' (shooting)',
]}
}
if(assetCatalog['Weapon§'+shooter.name].stats?.Points?.value){
assetCatalog['Wargear§'+bareName].stats = assetCatalog['Wargear§'+bareName].stats || {};
assetCatalog['Wargear§'+bareName].stats.Points = assetCatalog['Wargear§'+bareName].stats.Points || {};
assetCatalog['Wargear§'+bareName].stats.Points.value = assetCatalog['Weapon§'+shooter.name].stats.Points.value;
delete assetCatalog['Weapon§'+shooter.name].stats;
delete assetCatalog['Weapon§'+bareName+' (melee)'].stats;
}
}
})
data.wargear.composed = [];
data.wargear.datasheets_wargear.forEach((wargear) => {
let wargearArr = data.wargear.wargear_list.filter(wargear_list => wargear_list.wargear_id == wargear.wargear_id);
let thisWargear = data.wargear.wargear.find(e=>e.wargear_id===wargear.wargear_id);
data.wargear.composed.push({
...wargear,
...thisWargear,
itemKey: (wargearArr.length == 1 ? 'Weapon§' : 'Wargear§') + thisWargear.name
});
});
}
createWargearStat = (i,wargearArr,modelLoadout,assetCatalog) => {
var stringSimilarity = require('string-similarity');
let bestMatchIndex = stringSimilarity.findBestMatch(modelLoadout[i]?.toLowerCase() || '',wargearArr.map(gear => gear.itemKey.toLowerCase())).bestMatchIndex;
let current = i < modelLoadout.length ? wargearArr[bestMatchIndex].itemKey.split('§')[1] : '-';
let tempStat = {
value: current,
label: 'Loadout',
statType: 'rank',
statOrder: i+1,
group: 'Loadout',
groupOrder: 2,
ranks: {
'-': {order: 0},
},
visibility: 'active',
dynamic: true
}
wargearArr.forEach((wargear,i) => {
let wargearName = wargear.itemKey.split('§')[1];
let actualTrait = assetCatalog[wargear.itemKey];
let assignedTrait = (actualTrait.stats?.Points?.value === undefined && !wargear.cost) || actualTrait.stats?.Points?.value == wargear.cost ? wargear.itemKey : {
item: wargear.itemKey,
stats: {Points: {value: Number(wargear.cost)}}
}
tempStat.ranks[wargearName] = {
order: i+1,
traits: [{trait: assignedTrait}],
}
});
return tempStat
}
processPsychicPowers = (data,assetCatalog) => {
data.psychicPowers.forEach(power => {
if(power.type){
let powerName = power.type + '§' + power.name.toLowerCase().split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
let tempPower = {
text: formatText(power.description)
};
if(power.roll) tempPower.stats = {Roll:{value: power.roll}};
assetCatalog[powerName] = tempPower;
}else{
let powerName = 'Psychic Power§' + power.name.toLowerCase().split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
let tempPower = {
text: formatText(power.description),
stats:{Roll:{
value: power.type + (power.roll ? (' ' + power.roll) : '')
}}
};
assetCatalog[powerName] = tempPower;
}
});
}
processWarlordTraits = (data,assetCatalog) => {
data.warlordTraits.forEach(trait => {
let traitName = 'Warlord Trait§' + trait.name.toLowerCase().split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
let tempTrait = {
text: formatText(trait.description),
stats:{Discipline:{
value: trait.type + (trait.roll ? (' ' + trait.roll) : '')
}}
};
assetCatalog[traitName] = tempTrait;
});
}
formatText = (text,log = false) => {
let newText = text
let replacePatterns1 = {
pPattern: [/<p[^>]+>((?:.(?!\<\/p\>))*.)<\/p>/g,'$1'],
divPattern: [/<div[^>]+>((?:.(?!\<\/div\>))*.)<\/div>/g,'$1'],
anchorPattern: [/<a[\s]+href="([^>]+)">((?:.(?!\<\/a\>))*.)<\/a>/g,'[$2](https://wahapedia.ru$1)'],
tooltipPattern: [/<span[\s]+class="tooltip([^>]+)>((?:.(?!\<\/span\>))*.)<\/span>/g,'$2'],
kwb3Pattern: [/<span[\s]+class="kwb3">((?:.(?!\<\/span\>))*.)<\/span>/g,'$1'],
boldunderPattern: [/<span[\s]+class="kwb kwbu">((?:.(?!\<\/span\>))*.)<\/span>/g,'<b><i>$1</i></b>'],
boldPattern: [/<span[\s]+class="kwb">((?:.(?!\<\/span\>))*.)<\/span>/g,'<b>$1</b>'],
underPattern: [/<span[\s]+class="kwbu">((?:.(?!\<\/span\>))*.)<\/span>/g,'<i>$1<i>'],
ttPattern: [/<span[\s]+class="tt">((?:.(?!\<\/span\>))*.)<\/span>/g,'<b>$1</b>'],
boldunderPattern2: [/<span[\s]+class="kwb kwbu">((?:.(?!\<\/span\>))*.)<\/span>/g,'<b><i>$1</i></b>'],
boldPattern2: [/<span[\s]+class="kwb">((?:.(?!\<\/span\>))*.)<\/span>/g,'<b>$1</b>'],
underPattern2: [/<span[\s]+class="kwbu">((?:.(?!\<\/span\>))*.)<\/span>/g,'<i>$1<i>'],
ttPattern2: [/<span[\s]+class="tt">((?:.(?!\<\/span\>))*.)<\/span>/g,'<b>$1</b>'],
h_customPattern: [/<span[\s]+class="h_custom">((?:.(?!\<\/span\>))*.)<\/span>/g,'<b>$1</b>'],
redfontPattern: [/<span[\s]+class="redfont">((?:.(?!\<\/span\>))*.)<\/span>/g,'<b>$1</b>'],
}
let replacePatterns2 = {
doubleBoldEndPattern: [/<\/b><\/b>/g,'</b>'],
doubleItalicsEndPattern: [/<\/i>((?:.(?!\<i\>))*)<\/i>/g,'$1<i>'],
whitespacePattern1: [/\<\/b\>[\s]\<b\>/g,' '],
whitespacePattern2: [/\<\/i\>[\s]\<i\>/g,' '],
boldTranslationPattern: [/<\/?b>/g,'**'],
doubleBoldPattern: [/\*\*\*\*/g,''],
doubleBoldPattern2: [/\*\*\s\*\*/g,' '],
italicsTranslationPattern: [/<\/?i>/g,'*']
}
newText = text.replace(/kwb2/g,'kwb');
if(log) console.log(newText)
Object.entries(replacePatterns1).forEach(([name,pattern]) => {
newText = newText.replace(pattern[0],pattern[1]);
if(log) console.log(name,newText)
});
if(log) console.log(newText)
let newTextArr = newText.split('<b>');
if(log) console.log(newTextArr)
newTextArr.forEach((sliver,i,a) => {
if(i > 0 && a[i+1]?.includes('</b>')){
a[i+1] = a[i] + a[i+1];
delete a[i];
}
});
newText = Object.values(newTextArr).join('<b>');
newTextArr = newText.split('<i>');
newTextArr.forEach((sliver,i,a) => {
if(i > 0 && a[i+1]?.includes('</i>')){
a[i+1] = a[i] + a[i+1];
delete a[i];
}
});
newText = Object.values(newTextArr).join('<i>');
Object.entries(replacePatterns2).forEach(([name,pattern]) => {
newText = newText.replace(pattern[0],pattern[1]);
if(log) console.log(name,newText)
});
// newText = newText.replace(/"/g,'″'); // too many html structures get screwed by this
newText = newText.replace(/<ul[^>]+><li>/g,'* ');
if(log) console.log(newText)
newText = newText.replace(/<\/li><li>/g,'\n* ');
if(log) console.log(newText)
newText = newText.replace(/<\/li><\/ul>/g,'');
if(log) console.log(newText)
newText = newText.replace(/<br>/g,'\n\n');
if(log) console.log(newText)
return newText
}
processUnits = (data,assetCatalog) => {
var stringSimilarity = require('string-similarity');
data.datasheets.forEach(datasheet => {
let unitId = datasheet.datasheet_id;
let tempItem = {stats:{
'Power Level': {
value: Number(datasheet.power_points)
}
},keywords:{},assets:{}};
let models = data.models.filter(model => model.datasheet_id === unitId);
// console.log(unitId,models)
if(models[0]?.line === 1 && models[0]?.models_per_unit.includes('-')){
tempItem.stats.model = {
value: 'Model§' + data.models.filter(model => model.datasheet_id === unitId && model.line === 1)[0].name
}
}
let keywords = data.keywords.filter(keyword => keyword.datasheet_id === unitId && !keyword.is_faction_keyword);
if(keywords.length) tempItem.keywords.Keywords = keywords.map(keyword => keyword.keyword);
let factionKeywords = data.keywords.filter(keyword => keyword.datasheet_id === unitId && keyword.is_faction_keyword);
if(factionKeywords.length) tempItem.keywords.Faction = factionKeywords.map(keyword => keyword.keyword);
let options = data.options.filter(option => option.datasheet_id === unitId);
tempItem.text = formatText(datasheet.unit_composition + '\n\n' + options.map(option => (option.button || '') + ' ' + option.description).join('\n\n') + '\n\n' + datasheet.psyker);
if(models[0]?.models_per_unit?.includes('-')){
tempItem.stats[datasheet.name] = {
statType: 'numeric',
dynamic: true,
visibility: 'always',
};
let stat = tempItem.stats[datasheet.name];
let range = models[0].models_per_unit.split('-');
stat.value = Number(range[0]);
stat.min = Number(range[0]);
stat.max = Number(range[1]);
let basePlThresh = stat.min;
if(!(stat.max % stat.min)){
// console.log(datasheet.name,'has a clean threshold')
stat.increment = {value: stat.min};
}
else if(!((stat.max + 1) % (stat.min + 1)) && models[1]?.models_per_unit == 1){
// console.log(datasheet.name,'has a sergeant')
stat.increment = {value: stat.min + 1};
// console.log(stat)
basePlThresh ++;
}else tempItem.text += '\n\nERROR: there might be a problem with incrementation that will require inputting by hand.';
let PLArr = datasheet.unit_composition.split(/(\<b\>Power Rating |\<\/b\>)/).map(el => Number(el.replace('+','plus'))).filter(el => !isNaN(el));
if(PLArr.length){
let tempInc = PLArr[0] - datasheet.power_points;
// console.log(datasheet.name,basePlThresh,tempInc,PLArr)
for (let i = 0; i < PLArr.length; i++) {
if(PLArr[i] !== ((i+1) * tempInc) + Number(datasheet.power_points)){
// console.log(PLArr[i],tempInc,Number(datasheet.power_points), ((i+1) * tempInc) + Number(datasheet.power_points))
tempItem.text += '\n\nERROR: there might be a problem with Power Rating that will require a custom rule.';
tempInc = 0;
break;
}
}
if(tempInc){
tempItem.stats.poweri = {value:tempInc};
for (let i = 0; i < PLArr.length; i++) {
tempItem.stats['power'+(i+1)] = {
"value": (basePlThresh * (i + 1)) + 1
}
}
}
}else if(datasheet.unit_composition.includes('Power Rating')) tempItem.text += '\n\nERROR: there might be a problem with Power Rating that will require a custom rule.';
}
let modelList = [];
models.forEach(model => {
let [minQty,maxQty] = model.models_per_unit.split('-').map(qty => Number(qty));
let statlineName = model.itemKey.replace('Model§','Statline§');
if(minQty){
let defaultStatline = assetCatalog[statlineName];
// console.log(defaultStatline)
let tempTrait = {item: model.itemKey};
if(minQty > 1) tempTrait.quantity = minQty;
// console.log(datasheet.name,model.name,model.models_per_unit,models.length)
if(model.models_per_unit == 1 && models.length == 1){
tempTrait.stats = tempTrait.stats || {};
tempTrait.stats.Points = tempTrait.stats.Points || {};
tempTrait.stats.Points.visibility = 'hidden';
}
let tempStatline = {...tempTrait,item: statlineName};
delete tempStatline.quantity;
// console.log(tempTrait)
// console.log(tempStatline)
if(Object.keys(tempStatline).length === 1) tempStatline = statlineName;
if(Object.keys(tempTrait).length === 1) tempTrait = model.itemKey;
tempItem.assets.traits = tempItem.assets.traits || [];
tempItem.assets.traits.push(tempTrait);
modelList.push(model)
tempItem.assets.traits.push(tempStatline);
}
if(minQty > 1 || maxQty > 1){
tempItem.allowed = tempItem.allowed || {};
tempItem.allowed.items = tempItem.allowed.items || [];
tempItem.allowed.items.push(model.itemKey)
}
});
let abilities = data.abilities.composed.filter(ability => ability.datasheet_id === unitId);
let abilityList = abilities.filter(ability => ability.datasheet_id === unitId && !ability.is_other_wargear);
let wargearList = abilities.filter(ability => ability.datasheet_id === unitId && ability.is_other_wargear);
abilityList.forEach(ability => {
tempItem.assets = tempItem.assets || {};
tempItem.assets.traits = tempItem.assets.traits || [];
tempItem.assets.traits.push(ability.itemKey);
});
const order = ['Statline§', 'Ability§', 'Wargear§', 'Psychic Power§', 'Model§'];
tempItem.assets.traits.sort((a, b) => stringSimilarity.findBestMatch((a.item || a),order).bestMatchIndex - stringSimilarity.findBestMatch((b.item || b),order).bestMatchIndex);
if(datasheet.psyker?.includes('Smite')){
tempItem.assets = tempItem.assets || {};
tempItem.assets.traits = tempItem.assets.traits || [];
tempItem.assets.traits.push('Psychic Power§Smite');
}
Array.from(new Set(data.psychicPowers.map(power => power.type))).forEach(discipline => {
// console.log(discipline)
// console.log(datasheet.psyker)
let test = new RegExp(discipline,'gi')
if(test.test(datasheet.psyker)){
tempItem.allowed = tempItem.allowed || {};
tempItem.allowed.classifications = tempItem.allowed.classifications || [];
tempItem.allowed.classifications.push(discipline);
}
});
wargearList.forEach(wargear => {
let tempWargear = wargear.cost === assetCatalog[wargear.itemKey].stats?.Points?.value ? wargear.itemKey : {
item: wargear.itemKey,
stats: {
Points: {value: wargear.cost}
}
};
tempItem.stats = tempItem.stats || {};
tempItem.stats[wargear.name] = {
value: 0,
statType: 'rank',
statOrder: 10,
ranks: {
0: {order: 0,number: 0,icons: ['cancel']},
1: {order: 1,number: 1,icons: ['confirmed'],traits: [{trait: tempWargear}]}
},
visibility: 'active',
dynamic: true
}
});
// console.log(datasheet.name,unitId,wargearList)
let wargearArr = data.wargear.composed.filter(wargear => wargear.datasheet_id == unitId).sort((a,b) => a.itemKey.localeCompare(b.itemKey));
wargearArr.slice().forEach((gear,i) => {
if(wargearArr[i].itemKey?.includes(' (melee)') && wargearArr[i+1]?.includes(' (shooting)')){
wargearArr[i].itemKey = wargearArr[i].itemKey.replace('Weapon§','Wargear§').replace(' (melee)','');
delete wargearArr[i+1];
}
});
wargearArr = Object.values(wargearArr);
let equippedWargearArr = datasheet.unit_composition?.replace(/is equipped<br>with/g,'is equipped with').replace(/(<br>|<ul><li>|<li><li>|<\/li><\/ul>|<\/b> |\.\s)/g,'. ').split('. ').filter(el => el.includes('is equipped')).map(el => el.split(/is equipped with/).map(subEl => subEl.split('; ').map(equip => equip.replace(/^([:Aa1]\s)*/,''))));
// console.log(datasheet.name,unitId,wargearArr,datasheet.unit_composition)
equippedWargearArr?.forEach(modelLoadout => {
// console.log(modelLoadout[0][0],modelLoadout[1])
if(!modelLoadout[1]?.includes(' nothing.')){
let upgradeQty = modelLoadout[1]?.length ? (modelLoadout[1].length + 1) : 0;
if(
stringSimilarity.compareTwoStrings(modelLoadout[0][0],'Every model') > .5
|| stringSimilarity.compareTwoStrings(modelLoadout[0][0],'Each model') > .5
|| stringSimilarity.compareTwoStrings(modelLoadout[0][0],'This model') > .5
){
models.forEach(modelData => {
let tempItem = assetCatalog[modelData.itemKey];
if(upgradeQty) tempItem.stats = tempItem.stats || {};
for (let i = 0; i < upgradeQty; i++) {
tempItem.stats['loadout'+(i+1)] = createWargearStat(i,wargearArr,modelLoadout[1],assetCatalog);
// console.log(tempItem,upgradeQty)
}
});
}else{
let modelNames = models.map(thisModel => thisModel.name);
let modelIndex = stringSimilarity.findBestMatch(modelLoadout[0][0],modelNames).bestMatchIndex;
let tempItem = assetCatalog[models[modelIndex].itemKey];
if(upgradeQty) tempItem.stats = tempItem.stats || {};
for (let i = 0; i < upgradeQty; i++) {
tempItem.stats['loadout'+(i+1)] = createWargearStat(i,wargearArr,modelLoadout[1],assetCatalog);
// console.log(tempItem,upgradeQty)
}
}
}
});
let source = data.sources.filter(source => source.source_id == datasheet.source_id)[0];
// console.log(source)
if(source){
let errataDate = source.errata_date.split(' ')[0].split('.').reverse().join('-');
tempItem.meta = tempItem.meta || {};
tempItem.meta.Publication = `[${source.name} (${source.type}) ${ordinalize(source.edition)} ed. ${source.version || ''} @${errataDate}](${source.errata_link})`;
}
// TODO implement dynamic stats
let modelDamage = data.damage.filter(dmgLine => dmgLine.datasheet_id == unitId);
if(modelDamage.length){
let modelItemKey = models.filter(model => model.datasheet_id === unitId)[0].itemKey;
// console.log(unitId,modelItemKey)
assetCatalog[modelItemKey].stats['W'] = {
value: assetCatalog[modelItemKey].stats['W'].value,
max: assetCatalog[modelItemKey].stats['W'].value,
min: 1,
dynamic: true,
increment: {value: 1},
statType: 'numeric',
visibility: 'always',
}
assetCatalog[modelItemKey].rules = assetCatalog[modelItemKey].rules || {};
assetCatalog[modelItemKey].rules.dynamicDamageMid = generateDamageRule(modelDamage[0],modelDamage[2]);
assetCatalog[modelItemKey].rules.dynamicDamageLow = generateDamageRule(modelDamage[0],modelDamage[3]);
}
assetCatalog[datasheet.role + '§' + datasheet.name] = tempItem;
});
}
generateDamageRule = (damageRows,currentRow) => {
// console.log(damageRows,currentRow)
let [min,max] = currentRow.col1.split('-');
let newRule = {
evals: [
{
paths: [
['{self}','stats','W','value']
],
max: max,
min: min,
operator: 'AND',
not: false,
actionable: true
}
],
failState: 'pass',
evaluate: 'AND',
actions: [
{
paths: [
['{self}','stats',damageRows.col2,'value']
],
actionType: 'set',
value: typeof currentRow.col2 === 'number' ? currentRow.col2 : currentRow.col2?.replace('"',''),
iterations: 1
},
{
paths: [
['{self}','stats',damageRows.col3,'value']
],
actionType: 'set',
value: typeof currentRow.col3 === 'number' ? currentRow.col3 : currentRow.col3?.replace('"',''),
iterations: 1
},
{
paths: [
['{self}','stats',damageRows.col4,'value']
],
actionType: 'set',
value: typeof currentRow.col4 === 'number' ? currentRow.col4 : currentRow.col4?.replace('"',''),
iterations: 1
}
]
}
return newRule
}
processFactions = (data,assetTaxonomy) => {
let fac = data.factions[0].main_faction_id;
// console.log(fac,data.factions.length)
data.factCurrent = subFacNames[fac];
if(data.factions.length > 1){
assetTaxonomy.Detachment = {
stats: {
[subFacNames[fac]]: {
statType: 'rank',
value: '-',
ranks: {
'-': {
order: 0
},
},
dynamic: true,
}
},
rules: {
'populate faction': {
evals: [
{
paths: [
["{self}","stats","Brotherhood","value"]
],
value: "-",
operator: "AND",
not: true,
actionable: true
}
],
failState: 'pass',
evaluate: 'OR',
actions: [
{
paths: [
[
'{self}',
'assets',
'templateClass',
'Unit',
'traits',
'classification',
'Ability',
'stats',
subFacNames[fac],
'value',
]
],
actionType: 'set',
value: [
'{self}',
'stats',
subFacNames[fac],
'value',
],
iterations: 1
}
]
}
}
}
data.factions.filter(faction => faction.faction_id != faction.main_faction_id).forEach((faction,i) => {
let newRank = {order:i+1}
assetTaxonomy.Detachment.stats[subFacNames[fac]].ranks[faction.name] = newRank;
});
assetTaxonomy.Unit = {
rules: {
'replace subfaction keyword': {
evals: [
{
paths: [
['{parent}','stats',data.factCurrent,'value']
],
value: '-',
operator: 'AND',
not: true
},
{
paths: [
['{self}','keywords','Faction']
],
value: `<${data.factCurrent}>`,
contains: true,
operator: 'AND',
not: false,
actionable: true
}
],
failState: 'pass',
evaluate: 'AND',
actions: [
{
paths: [
['{self}','keywords','Faction']
],
actionType: 'remove',
value: `<${data.factCurrent}>`,
iterations: 1
},
{
paths: [
['{self}','keywords','Faction']
],
actionType: 'add',
value: ['{parent}','stats',data.factCurrent,'processed','rank','current'],
iterations: 1
}
]
}
}
}
}
}
processPsychicClasses = (data,assetTaxonomy) => {
data.psychicPowers.forEach(power => {
if(power.type){
assetTaxonomy[power.type] = assetTaxonomy[power.type] || {
templateClass: 'Psychic Power',
stats:{Roll:{
value: null
}}
}
}
});
}
module.exports = { processItems };

View file

@ -0,0 +1,8 @@
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}

View file

@ -0,0 +1,270 @@
var express = require('express');
var router = express.Router();
const pool = require('../config/data');
const SqlString = require('sqlstring');
const { processItems } = require('../public/javascripts/manifest.process');
/* GET home page. */
router.get('/', function(req, res, next) {
console.log(req.body)
// var sql = SqlString.format("SELECT * FROM datasheets;");
var sql = SqlString.format("SELECT * FROM factions;");
console.log(sql)
pool.query(sql, function (error, results, fields) {
if(error) {
console.log(error);
res.send(sql);
return;
}
console.log(results)
let factionList = results.filter(faction => faction.faction_id === faction.main_faction_id).sort((a,b) => a.name.localeCompare(b.name));
console.log(factionList)
//if we actually get a result
res.render('index', { title: 'Wahapedia Exportion', data: factionList});
// res.send(JSON.stringify(results.map(datasheet => `${datasheet.datasheet_id}: ${datasheet.name} ${datasheet.unit_composition}`)));
});
});
router.get('/favicon.ico', function(req, res, next) {})
router.get('/:faction', async function (req, res, next) {
console.log('req.body',req.body)
let sql = '';
let allResults = {};
allResults.factions = await getFactions(req.params.faction);
allResults.datasheets = await getDatasheets(req.params.faction);
let datasheetList = Array.from(new Set(allResults.datasheets.map(datasheet => datasheet.datasheet_id)));
console.log(datasheetList)
allResults.keywords = await getKeywords(datasheetList);
allResults.models = await getModels(datasheetList);
allResults.damage = await getDamage(datasheetList);
allResults.wargear = await getWargear(datasheetList);
allResults.abilities = await getAbilities(datasheetList);
allResults.options = await getOptions(datasheetList);
allResults.psychicPowers = await getPsychicPowers(req.params.faction);
allResults.stratagems = await getStrategems(req.params.faction);
allResults.warlordTraits = await getWarlordTraits(req.params.faction);
allResults.sources = await getSources();
// console.log('allresults',allResults)
allResults['!'] = processInfo(allResults,req.params.faction);
allResults['!'].manifest.assetTaxonomy = processClasses(allResults);
allResults['!'].manifest.assetCatalog = processItems(allResults);
res.send(JSON.stringify(allResults['!']));
});
let getFactions = async (fac) => {
const sql = SqlString.format("SELECT * FROM factions WHERE main_faction_id = ?",[fac]);
console.log('query',fac,sql)
let results = await new Promise((resolve, reject) => pool.query(sql, (error, results) => {
if(error) {
console.log(error);
reject(error);
return;
}else{
resolve(results);
}
}));
// console.log('factions',results)
return results;
}
let getDatasheets = async (fac) => {
const sql = SqlString.format("SELECT * FROM datasheets WHERE faction_id = ?",[fac]);
let results = await new Promise((resolve, reject) => pool.query(sql, (error, results) => {
if(error) {
console.log(error);
reject(error);
return;
}else{
resolve(results);
}
}));
// console.log('datasheets',results)
return results;
}
let getKeywords = async (datasheets) => {
const sql = SqlString.format("SELECT * FROM datasheets_keywords WHERE datasheet_id in (?)",[datasheets]);
console.log('query',sql)
let results = datasheets.length ? await new Promise((resolve, reject) => pool.query(sql, (error, results) => {
if(error) {
console.log(error);
reject(error);
return;
}else{
resolve(results);
}
})) : {};
// console.log('keywords',results)
return results;
}
let getModels = async (datasheets) => {
const sql = SqlString.format("SELECT * FROM datasheets_models WHERE datasheet_id in (?)",[datasheets]);
console.log('query',sql)
let results = datasheets.length ? await new Promise((resolve, reject) => pool.query(sql, (error, results) => {
if(error) {
console.log(error);
reject(error);
return;
}else{
resolve(results);
}
})) : {};
// console.log('models',results)
return results;
}
let getDamage = async (datasheets) => {
const sql = SqlString.format("SELECT * FROM datasheets_damage WHERE datasheet_id in (?)",[datasheets]);
console.log('query',sql)
let results = datasheets.length ? await new Promise((resolve, reject) => pool.query(sql, (error, results) => {
if(error) {
console.log(error);
reject(error);
return;
}else{
resolve(results);
}
})) : {};
// console.log('models',results)
return results;
}
let getWargear = async (datasheets) => {
let sql = SqlString.format("SELECT * FROM datasheets_wargear WHERE datasheet_id in (?)",[datasheets]);
let datasheets_wargear = await new Promise((resolve, reject) => pool.query(sql, (error, results) => {
if(error) {
console.log(error);
reject(error);
return;
}else{
resolve(results);
}
}));
let wargearIDs = Array.from(new Set(datasheets_wargear?.map(wargear => wargear?.wargear_id)));
sql = SqlString.format("SELECT * FROM wargear_list WHERE wargear_id in (?)",[wargearIDs]);
let wargear_list = wargearIDs.length ? await new Promise((resolve, reject) => pool.query(sql, (error, results) => {
if(error) {
console.log(error);
reject(error);
return;
}else{
resolve(results);
}
})) : {};
sql = SqlString.format("SELECT * FROM wargear WHERE wargear_id in (?)",[wargearIDs]);
let wargear = wargearIDs.length ? await new Promise((resolve, reject) => pool.query(sql, (error, results) => {
if(error) {
console.log(error);
reject(error);
return;
}else{
resolve(results);
}
})) : {};
// console.log('models',results)
return {
datasheets_wargear:datasheets_wargear,
wargear_list:wargear_list,
wargear:wargear,
};
}
let getAbilities = async (datasheets) => {
let sql = SqlString.format("SELECT * FROM datasheets_abilities WHERE datasheet_id in (?)",[datasheets]);
let datasheets_abilities = await new Promise((resolve, reject) => pool.query(sql, (error, results) => {
if(error) {
console.log(error);
reject(error);
return;
}else{
resolve(results);
}
}));
let abilityIDs = Array.from(new Set(datasheets_abilities.map(ability => ability.ability_id)));
sql = SqlString.format("SELECT * FROM abilities WHERE ability_id in (?)",[abilityIDs]);
let abilities = await new Promise((resolve, reject) => pool.query(sql, (error, results) => {
if(error) {
console.log(error);
reject(error);
return;
}else{
resolve(results);
}
}));
// console.log('models',results)
return {
datasheets_abilities:datasheets_abilities,
abilities:abilities,
};
}
let getOptions = async (datasheets) => {
const sql = SqlString.format("SELECT * FROM datasheets_options WHERE datasheet_id in (?)",[datasheets]);
let results = await new Promise((resolve, reject) => pool.query(sql, (error, results) => {
if(error) {
console.log(error);
reject(error);
return;
}else{
resolve(results);
}
}));
return results;
}
let getPsychicPowers = async (fac) => {
const sql = SqlString.format("SELECT * FROM psychic_powers WHERE faction_id = ?",[fac]);
let results = await new Promise((resolve, reject) => pool.query(sql, (error, results) => {
if(error) {
console.log(error);
reject(error);
return;
}else{
resolve(results);
}
}));
return results;
}
let getStrategems = async (fac) => {
const sql = SqlString.format("SELECT * FROM strategems WHERE faction_id = ?",[fac]);
let results = await new Promise((resolve, reject) => pool.query(sql, (error, results) => {
if(error) {
console.log(error);
reject(error);
return;
}else{
resolve(results);
}
}));
return results;
}
let getWarlordTraits = async (fac) => {
const sql = SqlString.format("SELECT * FROM warlord_traits WHERE faction_id = ?",[fac]);
let results = await new Promise((resolve, reject) => pool.query(sql, (error, results) => {
if(error) {
console.log(error);
reject(error);
return;
}else{
resolve(results);
}
}));
return results;
}
let getSources = async () => {
const sql = SqlString.format("SELECT * FROM sources");
let results = await new Promise((resolve, reject) => pool.query(sql, (error, results) => {
if(error) {
console.log(error);
reject(error);
return;
}else{
resolve(results);
}
}));
return results;
}
module.exports = router;

View file

@ -0,0 +1,6 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}

View file

@ -0,0 +1,7 @@
extends layout
block content
h1= title
each faction in data
p
a(href=`/${faction.faction_id}`)= faction.name

View file

@ -0,0 +1,7 @@
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content