// ==UserScript==
// @name Scryfall TTS deck save
// @namespace http://scryfall.com/
// @version 0.2
// @description Generate Tabletop Simulator deck object json
// @author hyper
// @match https://scryfall.com/*/decks/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
const controls = document.querySelectorAll('.deck-controls-group')[1];
controls.append(createSaveButton());
})();
function createExportButton(){
const btn = document.createElement('button');
btn.className = 'button-n tiny bp-mid-only';
btn.type = 'button';
btn.innerHTML = '<b>TTS Main</b>';
btn.onclick = () => showsheet();
return btn;
}
function createTokensButton(){
const btn = document.createElement('button');
btn.className = 'button-n tiny bp-mid-only';
btn.type = 'button';
btn.innerHTML = '<b>TTS Side & Tokens</b>';
btn.onclick = () => showsheet(false, true);
return btn;
}
function createSaveButton(){
const btn = document.createElement('button');
btn.className = 'button-n tiny bp-mid-only';
btn.type = 'button';
btn.innerHTML = '<b>TTS Save</b>';
btn.onclick = () => downloadSave();
return btn;
}
async function showsheet(main=true, tokens=false){
const imgs = await collectImages(main, tokens);
const img = createCanvasImage(imgs.flat());
img.style.maxWidth='100%';
img.style.maxHeight='100%';
const container = document.createElement('div');
container.style.position = 'fixed';
container.style.top = '0';
container.style.left = '0';
container.style.width = '100vw';
container.style.height = '100vh';
container.style.zIndex = '1000';
container.style.padding = '10vw';
container.style.background = 'rgba(0,0,0,.7)';
container.append(img);
container.onclick = () => container.remove();
document.body.append(container);
}
function loadImg(url){
const img = document.createElement('img');
img.src = url;
return new Promise(resolve =>{
img.onload = () => resolve(img);
});
}
async function loadCard(url){
const res = await fetch(url);
const json = await res.json();
const imguris = json.image_uris || json.card_faces[0].image_uris;
const related = (json.all_parts || []).slice(1);
const name = json.card_faces ? json.card_faces.map(cf => cf.printed_name || cf.name).join(' // ') : json.printed_name || json.name;
const cost = json.mana_cost;
const typeline = json.card_faces ? json.card_faces.map(cf => cf.printed_type_line || cf.type_line).join(' // ') : json.printed_type_line || json.type_line;
const stats = json.power ? `${json.power}/${json.toughness}` : undefined;
const oracle = json.card_faces ? json.card_faces.map(cf => cf.printed_text || cf.oracle_text).join(' // ') : json.printed_text || json.oracle_text;
const head = `${name} - ${cost}\n${stats || ""} ${typeline}`;
return [imguris.large, related, head, oracle];
}
async function collectCards(main=true, tokens=false){
const cards = [...document.querySelectorAll('.deck-list-entry')].map(
async item => {
const side = item.parentElement.parentElement.firstElementChild.innerText.startsWith('SIDEBOARD');
const num = parseInt(item.querySelector('.deck-list-entry-count').innerText);
const cardUrl = item.querySelector('a').href;
const url = cardUrl.slice(0, cardUrl.lastIndexOf('/')).replace('scryfall', 'api.scryfall').replace('/card/', '/cards/');
const card = await loadCard(url);
const cards = [];
if( (main && !side) || (tokens && side)){
for(let i = 0; i < num; i ++)
cards.push(card);
}
if(tokens){
const tokens = card[1];
const loaded = tokens.map(c => loadCard(c.uri));
cards.push(...await Promise.all(loaded));
}
return cards;
}
);
return (await Promise.all(cards)).flat();
}
async function collectImages(main=true, tokens=false){
return await Promise.all(collectCards(main, tokens).map(card => loadImg(card[0])));
}
function createCanvasImage(imgs){
const w = 672;
const h = 936;
const canvas = document.createElement('canvas');
canvas.width = w * 10;
canvas.height = h * 7;
const ctx = canvas.getContext('2d');
let x = 0;
let y = 0;
function inc(){
x+=1;
if(x>=10){
x-=10;
y+=1;
}
}
for(const img of imgs){
ctx.drawImage(img, x * w, y * h);
inc();
}
return canvas;
}
async function downloadSave(){
const cards = await collectCards();
const cardItems = cards.map((c,i) => makeTTSCard({name: c[2], oracle: c[3], face: c[0], id: i+1}));
const deck = makeTTSDeck(cardItems);
deck.Nickname = "Main";
const sides = await collectCards(false, true);
const sideItems = sides.map((c,i) => makeTTSCard({name: c[2], oracle: c[3], face: c[0], id: i+1}));
const sideDeck = makeTTSDeck(sideItems);
sideDeck.Transform.posX = 3;
sideDeck.Nickname = "Sideboard";
const name = document.querySelector(".deck-details-title").innerText.trim();
const save = makeTTSSave(deck, sideDeck);
download(name + ".json", JSON.stringify(save, null, 4));
}
function download(filename, text) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
function makeTTSCard({name, oracle, face, id}){
const card = {...TTSCard};
card.Nickname = name;
card.Description = oracle;
card.CardID = id * 100;
card.CustomDeck = {};
card.CustomDeck[id] = {
FaceURL: face,
BackURL: "http://ww1.sinaimg.cn/large/8239391egy1gg9acxj0ryj207d0aldj7.jpg",
"NumWidth": 1,
"NumHeight": 1,
"BackIsHidden": true,
"UniqueBack": false,
"Type": 0
};
card.GUID = Math.random().toString(16).slice(-6);
return card;
}
function makeTTSDeck(cards){
const deck = {...TTSDeck};
deck.DeckIDs = cards.map(c => c.CardID);
deck.CustomDeck = cards.reduce((a,b) => ({...a, ...b.CustomDeck}), {});
deck.ContainedObjects = cards;
deck.GUID = Math.random().toString(16).slice(-6);
return deck;
}
function makeTTSSave(...ObjectStates){
return {...TTS, ObjectStates};
}
const TTS = {
"SaveName": "",
"GameMode": "",
"Date": "",
"Gravity": 0.5,
"PlayArea": 0.5,
"GameType": "",
"GameComplexity": "",
"Tags": [],
"Table": "",
"Sky": "",
"Note": "",
"Rules": "",
"TabStates": {},
"LuaScript": "",
"LuaScriptState": "",
"XmlUI": "",
"VersionNumber": "",
}
const TTSDeck = {
"Name": "Deck",
"Transform": {
"posX": 0,
"posY": 0,
"posZ": 0,
"rotX": 0,
"rotY": 180,
"rotZ": 180.0,
"scaleX": 1.0,
"scaleY": 1.0,
"scaleZ": 1.0
},
"Nickname": "",
"Description": "",
"GMNotes": "",
"ColorDiffuse": {
"r": 0.713235259,
"g": 0.713235259,
"b": 0.713235259
},
"Locked": false,
"Grid": true,
"Snap": true,
"IgnoreFoW": false,
"MeasureMovement": false,
"DragSelectable": true,
"Autoraise": true,
"Sticky": true,
"Tooltip": true,
"GridProjection": false,
"HideWhenFaceDown": true,
"Hands": false,
"SidewaysCard": false,
"LuaScript": "",
"LuaScriptState": "",
"XmlUI": "",
}
const TTSCard = {
"Name": "Card",
"Transform": {
"posX": 0,
"posY": 0,
"posZ": 0,
"rotX": 0,
"rotY": 180,
"rotZ": 180.0,
"scaleX": 1.0,
"scaleY": 1.0,
"scaleZ": 1.0
},
"Nickname": "",
"Description": "",
"GMNotes": "",
"ColorDiffuse": {
"r": 0.713235259,
"g": 0.713235259,
"b": 0.713235259
},
"Locked": false,
"Grid": true,
"Snap": true,
"IgnoreFoW": false,
"MeasureMovement": false,
"DragSelectable": true,
"Autoraise": true,
"Sticky": true,
"Tooltip": true,
"GridProjection": false,
"HideWhenFaceDown": true,
"Hands": true,
"CardID": 0,
"SidewaysCard": false,
"LuaScript": "",
"LuaScriptState": "",
"XmlUI": ""
};