Start migration to Express/Vue/Parcel
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
||||
.tern-port
|
||||
/node_modules/
|
||||
/decks
|
||||
/dist/
|
||||
/.cache/
|
||||
|
@ -1,4 +0,0 @@
|
||||
body {
|
||||
background-color: #eee;
|
||||
color: #444;
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
#cardEditor {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: gray;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
* {
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: inline-block;
|
||||
border-radius: 5px;
|
||||
width: 142px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
#card-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#hand {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
height: 20%;
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.card-pile {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
color: #eee;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.shake {
|
||||
animation: shake 0.8s;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
10%, 90% {transform: translate(-1px, 0);}
|
||||
20%, 80% {transform: translate(2px, 0);}
|
||||
30%, 50%, 70% {transform: translate(-4px, 0);}
|
||||
40%, 60% {transform: translate(4px, 0);}
|
||||
}
|
||||
|
||||
.deck {
|
||||
background-color: blue;
|
||||
}
|
||||
|
||||
.discard {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
#shade {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: grey;
|
||||
opacity: 0.5;
|
||||
filter: alpha(opacity=50);
|
||||
}
|
||||
|
||||
#modal-content {
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
position: fixed;
|
||||
z-index: 101;
|
||||
top: 5%;
|
||||
left: 5%;
|
||||
width: 90%;
|
||||
background-color: white;
|
||||
overflow-y: auto;
|
||||
max-height: 90%;
|
||||
}
|
||||
|
||||
#modal-content .card {
|
||||
margin: 2px;
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="/js/editor.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/css/editor.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/common.css">
|
||||
<title>Editor</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<button type="button" id="saveButton">Save Deck</button>
|
||||
<button type="button" id="jsonInputDownload">Download Input JSON</button>
|
||||
<button type="button" id="outputDownload">Download Tabletop Output JSON</button>
|
||||
</div>
|
||||
<div>
|
||||
<label> Upload JSON: WARNING: WILL CLEAR DECK
|
||||
<input id="jsonUpload" type="file">
|
||||
</label>
|
||||
</div>
|
||||
<form id="deckForm">
|
||||
<div> <label> Deck Name: <input type="text" id="deckName"></label> </div>
|
||||
<div>
|
||||
<label> Deck Type: <select id="deckType">
|
||||
<option value="hero">hero</option>
|
||||
<option value="villain">villain</option>
|
||||
<option value="environment">environment</option>
|
||||
</select> </label>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form id="cardForm"></form>
|
||||
|
||||
<div id="deck"></div>
|
||||
</body>
|
||||
</html>
|
@ -1,16 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="/js/interact.js"></script>
|
||||
<script src="/js/playfield.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/css/playfield.css">
|
||||
<link rel="stylesheet" type="text/css" href="/css/common.css">
|
||||
<title>Playfield</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="card-container">
|
||||
<div class="card-pile deck" data-pile="deck">DECK</div>
|
||||
<div class="card-pile discard" data-pile="discard">DISCARD</div>
|
||||
<div id="hand"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
253
js/editor.js
@ -1,253 +0,0 @@
|
||||
//jshint browser:true
|
||||
//jshint esversion:6
|
||||
//jshint latedef:nofunc
|
||||
|
||||
let deckJSON, template;
|
||||
let selected;
|
||||
let deckName = window.location.pathname.split('/')[2];
|
||||
|
||||
document.title = "Editor|" + deckName;
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
// load deck input json
|
||||
fetch("deck.input.json")
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
deckJSON = json;
|
||||
makeSVGs(deckJSON);
|
||||
})
|
||||
.catch(error => console.error(error));
|
||||
|
||||
// deck JSON uploader
|
||||
document.querySelector('#jsonUpload').addEventListener('change', event => {
|
||||
let files = event.target.files;
|
||||
let reader = new FileReader();
|
||||
reader.onload = event => {
|
||||
deckJSON = JSON.parse(event.target.result);
|
||||
makeSVGs(deckJSON);
|
||||
};
|
||||
reader.readAsText(files[0]);
|
||||
});
|
||||
|
||||
// Upload on save button
|
||||
document.querySelector('#saveButton').addEventListener('click', upload);
|
||||
|
||||
// download input JSON
|
||||
document.querySelector('#jsonInputDownload').addEventListener(
|
||||
'click',
|
||||
() => downloadFile('data:application/json;charset=utf-8,' +
|
||||
encodeURIComponent(JSON.stringify(deckJSON)),
|
||||
deckName + '.input.json'));
|
||||
|
||||
// download input JSON
|
||||
document.querySelector('#outputDownload').addEventListener(
|
||||
'click',
|
||||
() => downloadFile('deck.tts.json',
|
||||
deckName + '.json'));
|
||||
|
||||
|
||||
// handle changes to deck editor
|
||||
document.querySelector('#deckForm').addEventListener('input', event => {
|
||||
let prop = event.target.id.substring(4).toLowerCase();
|
||||
deckJSON[prop] = event.target.value;
|
||||
if (prop === 'type') {
|
||||
makeSVGs(deckJSON);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// handle changes to card editor
|
||||
document.querySelector('#cardForm').addEventListener('input', event => {
|
||||
let prop = event.target.id.substring(5);
|
||||
if (prop !== "count") {
|
||||
wrapSVGText(selected.svg.querySelector('#' + prop),
|
||||
String(event.target.value));
|
||||
}
|
||||
if (event.target.value) {
|
||||
selected.json[prop] = event.target.value;
|
||||
}
|
||||
else {
|
||||
delete selected.json[prop];
|
||||
}
|
||||
});
|
||||
|
||||
// chrome doesn't seem to send input event on file select
|
||||
document.querySelector('#cardForm').addEventListener('change', event => {
|
||||
let prop = event.target.id.substring(5);
|
||||
if (prop === "image") {
|
||||
let files = event.target.files;
|
||||
let reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
selected.svg.querySelector('#' + prop)
|
||||
.setAttributeNS("http://www.w3.org/1999/xlink", "href", e.target.result);
|
||||
selected.json[prop] = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload',
|
||||
e => e.returnValue = "Unsaved changes blah blah");
|
||||
});
|
||||
|
||||
function downloadFile(file, name) {
|
||||
let dl = document.createElement('a');
|
||||
dl.setAttribute('href', file);
|
||||
dl.setAttribute('download', name);
|
||||
document.body.appendChild(dl);
|
||||
dl.click();
|
||||
document.body.removeChild(dl);
|
||||
}
|
||||
|
||||
function getSVGTemplate(name, callback) {
|
||||
return fetch("/template/" + name + ".svg")
|
||||
.then(response => response.text())
|
||||
.then(str => (new window.DOMParser()).parseFromString(str, "text/xml").activeElement);
|
||||
}
|
||||
|
||||
async function makeSVGs(deckJSON) {
|
||||
document.querySelector('#deckName').value = deckJSON.name || "";
|
||||
document.querySelector('#deckType').value = deckJSON.type || "";
|
||||
|
||||
let deck = document.querySelector('#deck');
|
||||
deck.innerHTML = "";
|
||||
|
||||
let template = await fetch(`/template/${deckJSON.type}/input.json`)
|
||||
.then(data => data.json());
|
||||
|
||||
let cardCount = Object.entries(template.cardTypes)
|
||||
.map(ct => deckJSON[ct[0]].length * (ct[1].back ? 2 : 1))
|
||||
.reduce((sum, current) => sum + current, 0);
|
||||
|
||||
// note: needs to be a for loop because it needs to be synchronous
|
||||
// and also have await
|
||||
// Although I suppose I could prefetch the SVGs and then do the rest...
|
||||
for (let cardType of Object.entries(template.cardTypes)) {
|
||||
let backSVG;
|
||||
if (cardType[1].back) {
|
||||
let backTemplate = cardType[1].back.template || (cardType[0] + "-back");
|
||||
backSVG = await getSVGTemplate(deckJSON.type + "/" + backTemplate);
|
||||
}
|
||||
let templateSVG = await getSVGTemplate(deckJSON.type + "/" + cardType[0]);
|
||||
console.log(templateSVG);
|
||||
|
||||
// build card SVGs
|
||||
deckJSON[cardType[0]].forEach(card => {
|
||||
makeCardSVG(deck, cardType[1], templateSVG, card);
|
||||
|
||||
// if there is a back, build it too
|
||||
if (cardType[1].back) {
|
||||
makeCardSVG(deck, cardType[1].back, backSVG, card, back=true);
|
||||
}
|
||||
});
|
||||
|
||||
// set div width/height based on number of cards
|
||||
deck.style.width = Math.ceil(Math.sqrt(cardCount)) *
|
||||
parseInt(templateSVG.getAttribute("width")) + "pt";
|
||||
deck.style.height = Math.ceil(Math.sqrt(cardCount)) *
|
||||
parseInt(templateSVG.getAttribute("height")) + "pt";
|
||||
};
|
||||
}
|
||||
|
||||
function setForm(cardTemplate, card) {
|
||||
let form = document.querySelector('#cardForm');
|
||||
form.innerHTML = "";
|
||||
|
||||
Object.entries(cardTemplate.inputs).forEach(prop => {
|
||||
let div = form.appendChild(document.createElement('div'));
|
||||
let label = div.appendChild(document.createElement('label'));
|
||||
label.textContent = prop[0];
|
||||
|
||||
let input = label.appendChild(
|
||||
document.createElement(prop[1] === 'textarea' ? 'textarea' : 'input'));
|
||||
input.id = "card-" + prop[0];
|
||||
|
||||
if (prop[1] === "image") {
|
||||
input.type = "file";
|
||||
}
|
||||
else {
|
||||
input.type = prop[1];
|
||||
input.value = card[prop[0]] || "";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function makeCardSVG(deck, cardInputTemplate, templateSVG, card, back=false) {
|
||||
let propSource = (back && card.back) ? card.back : card;
|
||||
let cardSVG = deck.appendChild(templateSVG.cloneNode(true));
|
||||
cardSVG.addEventListener('click', () => {
|
||||
selected = {svg: cardSVG, json: card};
|
||||
setForm(cardInputTemplate, card);
|
||||
}, true);
|
||||
Object.keys(cardInputTemplate.inputs).forEach(prop => {
|
||||
let inputProp = propSource[prop] || card[prop] || "";
|
||||
wrapSVGText(cardSVG.querySelector('#' + prop), String(inputProp));
|
||||
});
|
||||
Object.entries(cardInputTemplate.hide || []).forEach(hidable => {
|
||||
cardSVG.querySelector('#' + hidable[0])
|
||||
.setAttribute('display', hidable[1] in propSource ? '' : 'none');
|
||||
});
|
||||
}
|
||||
|
||||
function upload() {
|
||||
let deck = document.querySelector('#deck');
|
||||
|
||||
// POST the generated SVGs to the server
|
||||
let data = (new XMLSerializer()).serializeToString(deck);
|
||||
fetch('upload', {
|
||||
method: 'post',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({body: data, json: deckJSON})
|
||||
});
|
||||
}
|
||||
|
||||
function wrapSVGText(e, string) {
|
||||
// TODO: bold or italic text
|
||||
e.innerHTML = ""; // clear element
|
||||
let lines = string.split("\n");
|
||||
if (e.getAttribute('default-font-size'))
|
||||
e.setAttribute('font-size', e.getAttribute('default-font-size'));
|
||||
e.setAttribute('default-font-size', e.getAttribute('font-size'));
|
||||
while (lines.length > 0) {
|
||||
let words = lines.shift().split(" ");
|
||||
let tspan = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
|
||||
tspan.setAttribute('x', e.getAttribute('x'));
|
||||
if (e.innerHTML !== "") tspan.setAttribute('dy', e.getAttribute('font-size'));
|
||||
e.appendChild(tspan);
|
||||
let line = [];
|
||||
while(words.length > 0) {
|
||||
let word = words.shift();
|
||||
if (word === "") word = " ";
|
||||
line.push(word);
|
||||
tspan.innerHTML = line.join(" ");
|
||||
// horizontal overflow
|
||||
// TODO: actually use units (also applies to vertical)
|
||||
if (parseFloat(e.getAttribute("width")) &&
|
||||
tspan.getComputedTextLength() > parseFloat(e.getAttribute("width"))) {
|
||||
// if we have height, we can line wrap
|
||||
if (parseFloat(e.getAttribute("height")) &&
|
||||
e.children.length * parseFloat(e.getAttribute('font-size')) <
|
||||
parseFloat(e.getAttribute('height'))) {
|
||||
words.unshift(line.pop());
|
||||
tspan.innerHTML = line.join(" ");
|
||||
line = [];
|
||||
|
||||
tspan = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
|
||||
tspan.setAttribute('x', e.getAttribute('x'));
|
||||
tspan.setAttribute('dy', e.getAttribute('font-size'));
|
||||
e.appendChild(tspan);
|
||||
}
|
||||
// vertical overflow or horizontal overflow with no height variable
|
||||
// TODO: better with recursion instead?
|
||||
else {
|
||||
e.innerHTML = ""; // remove all tspans
|
||||
// TODO: maybe binary search font size later if I really care
|
||||
e.setAttribute('font-size', parseFloat(e.getAttribute('font-size')) * 0.9);
|
||||
words = [];
|
||||
lines = string.split('\n');
|
||||
console.log("resetting, size= " + e.getAttribute('font-size'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5978
js/interact.js
319
js/playfield.js
@ -1,319 +0,0 @@
|
||||
//jshint browser:true
|
||||
//jshint esversion:6
|
||||
//jshint latedef:nofunc
|
||||
/* globals interact:true */
|
||||
|
||||
let deckNum, deckJSON, cardCount, deckWidth, deckHeight,
|
||||
piles = {'deck': [], discard: []};
|
||||
|
||||
document.title = "Playfield|" + window.location.pathname.split('/')[2];
|
||||
|
||||
interact.dynamicDrop(true);
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
fetch("deck.json")
|
||||
.then(data => data.json())
|
||||
.then(json => {
|
||||
deckJSON = json;
|
||||
deckNum = Object.keys(deckJSON.CustomDeck)[0];
|
||||
piles.deck = deckJSON.DeckIDs.map(c => c - deckNum * 100);
|
||||
cardCount = piles.deck.length;
|
||||
shuffle(piles.deck);
|
||||
deckWidth = deckJSON.CustomDeck[deckNum].NumWidth;
|
||||
deckHeight = deckJSON.CustomDeck[deckNum].NumHeight;
|
||||
});
|
||||
|
||||
window.addEventListener("contextmenu", event => event.preventDefault());
|
||||
});
|
||||
|
||||
let cardInteract = interact('.card', {ignoreFrom: '.in-list'})
|
||||
.draggable({
|
||||
restrict: {
|
||||
restriction: "parent",
|
||||
endOnly: true,
|
||||
elementRect: { top: 0, left: 0, bottom: 1, right: 1 }
|
||||
},
|
||||
|
||||
snap: {
|
||||
targets: [() => {
|
||||
// TODO: maybe change to dropzone?
|
||||
let pos = document.body.getBoundingClientRect(),
|
||||
hand = document.getElementById('hand'),
|
||||
style = window.getComputedStyle(hand),
|
||||
handHeight = parseInt(style.getPropertyValue("height"));
|
||||
return {y: pos.bottom,
|
||||
range: handHeight/2};
|
||||
}],
|
||||
relativePoints: [{x: 0.5 , y: 1}]
|
||||
},
|
||||
|
||||
onmove: event => {
|
||||
dragMoveListener(event);
|
||||
// raise to top
|
||||
event.target.parentElement.removeChild(event.target);
|
||||
document.querySelector("#card-container").appendChild(event.target);
|
||||
}
|
||||
})
|
||||
.on('doubletap', event => {
|
||||
let scale = parseFloat(event.target.getAttribute('data-scale')) === 2 ? 1 : 2;
|
||||
event.target.setAttribute('data-scale', scale);
|
||||
dragMoveListener({target: event.target, dx: 0, dy: 0});
|
||||
})
|
||||
.on('tap', event => {
|
||||
let card = event.target;
|
||||
let cardNum = parseInt(card.getAttribute('data-num'));
|
||||
if (card.dataset.flipped === "true") {
|
||||
card.dataset.flipped = false;
|
||||
let style = window.getComputedStyle(card);
|
||||
card.style.backgroundPositionX =
|
||||
-(cardNum % deckWidth) * parseInt(style.getPropertyValue("width")) + "px";
|
||||
card.style.backgroundPositionY =
|
||||
-Math.floor(cardNum/deckWidth) * parseInt(style.getPropertyValue("height")) + "px";
|
||||
return;
|
||||
}
|
||||
|
||||
let cardData = deckJSON.ContainedObjects.find(c => c.CardID === (cardNum + deckNum * 100));
|
||||
let backNum = cardData.States && cardData.States["2"].CardID;
|
||||
if (backNum) {
|
||||
card.dataset.flipped = true;
|
||||
let style = window.getComputedStyle(card);
|
||||
card.style.backgroundPositionX =
|
||||
-(backNum % deckWidth) * parseInt(style.getPropertyValue("width")) + "px";
|
||||
card.style.backgroundPositionY =
|
||||
-Math.floor(backNum/deckWidth) * parseInt(style.getPropertyValue("height")) + "px";
|
||||
}
|
||||
});
|
||||
|
||||
interact('.card.in-list')
|
||||
.draggable({
|
||||
restrict: {
|
||||
restriction: "parent",
|
||||
endOnly: false,
|
||||
elementRect: { top: 0, left: 0, bottom: 1, right: 1 }
|
||||
},
|
||||
autoScroll: false,
|
||||
|
||||
onmove: dragMoveListener,
|
||||
onend: event => {
|
||||
// reset transform and data attributes
|
||||
event.target.style.webkitTransform = event.target.style.transform = '';
|
||||
|
||||
event.target.removeAttribute('data-x');
|
||||
event.target.removeAttribute('data-y');
|
||||
}
|
||||
})
|
||||
.dropzone({
|
||||
accept: '.card',
|
||||
ondropmove: event => {
|
||||
let target = event.target;
|
||||
// TODO: there must be a better way to do this
|
||||
let relCursorPos = (event.dragEvent.pageX -
|
||||
target.getBoundingClientRect().left) /
|
||||
parseInt(window.getComputedStyle(target).width);
|
||||
|
||||
let oldOffsetLeft = event.relatedTarget.offsetLeft;
|
||||
let oldOffsetTop = event.relatedTarget.offsetTop;
|
||||
|
||||
// move the object in the DOM
|
||||
if (relCursorPos < 0.5 || relCursorPos === Infinity) {
|
||||
target.parentElement.insertBefore(event.relatedTarget, target);
|
||||
}
|
||||
else {
|
||||
target.parentElement.insertBefore(event.relatedTarget, target.nextSibling);
|
||||
}
|
||||
|
||||
// update position
|
||||
dragMoveListener({target: event.relatedTarget,
|
||||
dx: oldOffsetLeft - event.relatedTarget.offsetLeft,
|
||||
dy: oldOffsetTop - event.relatedTarget.offsetTop});
|
||||
|
||||
// rebuild source pile
|
||||
piles[target.getAttribute('data-pile')] =
|
||||
Array.from(target.parentElement.children).map(
|
||||
c => c.getAttribute('data-num'));
|
||||
}
|
||||
})
|
||||
.on('tap', event => {
|
||||
let target = event.target;
|
||||
console.log(`Drawing ${target.getAttribute('data-num')} from ${target.getAttribute('data-pile')}`);
|
||||
|
||||
let listDiv = target.parentElement;
|
||||
// re-parent
|
||||
document.querySelector("#card-container").appendChild(target);
|
||||
|
||||
// rebuild source pile
|
||||
piles[target.getAttribute('data-pile')] =
|
||||
Array.from(listDiv.children).map(c => c.getAttribute('data-num'));
|
||||
|
||||
// remove list class
|
||||
target.classList.remove('in-list');
|
||||
|
||||
// fix position
|
||||
target.style.position = "fixed";
|
||||
|
||||
});
|
||||
|
||||
interact('.card-pile')
|
||||
.dropzone({
|
||||
accept: '.card:not(.in-pile)',
|
||||
ondrop: event => {
|
||||
// TODO: fix possible duped zeros
|
||||
let pileName = event.target.getAttribute('data-pile');
|
||||
let cardNum = event.relatedTarget.getAttribute('data-num');
|
||||
if (event.dragEvent.shiftKey) {
|
||||
console.log(`Adding ${cardNum} bottom of to ${pileName}`);
|
||||
piles[pileName].unshift(cardNum);
|
||||
}
|
||||
else {
|
||||
console.log(`Adding ${cardNum} to ${pileName}`);
|
||||
piles[pileName].push(cardNum);
|
||||
}
|
||||
|
||||
// remove from DOM
|
||||
event.relatedTarget.parentElement.removeChild(event.relatedTarget);
|
||||
|
||||
// update deck text
|
||||
event.target.innerHTML = `${pileName.toUpperCase()}<br>${piles[pileName].length}/${cardCount}`;
|
||||
}
|
||||
})
|
||||
.draggable({manualStart: true})
|
||||
.on('move', event => {
|
||||
let interaction = event.interaction;
|
||||
let pileName = event.target.getAttribute('data-pile');
|
||||
let pile = piles[pileName];
|
||||
|
||||
// if the pointer was moved while being held down
|
||||
// and an interaction hasn't started yet
|
||||
// and there are cards in the pile
|
||||
if (interaction.pointerIsDown &&
|
||||
!interaction.interacting() &&
|
||||
pile.length > 0) {
|
||||
// draw a new card
|
||||
let newCard = makeCard(pile.pop());
|
||||
newCard.style.position = "fixed";
|
||||
newCard.style.top = event.pageY;
|
||||
newCard.style.left = event.pageX;
|
||||
// insert the card to the page
|
||||
document.querySelector("#card-container").appendChild(newCard);
|
||||
|
||||
// update deck text
|
||||
event.target.innerHTML = `${pileName.toUpperCase()}<br>${pile.length}/${cardCount}`;
|
||||
|
||||
// start a drag interaction targeting the clone
|
||||
interaction.start({name: 'drag'}, cardInteract, newCard);
|
||||
}
|
||||
})
|
||||
.on('hold', event => {
|
||||
let pile = piles[event.target.getAttribute('data-pile')];
|
||||
let searchBox = document.createElement('input');
|
||||
let container = document.createElement('div');
|
||||
let cardList = document.createElement('div');
|
||||
searchBox.setAttribute('type', 'search');
|
||||
searchBox.setAttribute('placeholder', 'Filter');
|
||||
searchBox.addEventListener('input', event => {
|
||||
let input = event.target.value;
|
||||
Array.from(cardList.children).forEach(card => {
|
||||
let cardNum = parseInt(card.getAttribute('data-num'));
|
||||
let cardData = deckJSON.ContainedObjects.find(c => c.CardID === (cardNum + deckNum * 100));
|
||||
card.style.display =
|
||||
(cardData.Nickname.toLowerCase().includes(input.toLowerCase()) ||
|
||||
cardData.Description.toLowerCase().includes(input.toLowerCase())) ?
|
||||
"": "none";
|
||||
});
|
||||
});
|
||||
container.appendChild(searchBox);
|
||||
|
||||
pile.forEach(cardNum => {
|
||||
let newCard = makeCard(cardNum);
|
||||
newCard.classList.add('in-list');
|
||||
newCard.setAttribute('data-pile', event.target.getAttribute('data-pile'));
|
||||
cardList.appendChild(newCard);
|
||||
});
|
||||
container.appendChild(cardList);
|
||||
showModal(container);
|
||||
})
|
||||
.on('tap', event => {
|
||||
shuffle(piles[event.target.getAttribute('data-pile')]);
|
||||
event.target.classList.add("shake");
|
||||
// reset animation so it can be played again
|
||||
event.target.onanimationend = e => {
|
||||
event.target.classList.remove("shake");
|
||||
};
|
||||
});
|
||||
|
||||
function makeCard(cardNum) {
|
||||
// draw a new card
|
||||
let card = document.createElement('div');
|
||||
card.className = "card";
|
||||
|
||||
// temporary add so getComputedStyle works on Chrome
|
||||
document.body.appendChild(card);
|
||||
let style = window.getComputedStyle(card);
|
||||
card.setAttribute('data-num', cardNum);
|
||||
card.style.backgroundPositionX =
|
||||
-(cardNum % deckWidth) * parseInt(style.getPropertyValue("width")) + "px";
|
||||
card.style.backgroundPositionY =
|
||||
-Math.floor(cardNum/deckWidth) * parseInt(style.getPropertyValue("height")) + "px";
|
||||
let faceURI = encodeURI(deckJSON.CustomDeck[deckNum].FaceURL);
|
||||
card.style.backgroundImage = `url("${faceURI}")`;
|
||||
card.style.backgroundSize = `${deckWidth * 100}% ${deckHeight * 100}%`;
|
||||
document.body.removeChild(card);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
function showModal(content) {
|
||||
let shade = document.createElement('div');
|
||||
shade.id = "shade";
|
||||
shade.className = "modal";
|
||||
shade.addEventListener('click', hideModal);
|
||||
document.body.appendChild(shade);
|
||||
|
||||
let modal = document.createElement('div');
|
||||
modal.id = "modal-content";
|
||||
modal.className = "modal";
|
||||
modal.appendChild(content);
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
function hideModal() {
|
||||
document.querySelectorAll('.modal').forEach(
|
||||
e => e.parentElement.removeChild(e));
|
||||
}
|
||||
|
||||
// Fisher-Yates shuffle from https://bost.ocks.org/mike/shuffle/
|
||||
function shuffle(array) {
|
||||
let m = array.length, t, i;
|
||||
|
||||
// While there remain elements to shuffle…
|
||||
while (m) {
|
||||
// Pick a remaining element…
|
||||
i = Math.floor(Math.random() * m--);
|
||||
|
||||
// And swap it with the current element.
|
||||
t = array[m];
|
||||
array[m] = array[i];
|
||||
array[i] = t;
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
function dragMoveListener (event) {
|
||||
let target = event.target;
|
||||
|
||||
// keep the dragged position in the data-x/data-y attribute
|
||||
let x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
|
||||
let y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;
|
||||
let scale = (parseFloat(target.getAttribute('data-scale')) || 1);
|
||||
|
||||
// translate and scale the element
|
||||
target.style.webkitTransform =
|
||||
target.style.transform =
|
||||
'translate(' + x + 'px, ' + y + 'px) scale(' + scale + ')';
|
||||
|
||||
// update the posiion attributes
|
||||
target.setAttribute('data-x', x);
|
||||
target.setAttribute('data-y', y);
|
||||
}
|
||||
|
6739
package-lock.json
generated
21
package.json
@ -5,11 +5,28 @@
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js"
|
||||
"start": "node src/server.js",
|
||||
"start-parcel": "parcel src/index.html",
|
||||
"build": "parcel build --public-url ./ src/index.html"
|
||||
},
|
||||
"author": "Adam Goldsmith",
|
||||
"license": " CC-BY-SA-4.0",
|
||||
"dependencies": {
|
||||
"phantom": "^4.0.12"
|
||||
"express": "^4.16.4",
|
||||
"interactjs": "^1.3.4",
|
||||
"parcel-bundler": "^1.11.0",
|
||||
"phantom": "^4.0.12",
|
||||
"v-runtime-template": "^1.5.2",
|
||||
"vue": "^2.5.21",
|
||||
"vue-hot-reload-api": "^2.3.1",
|
||||
"vue-router": "^3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/component-compiler-utils": "^2.3.1",
|
||||
"parcel": "^1.11.0",
|
||||
"vue-template-compiler": "^2.5.21"
|
||||
},
|
||||
"alias": {
|
||||
"vue": "./node_modules/vue/dist/vue.common.js"
|
||||
}
|
||||
}
|
||||
|
263
server.js
@ -1,263 +0,0 @@
|
||||
// jshint node:true
|
||||
// jshint esversion:6
|
||||
"use strict";
|
||||
|
||||
const http = require('http'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
url = require('url'),
|
||||
phantom = require('phantom'),
|
||||
port = 8080;
|
||||
|
||||
const decks = ["the_Unholy_Priest_update_2", "NZoths_Invasion_1.2", "Puffer_Fish_input_1.3"];
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const uri = url.parse(req.url);
|
||||
|
||||
let pathParts = uri.pathname.split("/");
|
||||
switch (pathParts[1]) {
|
||||
case '':
|
||||
case 'index.html':
|
||||
sendIndex(res);
|
||||
break;
|
||||
case 'css':
|
||||
switch (pathParts[2]) {
|
||||
case 'playfield.css':
|
||||
case 'editor.css':
|
||||
case 'common.css':
|
||||
sendFile(res, path.join('css', pathParts[2]), 'text/css');
|
||||
break;
|
||||
default:
|
||||
send404(res, uri);
|
||||
}
|
||||
break;
|
||||
case 'js':
|
||||
switch (pathParts[2]) {
|
||||
case 'playfield.js':
|
||||
case 'editor.js':
|
||||
case 'interact.js':
|
||||
sendFile(res, path.join('js', pathParts[2]), 'application/javascript');
|
||||
break;
|
||||
default:
|
||||
send404(res, uri);
|
||||
}
|
||||
break;
|
||||
case 'template':
|
||||
pathParts.splice(0, 2); // remove first two elements
|
||||
let item = pathParts.join("/");
|
||||
console.log("template/" + item);
|
||||
switch (item) {
|
||||
case "card.json":
|
||||
case "deck.json":
|
||||
case "environment/input.json":
|
||||
case "hero/input.json":
|
||||
case "villain/input.json":
|
||||
sendFile(res, path.join("template", item), 'application/json');
|
||||
break;
|
||||
case "environment/deck.svg":
|
||||
case "hero/deck.svg":
|
||||
case "hero/character-back.svg":
|
||||
case "hero/character.svg":
|
||||
case "villain/deck.svg":
|
||||
case "villain/character.svg":
|
||||
case "villain/instructions.svg":
|
||||
sendFile(res, path.join("template", item), 'image/svg+xml');
|
||||
break;
|
||||
default:
|
||||
send404(res, uri);
|
||||
}
|
||||
break;
|
||||
case 'deck':
|
||||
if (pathParts.length < 3 || pathParts[2] === '') {
|
||||
sendIndex(res);
|
||||
break;
|
||||
}
|
||||
let deckName = decodeURI(pathParts[2]);
|
||||
switch (pathParts[3] || '') {
|
||||
case '':
|
||||
sendDeckIndex(res, deckName);
|
||||
break;
|
||||
case 'play':
|
||||
sendFile(res, 'html/playfield.html');
|
||||
break;
|
||||
case 'editor':
|
||||
sendFile(res, 'html/editor.html');
|
||||
break;
|
||||
case 'deck.png':
|
||||
sendFile(res, `decks/${deckName}.png`, 'image/png');
|
||||
break;
|
||||
case 'deck.json':
|
||||
sendFileJSON(res, deckName);
|
||||
break;
|
||||
case 'deck.tts.json':
|
||||
sendFile(res, `decks/${deckName}.json`, 'application/json');
|
||||
break;
|
||||
case 'deck.input.json':
|
||||
sendFile(res, `decks/${deckName}.input.json`, 'application/json');
|
||||
break;
|
||||
case 'upload':
|
||||
handleUpload(res, req);
|
||||
break;
|
||||
default:
|
||||
send404(res, uri);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
send404(res, uri);
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(process.env.PORT || port);
|
||||
console.log('listening on 8080');
|
||||
|
||||
function sendIndex(res) {
|
||||
const html = `
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="/css/common.css">
|
||||
<title>Index</title>
|
||||
</head>
|
||||
<body>
|
||||
<label>Create New Deck: <input type="text" onchange="window.location='/deck/' + event.target.value + '/editor'"></label>
|
||||
<ul>
|
||||
${(decks.map(d => `<li><a href="/deck/${d}">${d}</a></li>`).join(' '))}
|
||||
</ul>
|
||||
</body>
|
||||
</html>`;
|
||||
res.writeHead(200, {'Content-type': 'text/html; charset=utf-8'});
|
||||
res.end(html, 'utf-8');
|
||||
}
|
||||
|
||||
function sendDeckIndex(res, deckName) {
|
||||
const html = `
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="/css/common.css">
|
||||
<title>${deckName}</title>
|
||||
</head>
|
||||
<body>
|
||||
<ul>
|
||||
<li><a href="${deckName}/play">Play!</a></li>
|
||||
<li><a href="${deckName}/editor">Editor</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>`;
|
||||
res.writeHead(200, {'Content-type': 'text/html; charset=utf-8'});
|
||||
res.end(html, 'utf-8');
|
||||
}
|
||||
|
||||
function sendFileJSON(res, deckName) {
|
||||
fs.readFile(`decks/${deckName}.json`, (error, content) => {
|
||||
console.log(JSON.parse(content));
|
||||
res.writeHead(200, {'Content-type': 'application/json; charset=utf-8'});
|
||||
res.end(JSON.stringify(JSON.parse(content).ObjectStates[0]), 'utf-8');
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function handleUpload(res, req) {
|
||||
let body = '';
|
||||
|
||||
req.on('data', data => {
|
||||
body += data;
|
||||
// check for file > 100MB
|
||||
if (body.length > 1e8) {
|
||||
req.connection.destroy();
|
||||
console.log('upload too big');
|
||||
}
|
||||
});
|
||||
|
||||
req.on('end', () => {
|
||||
const json = JSON.parse(body);
|
||||
const deckJSON = json.json;
|
||||
const cardTemplate = fs.readFileSync('template/card.json');
|
||||
const template = JSON.parse(fs.readFileSync(`template/${deckJSON.type}/input.json`));
|
||||
const cardCount = Object.entries(template.cardTypes)
|
||||
.map(ct => deckJSON[ct[0]].length * (ct[1].back ? 2 : 1))
|
||||
.reduce((sum, current) => sum + current, 0);
|
||||
|
||||
let deckOut = JSON.parse(fs.readFileSync('template/deck.json'));
|
||||
deckOut.ObjectStates[0].Nickname = deckJSON.name;
|
||||
|
||||
Object.assign(deckOut.ObjectStates[0].CustomDeck['1'],
|
||||
{NumWidth: Math.ceil(Math.sqrt(cardCount)),
|
||||
NumHeight: Math.ceil(Math.sqrt(cardCount)),
|
||||
FaceURL: `http://${req.headers.host}/deck/${deckJSON.name}/deck.png`,
|
||||
BackURL: "http://cloud-3.steamusercontent.com/ugc/156906385556221451/CE2C3AFE1759790CB0B532FFD636D05A99EC91F4/"});
|
||||
|
||||
let index = 100;
|
||||
deckOut.ObjectStates[0].ContainedObjects =
|
||||
Object.entries(template.cardTypes).map(
|
||||
cardType => deckJSON[cardType[0]].map(cardIn => {
|
||||
let cardOut = JSON.parse(cardTemplate);
|
||||
Object.assign(cardOut, {Nickname: cardIn.name,
|
||||
Description: cardIn.keywords,
|
||||
CardID: index});
|
||||
|
||||
for (let ii=0; ii<(cardIn.count || 1); ii++) {
|
||||
deckOut.ObjectStates[0].DeckIDs.push(index);
|
||||
}
|
||||
index++;
|
||||
|
||||
if(cardType[1].back) {
|
||||
let cardBack = JSON.parse(cardTemplate);
|
||||
Object.assign(cardBack, {Nickname: cardIn.back.name,
|
||||
Description: cardIn.back.keywords,
|
||||
CardID: index});
|
||||
cardOut.States = {"2": cardBack};
|
||||
index++;
|
||||
}
|
||||
return cardOut;
|
||||
}))
|
||||
.reduce((sum, cur) => sum.concat(cur), []);
|
||||
|
||||
fs.writeFileSync(`decks/${deckJSON.name}.json`, JSON.stringify(deckOut));
|
||||
fs.writeFileSync(`decks/${deckJSON.name}.input.json`, JSON.stringify(deckJSON));
|
||||
|
||||
console.log("making page");
|
||||
phantom.create().then(
|
||||
ph => ph.createPage().then(
|
||||
page => {
|
||||
page.on('onLoadFinished', status => {
|
||||
if (status !== 'success') {
|
||||
console.log('Failed to load page');
|
||||
ph.exit(1);
|
||||
}
|
||||
else {
|
||||
page.render(`decks/${deckJSON.name}.png`);
|
||||
page.close().then(() => ph.exit());
|
||||
}
|
||||
});
|
||||
page.property('zoomFactor', 2); // pretty arbitrary
|
||||
page.property('content', '<body style="margin:0;">' + json.body + '</body>');
|
||||
}));
|
||||
decks.push(deckJSON.name);
|
||||
});
|
||||
}
|
||||
|
||||
function send404(res, uri) {
|
||||
res.writeHead(404, {'Content-type': "text/html; charset=utf-8"});
|
||||
const html = `
|
||||
<head>
|
||||
<title>404 Not Found</title>
|
||||
<link rel="stylesheet" href="/css/common.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Error 404: Path ${uri.pathname} not found</h1>
|
||||
You seem to have gone to the wrong place, would you like to go
|
||||
back to the <a href="/">main page</a>?
|
||||
</body>`;
|
||||
res.end(html, 'utf-8');
|
||||
}
|
||||
|
||||
function sendFile(res, filename, contentType='text/html; charset=utf-8') {
|
||||
fs.readFile(filename, (error, content) => {
|
||||
res.writeHead(200, {'Content-type': contentType});
|
||||
res.end(content, 'utf-8');
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
9
src/App.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App',
|
||||
};
|
||||
</script>
|
32
src/DeckIndex.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<body>
|
||||
<label>Create New Deck:
|
||||
<input type="text"
|
||||
@change="window.location='/deck/' + event.target.value + '/editor'">
|
||||
</label>
|
||||
<ul>
|
||||
<li v-for="deck in decks">
|
||||
{{ deck }}:
|
||||
<router-link :to="'/play/' + deck"> Play </router-link>
|
||||
<router-link :to="'/edit/' + deck"> Edit </router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</body>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DeckIndex',
|
||||
data() {
|
||||
return {
|
||||
decks: null,
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
fetch('/decks.json')
|
||||
.then(r => r.json())
|
||||
.then(d => this.decks = d);
|
||||
},
|
||||
}
|
||||
</script>
|
187
src/Editor.vue
Normal file
@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div>
|
||||
<div id="controls">
|
||||
<div>
|
||||
<button type="button" @click="upload"> Save Deck </button>
|
||||
<button type="button" @click="jsonInputDownload">
|
||||
Download Input JSON
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
@click="downloadFile('deck.tts.json', deckName + '.json')">
|
||||
Download Tabletop Output JSON
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label> Upload JSON: WARNING: WILL CLEAR DECK
|
||||
<input @change="jsonUpload" type="file">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<form>
|
||||
<div>
|
||||
<label> Deck Name: <input type="text" v-model="deckName"> </label>
|
||||
</div>
|
||||
<div>
|
||||
<label> Deck Type:
|
||||
<select v-model="deckInfo.type">
|
||||
<option value="hero">hero</option>
|
||||
<option value="villain">villain</option>
|
||||
<option value="environment">environment</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="cardEditor" v-if="selected">
|
||||
<button class="close-editor" @click="selected = null">X</button>
|
||||
<div v-for="(type, prop) in selectedCardProps">
|
||||
<label> {{ prop }}
|
||||
<input v-if="type === Number" v-model="selected.card[prop]"/>
|
||||
<input v-if="type === 'file'" v-model="selected.card[prop]"/>
|
||||
<textarea v-else rows="1" v-model="selected.card[prop]"> </textarea>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref="deck" :style="deckStyle">
|
||||
<div v-for="card in cards" @click="selected = card">
|
||||
<Hero v-bind="card.card"> </Hero>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Hero from './template/hero/Hero.vue'
|
||||
|
||||
export default {
|
||||
name: 'Editor',
|
||||
|
||||
components: {Hero},
|
||||
|
||||
props: ['deckName'],
|
||||
data() {
|
||||
return {
|
||||
selected: null,
|
||||
deckInfo: {},
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
deckName() {
|
||||
document.title = "Editor|" + deckName;
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
fetch('/decks/' + this.deckName + '.input.json')
|
||||
.then(r => r.json())
|
||||
.then(j => this.deckInfo = j);
|
||||
/* window.addEventListener(
|
||||
* 'beforeunload', e => e.returnValue = "Unsaved changes blah blah"); */
|
||||
},
|
||||
|
||||
computed: {
|
||||
cards() {
|
||||
return Object
|
||||
.keys(this.deckInfo)
|
||||
.filter(cardType => typeof this.deckInfo[cardType] !== 'string')
|
||||
.flatMap(cardType => this.deckInfo[cardType].flatMap((card, index) => {
|
||||
let cardWrapper = {
|
||||
type: cardType,
|
||||
card: this.deckInfo[cardType][index],
|
||||
};
|
||||
return Array(card.count || 1).fill(cardWrapper);
|
||||
}));
|
||||
},
|
||||
|
||||
deckStyle() {
|
||||
// find minimum box to fit cards
|
||||
let rows = Math.ceil(Math.sqrt(this.cards.length));
|
||||
let columns = this.cards.length > rows * (rows - 1) ? rows : rows - 1;
|
||||
return {
|
||||
display: 'grid',
|
||||
'grid-template-columns': `repeat(${rows}, 1fr)`,
|
||||
'grid-template-rows': `repeat(${columns}, 1fr)`,
|
||||
};
|
||||
},
|
||||
|
||||
selectedCardProps() {
|
||||
// todo: make better
|
||||
return Hero.props;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
// deck JSON uploader
|
||||
jsonUpload(event) {
|
||||
let files = event.target.files;
|
||||
let reader = new FileReader();
|
||||
reader.onload = event => {
|
||||
this.deckInfo = JSON.parse(event.target.result);
|
||||
};
|
||||
reader.readAsText(files[0]);
|
||||
},
|
||||
|
||||
// download input JSON
|
||||
jsonInputDownload() {
|
||||
console.log(JSON.stringify(this.deckInfo));
|
||||
this.downloadFile('data:application/json;charset=utf-8,' +
|
||||
encodeURIComponent(JSON.stringify(this.deckInfo)),
|
||||
this.deckName + '.input.json')
|
||||
},
|
||||
|
||||
// chrome doesn't seem to send input event on file select
|
||||
fileUploaded(event) {
|
||||
let prop = event.target.id.substring(5);
|
||||
if (prop === "image") {
|
||||
let files = event.target.files;
|
||||
let reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
selected.svg.querySelector('#' + prop)
|
||||
.setAttributeNS("http://www.w3.org/1999/xlink", "href", e.target.result);
|
||||
selected.json[prop] = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(files[0]);
|
||||
}
|
||||
},
|
||||
|
||||
downloadFile(file, name) {
|
||||
let dl = document.createElement('a');
|
||||
dl.setAttribute('href', file);
|
||||
dl.setAttribute('download', name);
|
||||
document.body.appendChild(dl);
|
||||
dl.click();
|
||||
document.body.removeChild(dl);
|
||||
},
|
||||
|
||||
upload() {
|
||||
// POST the generated SVGs to the server
|
||||
let data = (new XMLSerializer()).serializeToString(this.$refs.deck);
|
||||
fetch('upload', {
|
||||
method: 'post',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({body: data, json: this.deckInfo})
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#cardEditor {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: gray;
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.close-editor {
|
||||
float: right;
|
||||
}
|
||||
</style>
|
4
src/index.html
Normal file
@ -0,0 +1,4 @@
|
||||
<body>
|
||||
<div id="charSheet"> </div>
|
||||
<script type="text/javascript" src="index.js"></script>
|
||||
</body>
|
21
src/index.js
Normal file
@ -0,0 +1,21 @@
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
|
||||
import App from './App.vue';
|
||||
import DeckIndex from './DeckIndex.vue';
|
||||
import Editor from './Editor.vue';
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const router = new VueRouter({
|
||||
routes: [
|
||||
{path: '/', component: DeckIndex},
|
||||
{path: '/edit/:deckName', component: Editor, props: true},
|
||||
],
|
||||
});
|
||||
|
||||
new Vue({
|
||||
el: '#charSheet',
|
||||
render: h => h(App),
|
||||
router,
|
||||
});
|
109
src/server.js
Normal file
@ -0,0 +1,109 @@
|
||||
// jshint node:true
|
||||
// jshint esversion:6
|
||||
"use strict";
|
||||
|
||||
const express = require('express'),
|
||||
Bundler = require('parcel-bundler'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
phantom = require('phantom'),
|
||||
port = process.env.PORT || 1234;
|
||||
|
||||
const decks = ["the_Unholy_Priest_update_2",
|
||||
"NZoths_Invasion_1.2",
|
||||
"Puffer_Fish_input_1.3"];
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.use('/template', express.static('template'));
|
||||
app.use('/decks', express.static('decks'));
|
||||
app.get('/decks.json', (req, res) => res.json(decks));
|
||||
app.post('/upload', handleUpload);
|
||||
|
||||
let bundler = new Bundler(path.join(__dirname, 'index.html'));
|
||||
app.use(bundler.middleware());
|
||||
|
||||
app.listen(port, () => console.log(`App listening on port ${port}!`));
|
||||
|
||||
function handleUpload(req, res) {
|
||||
const json = request.body;
|
||||
const deckJSON = json.json;
|
||||
const cardTemplate = fs.readFileSync('template/card.json');
|
||||
const template = JSON.parse(fs.readFileSync(`template/${deckJSON.type}/input.json`));
|
||||
const cardCount = Object.entries(template.cardTypes)
|
||||
.map(ct => deckJSON[ct[0]].length * (ct[1].back ? 2 : 1))
|
||||
.reduce((sum, current) => sum + current, 0);
|
||||
|
||||
let deckOut = JSON.parse(fs.readFileSync('template/deck.json'));
|
||||
deckOut.ObjectStates[0].Nickname = deckJSON.name;
|
||||
|
||||
Object.assign(deckOut.ObjectStates[0].CustomDeck['1'],
|
||||
{NumWidth: Math.ceil(Math.sqrt(cardCount)),
|
||||
NumHeight: Math.ceil(Math.sqrt(cardCount)),
|
||||
FaceURL: `http://${req.headers.host}/deck/${deckJSON.name}/deck.png`,
|
||||
BackURL: "http://cloud-3.steamusercontent.com/ugc/156906385556221451/CE2C3AFE1759790CB0B532FFD636D05A99EC91F4/"});
|
||||
|
||||
let index = 100;
|
||||
deckOut.ObjectStates[0].ContainedObjects =
|
||||
Object.entries(template.cardTypes).map(
|
||||
cardType => deckJSON[cardType[0]].map(cardIn => {
|
||||
let cardOut = JSON.parse(cardTemplate);
|
||||
Object.assign(cardOut, {Nickname: cardIn.name,
|
||||
Description: cardIn.keywords,
|
||||
CardID: index});
|
||||
|
||||
for (let ii=0; ii<(cardIn.count || 1); ii++) {
|
||||
deckOut.ObjectStates[0].DeckIDs.push(index);
|
||||
}
|
||||
index++;
|
||||
|
||||
if(cardType[1].back) {
|
||||
let cardBack = JSON.parse(cardTemplate);
|
||||
Object.assign(cardBack, {Nickname: cardIn.back.name,
|
||||
Description: cardIn.back.keywords,
|
||||
CardID: index});
|
||||
cardOut.States = {"2": cardBack};
|
||||
index++;
|
||||
}
|
||||
return cardOut;
|
||||
}))
|
||||
.reduce((sum, cur) => sum.concat(cur), []);
|
||||
|
||||
fs.writeFileSync(`decks/${deckJSON.name}.json`, JSON.stringify(deckOut));
|
||||
fs.writeFileSync(`decks/${deckJSON.name}.input.json`, JSON.stringify(deckJSON));
|
||||
|
||||
console.log("making page");
|
||||
phantom.create().then(
|
||||
ph => ph.createPage().then(
|
||||
page => {
|
||||
page.on('onLoadFinished', status => {
|
||||
if (status !== 'success') {
|
||||
console.log('Failed to load page');
|
||||
ph.exit(1);
|
||||
}
|
||||
else {
|
||||
page.render(`decks/${deckJSON.name}.png`);
|
||||
page.close().then(() => ph.exit());
|
||||
}
|
||||
});
|
||||
page.property('zoomFactor', 2); // pretty arbitrary
|
||||
page.property('content', '<body style="margin:0;">' + json.body + '</body>');
|
||||
}));
|
||||
decks.push(deckJSON.name);
|
||||
}
|
||||
|
||||
function send404(res, uri) {
|
||||
res.writeHead(404, {'Content-type': "text/html; charset=utf-8"});
|
||||
const html = `
|
||||
<head>
|
||||
<title>404 Not Found</title>
|
||||
<link rel="stylesheet" href="/css/common.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Error 404: Path ${uri.pathname} not found</h1>
|
||||
You seem to have gone to the wrong place, would you like to go
|
||||
back to the <a href="/">main page</a>?
|
||||
</body>`;
|
||||
res.end(html, 'utf-8');
|
||||
}
|
Before Width: | Height: | Size: 278 KiB After Width: | Height: | Size: 278 KiB |
26
src/template/hero/Hero.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<v-runtime-template :template="template"> </v-runtime-template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import fs from 'fs';
|
||||
import VRuntimeTemplate from "v-runtime-template";
|
||||
|
||||
const templates = {
|
||||
'character': fs.readFileSync(__dirname + '/character.svg', 'utf8'),
|
||||
'character-back': fs.readFileSync(__dirname + '/character-back.svg', 'utf8'),
|
||||
'deck': fs.readFileSync(__dirname + '/deck.svg', 'utf8'),
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'HeroCharacter',
|
||||
props: ['name', 'hp', 'keywords', 'text', 'quote', 'quoteCitation', 'artist'],
|
||||
components: {VRuntimeTemplate},
|
||||
|
||||
data() {
|
||||
return {
|
||||
template: templates['deck'],
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
Before Width: | Height: | Size: 845 B After Width: | Height: | Size: 845 B |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
@ -310,21 +310,23 @@
|
||||
<path d="m133.05 234.55c-7.6094 4.4883-21.801 7.2969-31.816 8.4258h-56.82c-17.391-2.0781-28.762-5.7461-34.582-10.348l4e-3 -9.8086c7.7344-6.6289 31.883-12.094 62.988-12.094 36.301 0 65.734 7.6094 65.734 17 0 0.36719-0.13672 1.0977-0.13672 1.0977 10.293 1.4922 9.9648 10.367 9.9648 10.367-7.5196-7.125-15.336-4.6406-15.336-4.6406z" fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="1.25"/>
|
||||
<path d="m171.5 27.502h-162v121.59h162z" fill="none" stroke="#000" stroke-miterlimit="10" stroke-width="2"/>
|
||||
<g font-family="'RedStateBlueState BB'">
|
||||
<g id="keywordBox">
|
||||
<text x="14.173298" y="24.167526" font-family="'CrashLanding BB'" font-size="23px"> {{ name }} </text>
|
||||
<g v-if="keywords">
|
||||
<path d="m126.21 156.92h-106.91v-11.305h106.91z" fill="#f8c711"/>
|
||||
<use transform="translate(18.45,145.7) matrix(.68531 0 0 .70654 .85059 .10938)" width="100%" height="100%" mask="url(#f)" xlink:href="#l"/>
|
||||
<path d="m126.96 144.87h-108.41v12.805h108.41zm-0.75 12.055h-106.91v-11.305h106.91z"/>
|
||||
<text id="keywords" x="21.05547" y="154.41197" font-family="'RedStateBlueState BB'" font-size="11.5px" font-weight="bold">Keywords</text>
|
||||
<text x="21.05547" y="154.41197" font-size="11.5px" font-weight="bold"> {{ keywords }} </text>
|
||||
</g>
|
||||
<text id="name" x="14.173298" y="24.167526" font-family="'CrashLanding BB'" font-size="23px">Header</text>
|
||||
<g id="hpMark">
|
||||
<g v-if="hp">
|
||||
<path d="m171.5 9.5h-29.047l-17.832 3.8359 16.25 5.9336-14.375 8.5625 13.312-0.25-9.5 13.379 15.312-8.2539-8.25 15.941 13.875-13.004 5.0625 13.754 7.125-19.379 6.9375 4.3125-3.0898-9.9141 4.2188-0.25z" fill="url(#k)" stroke="#000" stroke-width=".8"/>
|
||||
<text id="hp" x="154.03" y="30.44" fill="#000000" font-family="'CrashLanding BB'" font-size="35px" text-align="center" text-anchor="middle">3</text>
|
||||
<text x="154.03" y="30.44" fill="#000000" font-family="'CrashLanding BB'" font-size="35px" text-align="center" text-anchor="middle"> {{ hp }} </text>
|
||||
</g>
|
||||
<image id="image" x="21" y="34" width="162px" height="121px" preserveAspectRatio="xMidYMid"/>
|
||||
<text id="text" x="14.48318" y="165.95828" width="152" height="48.5" font-family="'RedStateBlueState BB'" font-size="8.7px">Text Here</text>
|
||||
<text id="quote" x="72.514046" y="221.9552" font-size="6.8px" font-style="italic" text-align="center" text-anchor="middle">"Type Quote Here!"</text>
|
||||
<text id="quoteCitation" x="101.80201" y="229.85782" font-size="5.8px" text-align="end" text-anchor="end">- Name, Comic #</text>
|
||||
<text id="artist" x="168.4621" y="247.85452" fill="#ffffff" font-family="'RedStateBlueState BB'" font-size="5.3px" text-align="end" text-anchor="end">Art By</text>
|
||||
<foreignObject x="14.5" y="160" width="152" height="48.5" font-family="'RedStateBlueState BB'" font-size="8.7px">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" v-html="text"> {{ text }} </div>
|
||||
</foreignObject>
|
||||
<text x="72.514046" y="222" font-size="6.8px" font-style="italic" text-align="center" text-anchor="middle"> {{ quote }} </text>
|
||||
<text x="101.80201" y="230" font-size="5.8px" text-align="end" text-anchor="end">- {{ quoteCitation }} </text>
|
||||
<text x="168.4621" y="247.85452" fill="#ffffff" font-family="'RedStateBlueState BB'" font-size="5.3px" text-align="end" text-anchor="end"> Art By {{ artist }} </text>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 314 KiB After Width: | Height: | Size: 314 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 316 KiB After Width: | Height: | Size: 316 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |