Appearance
Card & deck files
cards.json (pointed to by cards.dataUrl) is not written by hand — you generate it from whatever source already holds your card data (a spreadsheet export, a CMS, an API...). decks.json (pointed to by defaultRessources.decksUrl) is the opposite: you build each deck yourself in the app's deck builder, the same way a player would.
Card file generation script
cards.json is a dictionary keyed by card id, not an array. Each entry needs at least the shape below — face.front holds what's shown on the card, and the flat name/type/cost fields are used by search and deck-building filters.
json
{
"cardId": {
"id": "cardId",
"face": {
"front": {
"name": { "name": "cardName" },
"type": "cardType",
"cost": 3,
"image": "https://example.com/cards/cardId.webp"
}
},
"name": "cardName",
"type": "cardType",
"cost": 3
}
}Both scripts below do the same thing — loop over your raw card data and write cards.json — they only differ in where the raw data comes from. The only part you need to edit is mapCard(): fill in whatever fields your own source actually has.
Optional: let a card create tokens on right-click
Add a tokens array of card ids to any card entry (referencing other entries in the same cards.json) to let players right-click that card and create one of those as a token — the way some Magic cards create their own token creatures. The referenced card doesn't need to be a token itself; whatever gets created through this field is simply treated as one.
From a URL
js
// generate-cards-from-url.js
import fs from 'fs'
const SOURCE_URL = 'https://example.com/my-raw-cards.json'
// The only function you need to edit — map your raw card
// data to the shape gamefile.json expects.
function mapCard(raw) {
return {
id: raw.id,
face: {
front: {
name: { name: raw.name },
type: raw.type,
cost: raw.cost,
image: raw.imageUrl
}
},
name: raw.name,
type: raw.type,
cost: raw.cost
}
}
async function main() {
const res = await fetch(SOURCE_URL)
const rawCards = await res.json()
const cards = {}
for (const raw of rawCards) {
const card = mapCard(raw)
cards[card.id] = card
}
fs.writeFileSync('cards.json', JSON.stringify(cards, null, 2))
console.log(`Wrote ${Object.keys(cards).length} cards to cards.json`)
}
main()From a local JSON file
js
// generate-cards-from-local-file.js
import fs from 'fs'
const SOURCE_FILE = './raw-cards.json'
// The only function you need to edit — map your raw card
// data to the shape gamefile.json expects.
function mapCard(raw) {
return {
id: raw.id,
face: {
front: {
name: { name: raw.name },
type: raw.type,
cost: raw.cost,
image: raw.imageUrl
}
},
name: raw.name,
type: raw.type,
cost: raw.cost
}
}
function main() {
const rawCards = JSON.parse(fs.readFileSync(SOURCE_FILE, 'utf-8'))
const cards = {}
for (const raw of rawCards) {
const card = mapCard(raw)
cards[card.id] = card
}
fs.writeFileSync('cards.json', JSON.stringify(cards, null, 2))
console.log(`Wrote ${Object.keys(cards).length} cards to cards.json`)
}
main()Run either with node generate-cards-from-url.js, then point cards.dataUrl at wherever you host the resulting cards.json.
Prebuilt decks (decksUrl)
defaultRessources.decksUrl is optional — it points to decks.json, an array of starter decks players can pick up and play immediately, no deck-building required.
Unlike cards.json, this one really is built by hand — the same way you'd build any deck: use the in-app deck builder, then Export → Starter Deck, and paste the resulting decklist into the array. The array of pasted decklists is decks.json — one entry per starter deck you want to offer.
No decksUrl? No problem
Both defaultRessources.backgrounds and decksUrl are optional. Skip decksUrl entirely if you don't want to offer prebuilt decks yet.