/* * 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 = { "": "[guardian]", "": "[seeker]", "": "[rogue]", "": "[mystic]", "": "[survivor]", "": "[willpower]", "": "[intellect]", "": "[combat]", "": "[agility]", "": "[wild]", "": "[skull]", "": "[cultist]", "": "[tablet]", "": "[elder_thing]", "": "[bless]", "": "[curse]", "": "[eldersign]", "": "[auto_fail]", "": "[action]", "": "[free]", "": "[reaction]", "": "Forced", "": "Haunted", "": "Objective", "": "Patrol", "": "Revelation", "": "{Unique}", // TODO "": "[per_investigator]", "": "- ", "": "{Square}", // TODO // TODO "": "", // Tab spacing for bullet sections "": "", // Trait "": "", "": "", // Horizontal spacer "": "", // Large vertical spacer "": "", // Vertical spacer "": "", // 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, "", 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 "" + name + ": " + 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 let overrides = {}; try { const comments_json = JSON.parse(component.comment); if ("arkhamdb_override" in comments_json) { overrides = comments_json["arkhamdb_override"]; } } catch (e) {} 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(), overrides, )); case "AHLCG-Skill-Default": return order_by_keys(Object.assign( { type_code: "skill" }, common_data(), player_card_common(), overrides, )); 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(), overrides, )); case "AHLCG-Investigator-Default": return order_by_keys(Object.assign( { type_code: "investigator", }, common_data(), faction(), subtitle(), health_and_sanity(), investigator(), overrides, )); case "AHLCG-WeaknessEnemy-Default": return order_by_keys(Object.assign( { type_code: "enemy", }, common_data(), is_unique(), enemy(), overrides, )); case "AHLCG-WeaknessTreachery-Default": return order_by_keys(Object.assign( { type_code: "treachery", faction_code: "neutral", }, common_data(), treachery_subtype(), overrides, )); 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); }