Compare commits
22 Commits
82345b92bb
...
5b74cfe4b1
Author | SHA1 | Date | |
---|---|---|---|
5b74cfe4b1 | |||
66b0e4be3e | |||
633cd3b94e | |||
f55bed99a2 | |||
908c6a8ba1 | |||
7834b4572e | |||
f72d15098e | |||
a21fc55f83 | |||
25a029e69b | |||
31a367e5fa | |||
ff58e60a95 | |||
8bdb01f084 | |||
4e825e6160 | |||
ce9b34b62f | |||
4ce42c929b | |||
8609f63a7a | |||
412f45390a | |||
8cf31f4272 | |||
82fa427305 | |||
b121beff5a | |||
40050580f4 | |||
2e68593b30 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.log/
|
1
.prettierrc.json
Normal file
1
.prettierrc.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Adam Goldsmith <contact@adamgoldsmith.name>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
42
README.md
Normal file
42
README.md
Normal file
@ -0,0 +1,42 @@
|
||||
# SE3 Tabletop Simulator Deck Generator
|
||||
|
||||
This plugin for [Strange Eons 3](https://strangeeons.cgjennings.ca/index.html) allows you to generate images and corresponding "Saved Object" JSONs for decks which can be used in [Tabletop Simulator](https://tabletopsimulator.com/).
|
||||
|
||||
## Features
|
||||
|
||||
- Creates correctly sized deck images for Tabletop Simulator, splitting into multiple pages when necessary.
|
||||
- Supports multiple copies of cards, without duplicating them in the image.
|
||||
- Creates a "Saved Object" JSON that joins all of the pages into a single deck, and assigns the name of the card.
|
||||
|
||||
## Installation
|
||||
|
||||
Paste this URL into the top bar of the Plug-in Catalog in Strange Eons 3: `https://dev.adamgoldsmith.name/se3plugins/`.
|
||||
You should then be able to just select it from the list of available plugins.
|
||||
See the [official docs](http://se3docs.cgjennings.ca/um-plugins-catalogue.html) for more details.
|
||||
|
||||
Alternatively, you can clone this repository and build/install the plugin following [these instructions from the SE3 docs](http://se3docs.cgjennings.ca/dm-first-plugin.html#building-the-plug-in-bundle).
|
||||
|
||||
## Usage
|
||||
|
||||
With the plugin installed, right click on a Deck task, then select "Generate TTS Deck" from the dropdown menu.
|
||||
|
||||
The plugin will create a JSON file with the same name as the Deck task, as well as a number of JPEG files.
|
||||
Copy/move/symlink the JSON file to the `Saves/Saved Objects` folder in your [Tabletop Simulator Save Game Data](https://kb.tabletopsimulator.com/getting-started/technical-info/#save-game-data-location).
|
||||
Do not move the jpg files; their paths are absolute and must be changed in the JSON file if you move them.
|
||||
|
||||
The absolute paths to deck images are written into the TTS json, so it will only work locally unless you upload the images, and correct the URLs in the JSON file.
|
||||
Alternatively, you should in theory be able to use the [Upload All](https://kb.tabletopsimulator.com/custom-content/cloud-manager/#upload-all) feature in Tabletop Simulator to upload them to the Steam Cloud, although I have not have any success with this.
|
||||
|
||||
The first time you run the plugin will take a very long time, but future runs will be faster as the cards' images will be cached.
|
||||
|
||||
### Copies
|
||||
|
||||
This plugin supports the [Copies File](http://se3docs.cgjennings.ca/um-proj-deck-task.html#the-copies-file) for indicating the number of copies of a card to create.
|
||||
|
||||
## Limitations
|
||||
|
||||
This only supports decks of uniformly sized, single-sided cards (ie all with the same back).
|
||||
(Note that rotated cards do not count as being the same size for this purpose).
|
||||
The workaround for this is just to put each size/type of card into it's own deck.
|
||||
|
||||
The back image is taken from the first card in the deck.
|
10
eons-plugin
10
eons-plugin
@ -2,6 +2,12 @@
|
||||
# TTSDeck Root File
|
||||
#
|
||||
|
||||
id = CATALOGUEID{b601af40-5e79-4dd3-b920-0d8b215b7d77:2021-8-16-20-36-2-56}
|
||||
id = CATALOGUEID{b601af40-5e79-4dd3-b920-0d8b215b7d77:2024-8-22-17-22-14-302}
|
||||
|
||||
res://TTSDeck.js
|
||||
catalog-name = Tabletop Simulator Deck Generator
|
||||
catalog-credit = Adam Goldsmith <contact@adamgoldsmith.name>
|
||||
catalog-homepage = https://github.com/ad1217/SE3-TTSDeck
|
||||
catalog-description = This plugin for allows you to generate images and \
|
||||
corresponding "Saved Object" JSONs for decks which can be used in Tabletop Simulator.
|
||||
|
||||
res://ttsdeck/plugin.js
|
||||
|
@ -1,204 +0,0 @@
|
||||
/*
|
||||
* TTSDeck.js
|
||||
*
|
||||
* Creates a deck image and corresponding "Saved Object" JSON for use
|
||||
* in Tabletop Simulator
|
||||
*/
|
||||
|
||||
useLibrary('project');
|
||||
useLibrary('imageutils');
|
||||
useLibrary('threads');
|
||||
useLibrary('uilayout');
|
||||
importClass(arkham.project.CopiesList);
|
||||
|
||||
const TTSJson = require('./TTSJson.js');
|
||||
|
||||
// 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;
|
||||
|
||||
const TTS_CARDS_PER_IMAGE = 69;
|
||||
const TTS_MAX_ROWS = 7;
|
||||
|
||||
|
||||
const getName = () => 'TTSDeck';
|
||||
const getDescription = () => 'Generates a TTS deck image and JSON file';
|
||||
const getVersion = () => 1.0;
|
||||
const getPluginType = () => arkham.plugins.Plugin.INJECTED;
|
||||
|
||||
function unload() {
|
||||
unregisterAll();
|
||||
}
|
||||
|
||||
// Creates a test button during development that calls unload() to clean up.
|
||||
testProjectScript();
|
||||
|
||||
// Hack to override the default return value of 1
|
||||
function copyCount(copies_list, name) {
|
||||
const entries = copies_list.getListEntries().map(x => String(x));
|
||||
if (entries.indexOf(String(name)) == -1) {
|
||||
return 2;
|
||||
} else {
|
||||
return copies_list.getCopyCount(name);
|
||||
}
|
||||
}
|
||||
|
||||
// export front face, or retrive it from a cached file
|
||||
// TODO: handle two-sided cards
|
||||
function makeCardImage(card) {
|
||||
const component = ResourceKit.getGameComponentFromFile(card.file);
|
||||
|
||||
const cache_dir = new File(card.parent.file, '.ttsdeck_cache');
|
||||
const cached_file = new File(cache_dir, card.file.name + '.' + FORMAT);
|
||||
|
||||
if (cached_file.exists() && cached_file.lastModified() > card.file.lastModified()) {
|
||||
println("Got cached image for card", card);
|
||||
return ImageUtils.read(cached_file);
|
||||
} else {
|
||||
println("Generating image for card ", card);
|
||||
const sheets = component.createDefaultSheets();
|
||||
const card_image = sheets[0].paint(arkham.sheet.RenderTarget.EXPORT, RESOLUTION);
|
||||
|
||||
cache_dir.mkdir();
|
||||
ImageUtils.write(card_image, cached_file, FORMAT, -1, false, RESOLUTION);
|
||||
|
||||
return card_image;
|
||||
}
|
||||
}
|
||||
|
||||
function TTSDeckPage(busy_props, page_num, page_cards, copies_list) {
|
||||
this.rows = Math.min(Math.ceil(Math.sqrt(page_cards.length)), TTS_MAX_ROWS);
|
||||
this.columns = Math.ceil(page_cards.length / this.rows);
|
||||
this.deck_image = null;
|
||||
let deck_graphics;
|
||||
|
||||
this.card_jsons = [];
|
||||
for (let row = 0; row < this.rows; row++) {
|
||||
for (let col = 0; col < this.columns && row * this.columns + col < page_cards.length; col++) {
|
||||
if (busy_props.cancelled) return;
|
||||
let index = row * this.columns + col;
|
||||
let card = page_cards[index];
|
||||
busy_props.status = "Processing Card " + card;
|
||||
busy_props.currentProgress = (page_num - 1) * TTS_CARDS_PER_IMAGE + index;
|
||||
|
||||
try {
|
||||
let component = ResourceKit.getGameComponentFromFile(card.file);
|
||||
let copies = copyCount(copies_list, card.baseName);
|
||||
|
||||
for (let ii = 0; ii < copies; ii++) {
|
||||
this.card_jsons.push(TTSJson.makeCardJSON(page_num * 100 + index, component.getName()));
|
||||
}
|
||||
|
||||
let card_image = makeCardImage(card);
|
||||
|
||||
if (!this.deck_image) {
|
||||
this.deck_image = ImageUtils.create(
|
||||
card_image.width * this.columns, card_image.height * this.rows, false);
|
||||
deck_graphics = this.deck_image.createGraphics();
|
||||
}
|
||||
|
||||
deck_graphics.drawImage(card_image, col * card_image.width, row * card_image.height, null);
|
||||
} catch (ex) {
|
||||
alert('Error while processing ' + card + ': ' + ex, true);
|
||||
}
|
||||
}
|
||||
println("End of Row ", row);
|
||||
}
|
||||
|
||||
// TODO: this should either prompt the user or provde automatic uploading somewhere
|
||||
this.face_url = String((new File(page_cards[0].parent.file,
|
||||
page_cards[0].parent.getName() + '_' + page_num + '.' + FORMAT)).toPath().toUri());
|
||||
this.back_url = "TODO";
|
||||
}
|
||||
|
||||
function makeTTSDeck(busy_props, cards, copies_list) {
|
||||
const pages = [];
|
||||
|
||||
busy_props.title = "Processing Cards";
|
||||
busy_props.maximumProgress = cards.length;
|
||||
|
||||
for (let page_num = 0; page_num * TTS_CARDS_PER_IMAGE < cards.length; page_num++) {
|
||||
let page_cards = cards.slice(page_num * TTS_CARDS_PER_IMAGE, (page_num + 1) * TTS_CARDS_PER_IMAGE);
|
||||
printf("Making page %d, with %d cards:\n", page_num + 1, page_cards.length);
|
||||
pages.push(new TTSDeckPage(busy_props, page_num + 1, page_cards, copies_list));
|
||||
if (busy_props.cancelled) return [,];
|
||||
}
|
||||
|
||||
const deck_json = TTSJson.makeDeckJSON(pages);
|
||||
|
||||
return [deck_json, pages.map(page => page.deck_image)];
|
||||
}
|
||||
|
||||
function run() {
|
||||
const ttsDeckAction = JavaAdapter(TaskAction, {
|
||||
getLabel: () => 'Generate TTS Deck',
|
||||
getActionName: () => 'ttsdeck',
|
||||
// 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 {
|
||||
Thread.busyWindow(
|
||||
(busy_props) => this.performImpl(busy_props, member),
|
||||
'Setting up...',
|
||||
true);
|
||||
} catch (ex) {
|
||||
Error.handleUncaught(ex);
|
||||
} finally {
|
||||
Eons.setWaitCursor(false);
|
||||
}
|
||||
},
|
||||
performImpl: function performImpl(busy_props, member) {
|
||||
let copies_list;
|
||||
try {
|
||||
copies_list = new CopiesList(member);
|
||||
} catch (ex) {
|
||||
copies_list = new CopiesList();
|
||||
alert("unable to read copies list, using card count of 2 for all files", true);
|
||||
}
|
||||
|
||||
const children = member.getChildren();
|
||||
const page_cards = children.filter(child => {
|
||||
if (ProjectUtilities.matchExtension(child, 'eon')) {
|
||||
let component = ResourceKit.getGameComponentFromFile(child.file);
|
||||
return component.isDeckLayoutSupported();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const [deck_json, deck_images] = makeTTSDeck(busy_props, page_cards, copies_list);
|
||||
if (busy_props.cancelled) return;
|
||||
const saved_object = TTSJson.makeSavedObjectJSON([deck_json], member.getName());
|
||||
|
||||
busy_props.status = "";
|
||||
busy_props.maximumProgress = -1;
|
||||
busy_props.title = "Writing JSON";
|
||||
const json_file = new File(member.file, member.getName() + '.json');
|
||||
ProjectUtilities.writeTextFile(json_file, JSON.stringify(saved_object, null, 4));
|
||||
|
||||
busy_props.title = "Writing Images";
|
||||
busy_props.maximumProgress = deck_images.length;
|
||||
deck_images.forEach((deck_image, index) => {
|
||||
busy_props.currentProgress = index;
|
||||
const image_file = new File(member.file, member.getName() + '_' + (index + 1) + '.' + FORMAT);
|
||||
ImageUtils.write(deck_image, image_file, FORMAT, -1, false, RESOLUTION);
|
||||
});
|
||||
|
||||
member.synchronize();
|
||||
}
|
||||
});
|
||||
|
||||
ActionRegistry.register(ttsDeckAction, Actions.PRIORITY_IMPORT_EXPORT);
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
// Helper functions for Tabletop Simulator JSON output
|
||||
|
||||
exports.makeCardJSON = function makeCardJSON(card_id, nickname, description) {
|
||||
return {
|
||||
Name: "Card",
|
||||
Transform: {
|
||||
posX: 0,
|
||||
posY: 0,
|
||||
posZ: 0,
|
||||
rotX: 0,
|
||||
rotY: 0,
|
||||
rotZ: 0,
|
||||
scaleX: 1.0,
|
||||
scaleY: 1.0,
|
||||
scaleZ: 1.0,
|
||||
},
|
||||
Nickname: String(nickname),
|
||||
CardID: card_id,
|
||||
Description: String(description || ""),
|
||||
ColorDiffuse: {
|
||||
r: 0.713235259,
|
||||
g: 0.713235259,
|
||||
b: 0.713235259,
|
||||
},
|
||||
Locked: false,
|
||||
Grid: true,
|
||||
Snap: true,
|
||||
Autoraise: true,
|
||||
Sticky: true,
|
||||
Tooltip: true,
|
||||
SidewaysCard: false,
|
||||
};
|
||||
};
|
||||
|
||||
exports.makeDeckJSON = function makeDeckJSON(pages, nickname, description) {
|
||||
return {
|
||||
Name: "DeckCustom",
|
||||
Transform: {
|
||||
posX: 0,
|
||||
posY: 0,
|
||||
posZ: 0,
|
||||
rotX: 0,
|
||||
rotY: 0.0,
|
||||
rotZ: 0.0,
|
||||
scaleX: 1.0,
|
||||
scaleY: 1.0,
|
||||
scaleZ: 1.0,
|
||||
},
|
||||
Nickname: String(nickname || ""),
|
||||
Description: String(description || ""),
|
||||
ColorDiffuse: {
|
||||
r: 0.713239133,
|
||||
g: 0.713239133,
|
||||
b: 0.713239133,
|
||||
},
|
||||
Grid: true,
|
||||
Locked: false,
|
||||
SidewaysCard: false,
|
||||
DeckIDs: pages
|
||||
.map(page => page.card_jsons.map(card => card.CardID))
|
||||
.reduce((acc, val) => acc.concat(val), []),
|
||||
CustomDeck: pages.reduce((acc, page, index) => {
|
||||
acc[String(index + 1)] = {
|
||||
FaceURL: String(page.face_url),
|
||||
BackURL: String(page.back_url),
|
||||
NumWidth: page.columns,
|
||||
NumHeight: page.rows,
|
||||
BackIsHidden: true,
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
ContainedObjects: pages
|
||||
.map(page => page.card_jsons)
|
||||
.reduce((acc, val) => acc.concat(val), []),
|
||||
};
|
||||
};
|
||||
|
||||
exports.makeSavedObjectJSON = function makeSavedObjectJSON(objects, save_name) {
|
||||
return {
|
||||
SaveName: String(save_name || ""),
|
||||
GameMode: "",
|
||||
Date: "",
|
||||
Table: "",
|
||||
Sky: "",
|
||||
Note: "",
|
||||
Rules: "",
|
||||
PlayerTurn: "",
|
||||
ObjectStates: objects,
|
||||
};
|
||||
};
|
115
resources/ttsdeck/Card.js
Normal file
115
resources/ttsdeck/Card.js
Normal file
@ -0,0 +1,115 @@
|
||||
useLibrary("project");
|
||||
|
||||
function Card(member, arkhamdb_cycle_prefix, copies_list) {
|
||||
this.member = member;
|
||||
this.arkhamdb_cycle_prefix = arkhamdb_cycle_prefix;
|
||||
this.copies_list = copies_list;
|
||||
|
||||
this.component = ResourceKit.getGameComponentFromFile(member.file);
|
||||
}
|
||||
|
||||
Card.getImageFile = function getImageFile(parent, format, page_num) {
|
||||
return new File(
|
||||
parent.file,
|
||||
parent.getName() + "_" + page_num + "." + format
|
||||
);
|
||||
};
|
||||
|
||||
Card.prototype.makeImageUncached = function makeImageUncached(resolution, back) {
|
||||
println("Generating image for card ", this.member);
|
||||
const sheets = this.component.createDefaultSheets();
|
||||
const card_image = sheets[back ? 1 : 0].paint(
|
||||
arkham.sheet.RenderTarget.EXPORT,
|
||||
resolution
|
||||
);
|
||||
|
||||
return card_image;
|
||||
};
|
||||
|
||||
// export front face, or retrive it from a cached file
|
||||
// TODO: handle two-sided cards
|
||||
Card.prototype.makeImage = function makeImage(format, resolution) {
|
||||
const cache_dir = new File(this.member.parent.file, ".ttsdeck_cache");
|
||||
const cached_file = new File(cache_dir, this.member.file.name + "." + format);
|
||||
|
||||
if (
|
||||
cached_file.exists() &&
|
||||
cached_file.lastModified() > this.member.file.lastModified()
|
||||
) {
|
||||
println("Got cached image for card", this.member);
|
||||
return ImageUtils.read(cached_file);
|
||||
} else {
|
||||
const card_image = this.makeImageUncached(resolution);
|
||||
|
||||
cache_dir.mkdir();
|
||||
ImageUtils.write(card_image, cached_file, format, -1, false, resolution);
|
||||
|
||||
return card_image;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: allow setting a default copy count
|
||||
// Hack to override the default return value of 1
|
||||
Card.prototype.copyCount = function copyCount() {
|
||||
const entries = this.copies_list.getListEntries().map((x) => String(x));
|
||||
if (entries.indexOf(String(this.member.baseName)) == -1) {
|
||||
return 1;
|
||||
} else {
|
||||
return this.copies_list.getCopyCount(this.member.baseName);
|
||||
}
|
||||
};
|
||||
|
||||
Card.prototype.makeJSON = function makeJSON(card_id, description) {
|
||||
let card = {
|
||||
Name: "Card",
|
||||
Transform: {
|
||||
posX: 0,
|
||||
posY: 0,
|
||||
posZ: 0,
|
||||
rotX: 0,
|
||||
rotY: 0,
|
||||
rotZ: 0,
|
||||
scaleX: 1.0,
|
||||
scaleY: 1.0,
|
||||
scaleZ: 1.0,
|
||||
},
|
||||
Nickname: this.component.getName(),
|
||||
CardID: card_id,
|
||||
Description: description || "",
|
||||
ColorDiffuse: {
|
||||
r: 0.713235259,
|
||||
g: 0.713235259,
|
||||
b: 0.713235259,
|
||||
},
|
||||
Locked: false,
|
||||
Grid: true,
|
||||
Snap: true,
|
||||
Autoraise: true,
|
||||
Sticky: true,
|
||||
Tooltip: true,
|
||||
SidewaysCard: false,
|
||||
};
|
||||
|
||||
// TODO: could also do other fields, like "uses"
|
||||
// Hack for AHLCG SCED deckbuilder
|
||||
if (this.arkhamdb_cycle_prefix) {
|
||||
let arkhamdb_id =
|
||||
this.arkhamdb_cycle_prefix +
|
||||
String(this.component.settings.get("CollectionNumber")).padStart(3, "0");
|
||||
|
||||
let overrides = {};
|
||||
try {
|
||||
const comments_json = JSON.parse(this.component.comment);
|
||||
if ("tts_gmnotes_override" in comments_json) {
|
||||
overrides = comments_json["tts_gmnotes_override"];
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
let gmnotes = Object.assign({ id: arkhamdb_id }, overrides);
|
||||
card.GMNotes = JSON.stringify(gmnotes);
|
||||
}
|
||||
|
||||
return card;
|
||||
};
|
||||
|
||||
module.exports = Card;
|
174
resources/ttsdeck/TTSDeck.js
Normal file
174
resources/ttsdeck/TTSDeck.js
Normal file
@ -0,0 +1,174 @@
|
||||
useLibrary("threads");
|
||||
|
||||
const Card = require("ttsdeck/Card.js");
|
||||
|
||||
const TTS_CARDS_PER_IMAGE = 69;
|
||||
const TTS_MAX_ROWS = 7;
|
||||
|
||||
function TTSDeckPage(image_format, image_resolution, page_num, cards) {
|
||||
this.image_format = image_format;
|
||||
this.image_resolution = image_resolution;
|
||||
this.page_num = page_num;
|
||||
this.cards = cards;
|
||||
|
||||
this.rows = Math.min(Math.ceil(Math.sqrt(cards.length)), TTS_MAX_ROWS);
|
||||
this.columns = Math.ceil(cards.length / this.rows);
|
||||
this.deck_image = null;
|
||||
this.card_jsons = [];
|
||||
|
||||
this.face_url = String(
|
||||
Card.getImageFile(cards[0].member.parent, image_format, this.page_num)
|
||||
.toPath()
|
||||
.toUri()
|
||||
);
|
||||
this.back_url = String(
|
||||
Card.getImageFile(cards[0].member.parent, image_format, "back")
|
||||
.toPath()
|
||||
.toUri()
|
||||
);
|
||||
}
|
||||
|
||||
TTSDeckPage.prototype.build = function build(busy_props) {
|
||||
let deck_graphics;
|
||||
|
||||
for (let row = 0; row < this.rows; row++) {
|
||||
for (
|
||||
let col = 0;
|
||||
col < this.columns && row * this.columns + col < this.cards.length;
|
||||
col++
|
||||
) {
|
||||
if (busy_props.cancelled) return this;
|
||||
|
||||
let index = row * this.columns + col;
|
||||
let card = this.cards[index];
|
||||
busy_props.status = "Processing Card " + card.member;
|
||||
busy_props.currentProgress =
|
||||
(this.page_num - 1) * TTS_CARDS_PER_IMAGE + index;
|
||||
|
||||
try {
|
||||
let copies = card.copyCount();
|
||||
|
||||
for (let ii = 0; ii < copies; ii++) {
|
||||
this.card_jsons.push(card.makeJSON(this.page_num * 100 + index));
|
||||
}
|
||||
|
||||
let card_image = card.makeImage(
|
||||
this.image_format,
|
||||
this.image_resolution
|
||||
);
|
||||
|
||||
if (!this.deck_image) {
|
||||
this.deck_image = ImageUtils.create(
|
||||
card_image.width * this.columns,
|
||||
card_image.height * this.rows,
|
||||
false
|
||||
);
|
||||
deck_graphics = this.deck_image.createGraphics();
|
||||
}
|
||||
|
||||
deck_graphics.drawImage(
|
||||
card_image,
|
||||
col * card_image.width,
|
||||
row * card_image.height,
|
||||
null
|
||||
);
|
||||
} catch (ex) {
|
||||
Thread.invokeLater(() =>
|
||||
alert("Error while processing " + card + ": " + ex, true)
|
||||
);
|
||||
}
|
||||
}
|
||||
println("End of Row ", row);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
function TTSDeck(image_format, image_resolution, cards) {
|
||||
this.image_format = image_format;
|
||||
this.image_resolution = image_resolution;
|
||||
this.cards = cards;
|
||||
|
||||
this.pages = [];
|
||||
}
|
||||
|
||||
TTSDeck.prototype.build = function build(busy_props) {
|
||||
busy_props.title = "Processing Cards";
|
||||
busy_props.maximumProgress = this.cards.length;
|
||||
|
||||
for (
|
||||
let page_num = 0;
|
||||
page_num * TTS_CARDS_PER_IMAGE < this.cards.length;
|
||||
page_num++
|
||||
) {
|
||||
if (busy_props.cancelled) return this;
|
||||
|
||||
let page_cards = this.cards.slice(
|
||||
page_num * TTS_CARDS_PER_IMAGE,
|
||||
(page_num + 1) * TTS_CARDS_PER_IMAGE
|
||||
);
|
||||
printf("Making page %d, with %d cards:\n", page_num + 1, page_cards.length);
|
||||
this.pages.push(
|
||||
new TTSDeckPage(
|
||||
this.image_format,
|
||||
this.image_resolution,
|
||||
page_num + 1,
|
||||
page_cards
|
||||
).build(busy_props)
|
||||
);
|
||||
}
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
TTSDeck.prototype.getImages = function getImages() {
|
||||
return this.pages.map((page) => page.deck_image);
|
||||
};
|
||||
|
||||
TTSDeck.prototype.makeJSON = function makeJSON(nickname, description) {
|
||||
return {
|
||||
Name: "DeckCustom",
|
||||
Transform: {
|
||||
posX: 0,
|
||||
posY: 0,
|
||||
posZ: 0,
|
||||
rotX: 0,
|
||||
rotY: 0.0,
|
||||
rotZ: 0.0,
|
||||
scaleX: 1.0,
|
||||
scaleY: 1.0,
|
||||
scaleZ: 1.0,
|
||||
},
|
||||
Nickname: nickname || "",
|
||||
Description: description || "",
|
||||
ColorDiffuse: {
|
||||
r: 0.713239133,
|
||||
g: 0.713239133,
|
||||
b: 0.713239133,
|
||||
},
|
||||
Grid: true,
|
||||
Locked: false,
|
||||
SidewaysCard: false,
|
||||
DeckIDs: this.pages
|
||||
.map((page) => page.card_jsons.map((card) => card.CardID))
|
||||
.reduce((acc, val) => acc.concat(val), []),
|
||||
CustomDeck: this.pages.reduce((acc, page, index) => {
|
||||
acc[String(index + 1)] = {
|
||||
FaceURL: page.face_url,
|
||||
BackURL: page.back_url,
|
||||
NumWidth: page.columns,
|
||||
NumHeight: page.rows,
|
||||
BackIsHidden: true,
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
ContainedObjects: this.pages
|
||||
.map((page) => page.card_jsons)
|
||||
.reduce((acc, val) => acc.concat(val), []),
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
TTSDeckPage: TTSDeckPage,
|
||||
TTSDeck: TTSDeck,
|
||||
};
|
15
resources/ttsdeck/TTSJson.js
Normal file
15
resources/ttsdeck/TTSJson.js
Normal file
@ -0,0 +1,15 @@
|
||||
// Helper functions for Tabletop Simulator JSON output
|
||||
|
||||
exports.makeSavedObjectJSON = function makeSavedObjectJSON(objects, save_name) {
|
||||
return {
|
||||
SaveName: save_name || "",
|
||||
GameMode: "",
|
||||
Date: "",
|
||||
Table: "",
|
||||
Sky: "",
|
||||
Note: "",
|
||||
Rules: "",
|
||||
PlayerTurn: "",
|
||||
ObjectStates: objects,
|
||||
};
|
||||
};
|
207
resources/ttsdeck/plugin.js
Normal file
207
resources/ttsdeck/plugin.js
Normal file
@ -0,0 +1,207 @@
|
||||
/*
|
||||
* TTSDeck.js
|
||||
*
|
||||
* Creates a deck image and corresponding "Saved Object" JSON for use
|
||||
* in Tabletop Simulator
|
||||
*/
|
||||
|
||||
useLibrary("project");
|
||||
useLibrary("imageutils");
|
||||
useLibrary("threads");
|
||||
useLibrary("uilayout");
|
||||
useLibrary("uicontrols");
|
||||
importClass(arkham.project.CopiesList);
|
||||
|
||||
const Card = require("ttsdeck/Card.js");
|
||||
const { TTSDeck } = require("ttsdeck/TTSDeck.js");
|
||||
const TTSJson = require("ttsdeck/TTSJson.js");
|
||||
|
||||
const getName = () => "TTSDeck";
|
||||
const getDescription = () => "Generates a TTS deck image and JSON file";
|
||||
const getVersion = () => 1.0;
|
||||
const getPluginType = () => arkham.plugins.Plugin.INJECTED;
|
||||
|
||||
function unload() {
|
||||
unregisterAll();
|
||||
}
|
||||
|
||||
// Creates a test button during development that calls unload() to clean up.
|
||||
testProjectScript();
|
||||
|
||||
function settingsDialog(deck_task) {
|
||||
const task_settings = deck_task.getSettings();
|
||||
|
||||
const image_format_field = comboBox([
|
||||
ImageUtils.FORMAT_JPEG,
|
||||
ImageUtils.FORMAT_PNG,
|
||||
]);
|
||||
image_format_field.setSelectedItem(
|
||||
task_settings.get("tts_image_format", "jpg")
|
||||
);
|
||||
const resolution_field = textField(
|
||||
task_settings.get("tts_image_resolution", "200"),
|
||||
15
|
||||
);
|
||||
|
||||
const clear_cache_button = button("Clear Cache", undefined, function (e) {
|
||||
const cache_dir = new File(deck_task.file, ".ttsdeck_cache");
|
||||
cache_dir.listFiles().forEach((file) => file.delete());
|
||||
});
|
||||
|
||||
const panel = new Grid();
|
||||
// prettier-ignore
|
||||
panel.place(
|
||||
"Image Format", "",
|
||||
image_format_field, "grow,span",
|
||||
"Resolution", "",
|
||||
resolution_field, "grow,span",
|
||||
clear_cache_button, "grow,span"
|
||||
);
|
||||
const close_button = panel.createDialog("TTS Export").showDialog();
|
||||
return [
|
||||
close_button,
|
||||
image_format_field.getSelectedItem(),
|
||||
Number(resolution_field.text),
|
||||
];
|
||||
}
|
||||
|
||||
function run() {
|
||||
const ttsDeckAction = JavaAdapter(TaskAction, {
|
||||
getLabel: () => "Generate TTS Deck",
|
||||
getActionName: () => "ttsdeck",
|
||||
// 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) {
|
||||
let deck_task = ProjectUtilities.simplify(project, task, member);
|
||||
const [close_button, image_format, image_resolution] =
|
||||
settingsDialog(deck_task);
|
||||
|
||||
// User canceled the dialog or closed it without pressing ok
|
||||
if (close_button != 1) {
|
||||
return;
|
||||
}
|
||||
// persist settings
|
||||
const task_settings = deck_task.getSettings();
|
||||
task_settings.set("tts_image_format", image_format);
|
||||
task_settings.set("tts_image_resolution", image_resolution);
|
||||
deck_task.writeTaskSettings();
|
||||
|
||||
// If used with arkhamdb, get cycle prefix from settings
|
||||
const arkhamdb_cycle_prefix = task_settings.get("arkhamdb_cycle_prefix");
|
||||
|
||||
Eons.setWaitCursor(true);
|
||||
try {
|
||||
Thread.busyWindow(
|
||||
(busy_props) =>
|
||||
this.performImpl(
|
||||
busy_props,
|
||||
image_format,
|
||||
image_resolution,
|
||||
arkhamdb_cycle_prefix,
|
||||
deck_task
|
||||
),
|
||||
"Setting up...",
|
||||
true
|
||||
);
|
||||
} catch (ex) {
|
||||
Error.handleUncaught(ex);
|
||||
} finally {
|
||||
Eons.setWaitCursor(false);
|
||||
}
|
||||
},
|
||||
performImpl: function performImpl(
|
||||
busy_props,
|
||||
image_format,
|
||||
image_resolution,
|
||||
arkhamdb_cycle_prefix,
|
||||
member
|
||||
) {
|
||||
let copies_list;
|
||||
try {
|
||||
copies_list = new CopiesList(member);
|
||||
} catch (ex) {
|
||||
copies_list = new CopiesList();
|
||||
alert(
|
||||
"unable to read copies list, using card count of 2 for all files",
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const children = member.getChildren();
|
||||
const cards = children
|
||||
.map((child) => {
|
||||
if (ProjectUtilities.matchExtension(child, "eon")) {
|
||||
let card = new Card(child, arkhamdb_cycle_prefix, copies_list);
|
||||
if (card.component.isDeckLayoutSupported()) {
|
||||
return card;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter((card) => card !== undefined);
|
||||
|
||||
const deck = new TTSDeck(
|
||||
image_format,
|
||||
image_resolution,
|
||||
cards,
|
||||
copies_list
|
||||
).build(busy_props);
|
||||
|
||||
if (busy_props.cancelled) return;
|
||||
|
||||
const saved_object = TTSJson.makeSavedObjectJSON(
|
||||
[deck.makeJSON()],
|
||||
member.getName()
|
||||
);
|
||||
|
||||
busy_props.status = "";
|
||||
busy_props.maximumProgress = -1;
|
||||
busy_props.title = "Writing JSON";
|
||||
const json_file = new File(member.file, member.getName() + ".json");
|
||||
ProjectUtilities.writeTextFile(
|
||||
json_file,
|
||||
JSON.stringify(saved_object, null, 4)
|
||||
);
|
||||
|
||||
const deck_images = deck.getImages();
|
||||
busy_props.title = "Writing Images";
|
||||
busy_props.maximumProgress = deck_images.length;
|
||||
deck_images.forEach((deck_image, index) => {
|
||||
busy_props.currentProgress = index;
|
||||
const image_file = Card.getImageFile(member, image_format, index + 1);
|
||||
ImageUtils.write(
|
||||
deck_image,
|
||||
image_file,
|
||||
image_format,
|
||||
-1,
|
||||
false,
|
||||
image_resolution
|
||||
);
|
||||
});
|
||||
|
||||
let back_image = cards[0].makeImageUncached(image_resolution, true);
|
||||
const back_image_file = Card.getImageFile(member, image_format, "back");
|
||||
ImageUtils.write(
|
||||
back_image,
|
||||
back_image_file,
|
||||
image_format,
|
||||
-1,
|
||||
false,
|
||||
image_resolution
|
||||
);
|
||||
|
||||
member.synchronize();
|
||||
},
|
||||
});
|
||||
|
||||
ActionRegistry.register(ttsDeckAction, Actions.PRIORITY_IMPORT_EXPORT);
|
||||
}
|
Loading…
Reference in New Issue
Block a user