SE3-ArkhamDBExporter/resources/ArkhamDBExport.js

326 lines
8.8 KiB
JavaScript

/*
* ArkhamDBExport.js
*
* Generates a JSON file and card images for import into ArkhamDB
*/
useLibrary('imageutils');
useLibrary('project');
// The resolution (in pixels per inch) of the exported images
const RESOLUTION = 200;
// The extension of the image file format to use, e.g., png, jpg
const FORMAT = ImageUtils.FORMAT_JPEG;
// TODO: should be defined in strange eons somewhere
const pack_code = "kyo_player";
const cycle_prefix = "43";
function getName() {
return 'ArkhamDB Export';
}
function getDescription() {
return 'Generates a JSON file and card images for import into ArkhamDB';
}
function getVersion() {
return 1.0;
}
function getPluginType() {
return arkham.plugins.Plugin.INJECTED;
}
function unload() {
unregisterAll();
}
// Creates a test button during development that calls unload() to clean up.
testProjectScript();
function renameSlot(slot) {
if (slot.startsWith('1 ')) {
return slot.slice(2);
}
else if (slot.startsWith('2 ')) {
return slot.slice(2) + ' x2';
}
else {
return slot;
}
}
const tag_replacements = {
"<gua>": "[guardian]",
"<see>": "[seeker]",
"<rog>": "[rogue]",
"<mys>": "[mystic]",
"<sur>": "[survivor]",
"<wil>": "[willpower]",
"<int>": "[intellect]",
"<com>": "[combat]",
"<agi>": "[agility]",
"<wild>": "[wild]",
"<sku>": "[skull]",
"<cul>": "[cultist]",
"<tab>": "[tablet]",
"<mon>": "[elder_thing]",
"<ble>": "[bless]",
"<cur>": "[curse]",
"<eld>": "[eldersign]",
"<ten>": "[auto_fail]",
"<act>": "[action]",
"<fre>": "[free]",
"<rea>": "[reaction]",
"<for>": "<b>Forced</b>",
"<hau>": "<b>Haunted</b>",
"<obj>": "<b>Objective</b>",
"<pat>": "Patrol",
"<rev>": "<b>Revelation</b>",
"<uni>": "{Unique}", // TODO
"<per>": "[per_investigator]",
"<bul>": "- ",
"<squ>": "{Square}", // TODO
// TODO
"<bultab>": "", // Tab spacing for bullet sections
"<t>": "<b><i>", // Trait
"</t>": "</i></b>",
"<hs>": "", // Horizontal spacer
"<lvs>": "", // Large vertical spacer
"<vs>": "", // Vertical spacer
"<svs>": "", // Small vertical spacer
};
// TODO: handle investigator cards
const card_types = {
"AHLCG-Event-Default": "event",
"AHLCG-Skill-Default": "skill",
"AHLCG-Asset-Default": "asset",
// TODO: actually handle enemy weaknesses
"AHLCG-WeaknessEnemy-Default": "enemy",
"AHLCG-WeaknessTreachery-Default": "treachery",
};
function int_or_null(inp) {
if (inp == 'None') {
return null;
}
else if (inp == 'X') {
return -2;
}
else {
return parseInt(inp);
}
}
function leftPad(str, len, fill) {
return fill.repeat(Math.max(len - str.length, 0)) + str;
}
function replaceAll(str, search, replace) {
return str.split(search).join(replace);
}
function build_card(component) {
function substitute_tags(str) {
str = str.trim();
str = replaceAll(str, "<fullname>", String(component.getName()));
for (let tag in tag_replacements) {
str = replaceAll(str, tag, tag_replacements[tag]);
}
return str;
}
const code = cycle_prefix + leftPad(String(component.settings.get('CollectionNumber')), 3, '0');
const card_data = {
code: String(code),
deck_limit: 2, // TODO: could be derived?
flavor: substitute_tags(String(component.settings.get('Flavor'))),
illustrator: String(component.settings.get('Artist')),
is_unique: component.settings.getBoolean('Unique'),
name: substitute_tags(String(component.getName())),
pack_code: pack_code,
position: int_or_null(component.settings.get('CollectionNumber')),
quantity: 2, // TODO
//restrictions: null, // TODO
// TODO: should also handle "Victory" field
text: substitute_tags(String(
component.settings.get('Keywords') + '\n' + component.settings.get('Rules'))),
traits: substitute_tags(String(component.settings.get('Traits'))),
type_code: card_types[component.getFrontTemplateKey()],
xp: int_or_null(component.settings.get('Level')),
};
const raw_health = component.settings.get('Stamina');
if (raw_health && raw_health != 'None' && raw_health != '-') {
card_data.health = int_or_null(raw_health);
}
const raw_sanity = component.settings.get('Sanity');
if (raw_sanity && raw_sanity != 'None' && raw_sanity != '-') {
card_data.sanity = int_or_null(raw_sanity);
}
const skills = {
Agility: 0,
Intellect: 0,
Combat: 0,
Willpower: 0,
Wild: 0,
};
for (let i = 1; i<=6; i++) {
let skill_icon = component.settings.get('Skill' + i);
if (skill_icon in skills) {
skills[skill_icon] += 1;
}
}
for (let skill in skills) {
if (skills[skill] > 0) {
card_data["skill_" + skill.toLowerCase()] = skills[skill];
}
}
const raw_cost = component.settings.get('ResourceCost');
if (raw_cost) {
card_data.cost = int_or_null(raw_cost);
}
const raw_slot = component.settings.get('Slot');
if (raw_slot && raw_slot != 'None') {
card_data.slot = renameSlot(String(raw_slot));
const raw_slot2 = component.settings.get('Slot2');
if (raw_slot2 && raw_slot2 != 'None') {
card_data.slot += '. ' + renameSlot(String(raw_slot2));
}
}
const subtitle = component.settings.get('Subtitle');
if (subtitle && subtitle != '') {
card_data.subname = String(subtitle);
}
const faction = component.settings.get('CardClass');
if (faction) {
if (faction == 'Weakness') {
card_data.subtype_code = "weakness";
}
else if (faction == 'Basic Weakness') {
card_data.subtype_code = "basicweakness";
}
else {
card_data.faction_code = String(faction).toLowerCase();
const faction2 = component.settings.get('CardClass2');
if (faction2 && faction2 != 'None') {
card_data.faction2_code = String(faction2).toLowerCase();
}
}
}
if (card_types[component.getFrontTemplateKey()] == 'enemy') {
// TODO: "weakness" or "basicweakness"
card_data.subtype_code = "basicweakness";
}
// TODO: parse out some keywords into their own fields
// order by keys
const ordered_card_data = Object.keys(card_data).sort().reduce(
function(obj, key) {
obj[key] = card_data[key];
return obj;
},
{}
);
return ordered_card_data;
}
function exportCard(component, file) {
try {
// create the sheets that will paint the faces of the component
let sheets = component.createDefaultSheets();
if (sheets == null) return;
// export front face
let image = sheets[0].paint(arkham.sheet.RenderTarget.EXPORT, RESOLUTION);
ImageUtils.write(image, file, FORMAT, -1, false, RESOLUTION);
} catch (ex) {
println(ex);
println('Error while making image for ' + component.getName() + ', skipping file');
Error.handleUncaught(ex);
}
}
function run() {
const arkhamDBAction = JavaAdapter(TaskAction, {
getLabel: function getLabel() {
return 'Generate ArkhamDB Data';
},
getActionName: function getActionName() {
return 'arkhamdb';
},
// Applies to Deck Tasks
appliesTo: function appliesTo(project, task, member) {
if (member != null || task == null) {
return false;
}
const type = task.settings.get(Task.KEY_TYPE);
if (NewTaskType.DECK_TYPE.equals(type)) {
return true;
}
return false;
},
perform: function perform(project, task, member) {
member = ProjectUtilities.simplify(project, task, member);
Eons.setWaitCursor(true);
try {
this.performImpl(member);
} catch (ex) {
Error.handleUncaught(ex);
} finally {
Eons.setWaitCursor(false);
member.synchronize();
}
},
performImpl: function performImpl(member) {
const children = member.children.filter(function (child) {
return ProjectUtilities.matchExtension(child, 'eon');
});
const cards = [];
for (let child of children) {
try {
let component = ResourceKit.getGameComponentFromFile(child.file);
if (component.getFrontTemplateKey() in card_types) {
printf("Generating JSON/PNG for '%s'...\n", child);
let card_data = build_card(component);
cards.push(card_data);
let export_dir = new File(member.file, 'export');
let target_file = new File(export_dir, card_data.code + '.' + FORMAT);
if (!target_file.exists() || child.file.lastModified() > target_file.lastModified()) {
printf("Image for '%s' is out of date, rebuilding...\n", child);
export_dir.mkdir();
exportCard(component, target_file);
}
}
} catch (ex) {
println(ex);
println('Error while processing ' + child.name + ', skipping file');
Error.handleUncaught(ex);
}
}
cards.sort(function (a, b) {
return parseInt(a.code) - parseInt(b.code);
});
const file = new File(member.file, pack_code + '.json');
printf("Writing '%s'\n", file);
ProjectUtilities.writeTextFile(file, JSON.stringify(cards, null, 4));
println("Done!");
}
});
ActionRegistry.register(arkhamDBAction, Actions.PRIORITY_IMPORT_EXPORT);
}