539 lines
15 KiB
JavaScript
539 lines
15 KiB
JavaScript
/*
|
|
* ArkhamDBExport.js
|
|
*
|
|
* Generates a JSON file and card images for import into ArkhamDB
|
|
*/
|
|
|
|
useLibrary('imageutils');
|
|
useLibrary('project');
|
|
useLibrary('uilayout');
|
|
useLibrary('uicontrols');
|
|
importClass(arkham.project.CopiesList);
|
|
|
|
// 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;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// TODO: very incomplete
|
|
const double_sided = [
|
|
"AHLCG-Investigator-Default",
|
|
];
|
|
|
|
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
|
|
};
|
|
|
|
function int_or_null(inp) {
|
|
if (inp == 'None') {
|
|
return null;
|
|
}
|
|
else if (inp == 'X') {
|
|
return -2;
|
|
}
|
|
else {
|
|
return parseInt(inp);
|
|
}
|
|
}
|
|
|
|
function replaceAll(str, search, replace) {
|
|
return str.split(search).join(replace);
|
|
}
|
|
|
|
function UnsupportedComponentError() {}
|
|
|
|
function build_card(component, pack_code, cycle_prefix, copies) {
|
|
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;
|
|
}
|
|
|
|
function common_data() {
|
|
const code = cycle_prefix + String(component.settings.get('CollectionNumber')).padStart(3, '0');
|
|
|
|
return {
|
|
name: substitute_tags(component.getName()),
|
|
pack_code: pack_code,
|
|
quantity: copies,
|
|
|
|
// "Game Text" tab
|
|
traits: substitute_tags(component.settings.get('Traits')),
|
|
text: substitute_tags(
|
|
component.settings.get('Keywords') + '\n' + component.settings.get('Rules')),
|
|
flavor: substitute_tags(component.settings.get('Flavor')),
|
|
// TODO: "Victory" field
|
|
|
|
// "Collection" tab
|
|
code: code,
|
|
position: int_or_null(component.settings.get('CollectionNumber')),
|
|
|
|
// "Portraits" tab
|
|
illustrator: component.settings.get('Artist'),
|
|
//restrictions: null, // TODO
|
|
};
|
|
}
|
|
|
|
function is_unique() {
|
|
if (component.settings.getBoolean('Unique')) {
|
|
return { is_unique: true };
|
|
} else {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function health_and_sanity() {
|
|
const card_data = {};
|
|
const raw_health = component.settings.get('Stamina');
|
|
if (raw_health != 'None' && raw_health != '-') {
|
|
card_data.health = int_or_null(raw_health);
|
|
}
|
|
const raw_sanity = component.settings.get('Sanity');
|
|
if (raw_sanity != 'None' && raw_sanity != '-') {
|
|
card_data.sanity = int_or_null(raw_sanity);
|
|
}
|
|
return card_data;
|
|
}
|
|
|
|
function skill_icons() {
|
|
const card_data = {};
|
|
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];
|
|
}
|
|
}
|
|
|
|
return card_data;
|
|
}
|
|
|
|
function faction() {
|
|
const faction = component.settings.get('CardClass');
|
|
if (faction == 'Weakness') {
|
|
return {
|
|
faction_code: "neutral",
|
|
subtype_code: "weakness",
|
|
};
|
|
} else if (faction == 'BasicWeakness') {
|
|
return {
|
|
faction_code: "neutral",
|
|
subtype_code: "basicweakness",
|
|
};
|
|
} else {
|
|
const card_data = { faction_code: String(faction).toLowerCase() };
|
|
|
|
const faction2 = component.settings.get('CardClass2');
|
|
if (faction2 && faction2 != 'None') {
|
|
card_data.faction2_code = String(faction2).toLowerCase();
|
|
}
|
|
|
|
return card_data;
|
|
}
|
|
}
|
|
|
|
function player_card_common() {
|
|
return Object.assign(
|
|
{
|
|
deck_limit: 2, // TODO: could be derived?
|
|
xp: int_or_null(component.settings.get('Level')),
|
|
},
|
|
skill_icons(),
|
|
faction()
|
|
);
|
|
}
|
|
|
|
function cost() {
|
|
return { cost: int_or_null(component.settings.get('ResourceCost')) };
|
|
}
|
|
|
|
function slots() {
|
|
const card_data = {};
|
|
const raw_slot = component.settings.get('Slot');
|
|
if (raw_slot != 'None') {
|
|
card_data.slot = renameSlot(String(raw_slot));
|
|
const raw_slot2 = component.settings.get('Slot2');
|
|
if (raw_slot2 != 'None') {
|
|
card_data.slot += '. ' + renameSlot(String(raw_slot2));
|
|
}
|
|
}
|
|
|
|
return card_data;
|
|
}
|
|
|
|
function subtitle() {
|
|
const subtitle = component.settings.get('Subtitle');
|
|
if (subtitle != '') {
|
|
return { subname: String(subtitle) };
|
|
} else {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
|
|
function investigator() {
|
|
let back_text = Array(8).fill()
|
|
.map((_, i) => {
|
|
let index = (i + 1);
|
|
let name = substitute_tags(component.settings.get("Text" + index + "NameBack"));
|
|
let text = substitute_tags(component.settings.get("Text" + index + "Back"));
|
|
if (text) {
|
|
return "<b>" + name + "</b>: " + text;
|
|
} else {
|
|
return false;
|
|
}
|
|
})
|
|
.filter(x => x)
|
|
.join("\n");
|
|
|
|
return {
|
|
back_flavor: substitute_tags(component.settings.get('InvStoryBack')),
|
|
back_text: back_text,
|
|
deck_options: ["FIXME"], // TODO
|
|
deck_requirements: "FIXME", // TODO
|
|
double_sided: true,
|
|
is_unique: true,
|
|
skill_agility: int_or_null(component.settings.get('Agility')),
|
|
skill_intellect: int_or_null(component.settings.get('Intellect')),
|
|
skill_combat: int_or_null(component.settings.get('Combat')),
|
|
skill_willpower: int_or_null(component.settings.get('Willpower')),
|
|
};
|
|
}
|
|
|
|
function treachery_subtype() {
|
|
switch (String(component.settings.get('Subtype'))) {
|
|
// TODO: should "StoryWeakness" be different?
|
|
case 'StoryWeakness':
|
|
case 'Weakness':
|
|
return { subtype_code: "weakness" };
|
|
case 'BasicWeakness':
|
|
return { subtype_code: "basicweakness" };
|
|
default:
|
|
throw "Unknown Treachery Subtype:" + String(component.settings.get('Subtype'));
|
|
}
|
|
}
|
|
|
|
function enemy() {
|
|
let subtype;
|
|
switch (component.settings.get('Subtype')) {
|
|
case "Basic Weakness":
|
|
subtype = "basicweakness";
|
|
break;
|
|
case "Weakness":
|
|
// TODO: should these be different?
|
|
case "Investigator Weakness":
|
|
case "Story Weakness":
|
|
subtype = "weakness";
|
|
break;
|
|
}
|
|
|
|
return {
|
|
subtype_code: subtype,
|
|
// TODO: "per investigator" health
|
|
health: int_or_null(component.settings.get('Health')),
|
|
horror: int_or_null(component.settings.get('Horror')),
|
|
attack: int_or_null(component.settings.get('Attack')),
|
|
damage: int_or_null(component.settings.get('Damage')),
|
|
damage: int_or_null(component.settings.get('Damage')),
|
|
evade: int_or_null(component.settings.get('Evade')),
|
|
};
|
|
}
|
|
|
|
function order_by_keys(card_data) {
|
|
return Object.keys(card_data).sort().reduce(
|
|
function(obj, key) {
|
|
obj[key] = card_data[key];
|
|
return obj;
|
|
},
|
|
{}
|
|
);
|
|
}
|
|
|
|
// TODO: parse out some keywords into their own fields
|
|
|
|
println(String(component.getFrontTemplateKey()));
|
|
switch (String(component.getFrontTemplateKey())) {
|
|
case "AHLCG-Event-Default":
|
|
return order_by_keys(Object.assign(
|
|
{ type_code: "event" },
|
|
common_data(),
|
|
player_card_common(),
|
|
cost()
|
|
));
|
|
case "AHLCG-Skill-Default":
|
|
return order_by_keys(Object.assign(
|
|
{ type_code: "skill" },
|
|
common_data(),
|
|
player_card_common()
|
|
));
|
|
case "AHLCG-Asset-Default":
|
|
return order_by_keys(Object.assign(
|
|
{ type_code: "asset" },
|
|
common_data(),
|
|
player_card_common(),
|
|
cost(),
|
|
subtitle(),
|
|
is_unique(),
|
|
health_and_sanity(),
|
|
slots()
|
|
));
|
|
case "AHLCG-Investigator-Default":
|
|
return order_by_keys(Object.assign(
|
|
{ type_code: "investigator", },
|
|
common_data(),
|
|
faction(),
|
|
subtitle(),
|
|
health_and_sanity(),
|
|
investigator(),
|
|
));
|
|
case "AHLCG-WeaknessEnemy-Default":
|
|
return order_by_keys(Object.assign(
|
|
{ type_code: "enemy", },
|
|
common_data(),
|
|
is_unique(),
|
|
enemy()
|
|
));
|
|
case "AHLCG-WeaknessTreachery-Default":
|
|
return order_by_keys(Object.assign(
|
|
{ type_code: "treachery", },
|
|
common_data(),
|
|
treachery_subtype()
|
|
));
|
|
default:
|
|
throw new UnsupportedComponentError();
|
|
}
|
|
}
|
|
|
|
function exportCard(component, file, face) {
|
|
try {
|
|
// create the sheets that will paint the faces of the component
|
|
let sheets = component.createDefaultSheets();
|
|
if (sheets == null) return;
|
|
|
|
// export face
|
|
let image = sheets[face].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() + ' face ' + face + ', skipping file');
|
|
Error.handleUncaught(ex);
|
|
}
|
|
}
|
|
|
|
// Hack to override the default return value of 1
|
|
function copyCount(copies_list, name) {
|
|
const entries = copies_list.getListEntries().map(function (x) {
|
|
return String(x);
|
|
});
|
|
if (entries.indexOf(String(name)) == -1) {
|
|
return 2;
|
|
} else {
|
|
return copies_list.getCopyCount(name);
|
|
}
|
|
}
|
|
|
|
function settingsDialog(task_settings) {
|
|
const pack_code_field = textField(task_settings.get("arkhamdb_pack_code"), 15);
|
|
const cycle_prefix_field = textField(task_settings.get("arkhamdb_cycle_prefix"), 15);
|
|
const panel = new Grid();
|
|
panel.place(
|
|
"Pack Code", "",
|
|
pack_code_field, "grow,span",
|
|
"Cycle Prefix", "",
|
|
cycle_prefix_field, "grow"
|
|
);
|
|
const close_button = panel.createDialog('ArkhamDB Export').showDialog();
|
|
return [close_button, pack_code_field.text, cycle_prefix_field.text];
|
|
}
|
|
|
|
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) {
|
|
const deck_task = ProjectUtilities.simplify(project, task, member);
|
|
const task_settings = deck_task.getSettings();
|
|
const [close_button, pack_code, cycle_prefix] = settingsDialog(task_settings);
|
|
|
|
// User canceled the dialog or closed it without pressing ok
|
|
if (close_button != 1) {
|
|
return;
|
|
}
|
|
task_settings.set("arkhamdb_pack_code", pack_code);
|
|
task_settings.set("arkhamdb_cycle_prefix", cycle_prefix);
|
|
deck_task.writeTaskSettings();
|
|
|
|
Eons.setWaitCursor(true);
|
|
try {
|
|
this.performImpl(deck_task, pack_code, cycle_prefix);
|
|
} catch (ex) {
|
|
Error.handleUncaught(ex);
|
|
} finally {
|
|
Eons.setWaitCursor(false);
|
|
deck_task.synchronize();
|
|
}
|
|
},
|
|
performImpl: function performImpl(deck_task, pack_code, cycle_prefix) {
|
|
let copies_list;
|
|
try {
|
|
copies_list = new CopiesList(deck_task);
|
|
} catch (ex) {
|
|
copies_list = new CopiesList();
|
|
warn("unable to read copies list, using card count of 2 for all files", ex);
|
|
}
|
|
|
|
const children = deck_task.children.filter(function (child) {
|
|
return ProjectUtilities.matchExtension(child, 'eon');
|
|
});
|
|
|
|
const cards = [];
|
|
|
|
for (let child of children) {
|
|
try {
|
|
let component = ResourceKit.getGameComponentFromFile(child.file);
|
|
let copies = copyCount(copies_list, child.baseName);
|
|
let card_data;
|
|
try {
|
|
card_data = build_card(component, pack_code, cycle_prefix, copies);
|
|
} catch (ex) {
|
|
println(ex);
|
|
if (ex instanceof UnsupportedComponentError) {
|
|
println("Skipping unsupported component: " + component.getName());
|
|
continue;
|
|
} else {
|
|
throw ex;
|
|
}
|
|
}
|
|
printf("Generating JSON/PNG for '%s'...\n", child);
|
|
cards.push(card_data);
|
|
|
|
let export_dir = new File(deck_task.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, 0);
|
|
if (double_sided.includes(String(component.getFrontTemplateKey()))) {
|
|
let back_target_file = new File(export_dir, card_data.code + 'b.' + FORMAT);
|
|
exportCard(component, back_target_file, 1);
|
|
}
|
|
}
|
|
} 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(deck_task.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);
|
|
}
|