Scryfall TTS deck save

Generate Tabletop Simulator deck object json

当前为 2020-06-29 提交的版本,查看 最新版本

// ==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": ""
};