Last active
March 17, 2024 16:40
-
-
Save fmaylinch/c511180f974da8d352663d4f2e499438 to your computer and use it in GitHub Desktop.
Arkham Horror for fmaylinch.github.io
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let version = "v0.5.12 token color"; | |
$("#main").css({padding: "0.5em"}); | |
//$('#main-button').click(); | |
let tokens = [ | |
'+1', | |
0, 0, -1, -1, -1, -2, -2, | |
-3, -4, | |
"Skull", "Skull", "Cape", | |
"Fail", "+Effect"]; | |
let phases = ["mythos", "investigation", "enemy", "keep-up"] | |
let rawDataInput; | |
let data; | |
let savedDateElem; | |
let savedDate = "" | |
let logDiv; | |
let playersElem; | |
function players() { | |
let playersStr = playersElem.find("input").val().trim(); | |
let names = playersStr ? playersStr.split(/[, ]+/) : []; // ["Roland", "Skids"]; | |
return $("<div>").each(function() { | |
for (let n of names) $(this).append(player(n)); | |
}) | |
} | |
let colors = { | |
clues: "#008000", | |
damage: "#990000", | |
doom: "#9f1da5", | |
horror: "#0052cc", | |
location: "#084408", | |
phase: "#b74e91", | |
resources: "#663300", | |
uses: "#bb6622" | |
}; | |
function locationChanged(added, place) { | |
console.log("Location " + place + " was " + (added ? "added" : "removed")); | |
let locations = $(".location") | |
if (added) { | |
locations.append($('<option>', {value:place, text:place})); | |
} else { | |
locations.find(`option[value='${place}']`).remove(); | |
} | |
} | |
let remoteStorageDataId = "remoteStorageId" | |
let remoteStorageInstanceId = 'fmaylinch.github.io/arkham-horror-lgc-tracker'; | |
function loadRemoteStorage() { | |
const remoteStorageId = getDataValue(remoteStorageDataId); | |
if (!remoteStorageId) { | |
addLog("Can't load remote storage because remoteStorageId is not set") | |
return; | |
} | |
console.log("Loading state from remote storage with id: " + remoteStorageId) | |
const remoteStorage = new RemoteStorage({ | |
userId: remoteStorageId, | |
instanceId: remoteStorageInstanceId | |
}) | |
remoteStorage.getItem('data').then(data => { | |
if (data) { | |
console.log("Remote data loaded, saving to localStorage") | |
let rawData = JSON.stringify(data); | |
window.localStorage.setItem("arkham", rawData); | |
init(); | |
} else { | |
addLog("Remote data not found") | |
} | |
}); | |
} | |
function saveRemoteStorage() { | |
const remoteStorageId = getDataValue(remoteStorageDataId); | |
if (!remoteStorageId) { | |
addLog("Can't save remote storage because remoteStorageId is not set") | |
return; | |
} | |
addLog("Saving state to remote storage with id: " + remoteStorageId) | |
const remoteStorage = new RemoteStorage({ | |
userId: remoteStorageId, | |
instanceId: remoteStorageInstanceId | |
}) | |
remoteStorage.setItem('data', data).then(() => { | |
addLog("Remote data saved") | |
}); | |
} | |
init(); | |
function init() { | |
rawDataInput = dataInput(); | |
loadStorageData(); | |
savedDateElem = text(savedDate, "p"); | |
let debugElem = $("<pre>"); | |
// debugElem.text("show code").click(() => debugElem.text(codeMirror.getValue())) | |
logDiv = $("<div>") | |
let logContainer = $("<div>") | |
.append(text("Action log", "p") | |
.append(miniButton().text("clear").click(() => logDiv.empty()))) | |
.append(logDiv); | |
playersElem = playersInput(); | |
let doom = counter("scenario.doom"); | |
addLog("tokens: " + tokens.join(", ")); | |
$("#main").empty() | |
.append( text("Arkham Horror LGC", "h1") ) | |
.append( text(version, "p") ) | |
.append( text("", "p").append(link("ArkhamDB", "http://arkhamdb.com")) ) | |
.append( textarea("notes") ) | |
.append( text("Scenario", "h2") ) | |
.append( counter("scenario.act") ) | |
.append( counter("scenario.agenda") ) | |
.append( doom ) | |
.append( text("Locations (clues)", "h2") ) | |
.append( cardAdder("locations", "#0b3610", | |
() => "e.g. " + random(["house clues 2", "house c 2", "house 2"]), | |
[["clues"]], locationChanged) | |
) | |
.append( text("Other scenario cards", "h2") ) | |
.append( cardAdder("scenario", "#471616", | |
() => "e.g. " + random(["ghoul 2", "wizard 2 1"]), | |
[["damage"], ["damage", "doom"]]) | |
) | |
.append( text("Common", "h2") ) | |
.append( | |
select("phase", phases, (phase) => { | |
if (phase === "mythos") { | |
toastr.warning("Increased doom, pick cards"); | |
let doomInput = doom.find("input"); | |
doomInput.val(+doomInput.val() + 1).change(); | |
} | |
}) | |
) | |
.append( dice("token", tokens) ) | |
.append( dice("random", 100, 5) ) | |
.append( players() ) | |
.append( $("<hr>") ) | |
.append( playersElem ) | |
.append( logContainer ) | |
.append( $("<hr>") ) | |
.append( text("Debug and experiments", "h3") ) | |
.append( text("RemoteStorage", "p") ) | |
.append( row([ | |
input(remoteStorageDataId, "", "userId"), | |
miniButton().text("load").click(() => loadRemoteStorage()), | |
miniButton().text("save").click(() => saveRemoteStorage()) ])) | |
.append( text("localStorage", "p") ) | |
.append( rawDataInput ) | |
.append( text("Data loaded: " + now(), "p") ) | |
.append( row([ text("Data saved: ", "p"), savedDateElem ]) ) | |
.append( debugElem ) | |
} | |
function fetchCode(url) { | |
fetch(url) | |
.then(resp => resp.text()) | |
.then(code => { | |
codeMirror.setValue(code) | |
}) | |
} | |
function dice(name, n, times) { | |
let t = text("").css({fontSize:30}) | |
return $("<div>").css({display:"flex", alignItems:"center"}) | |
.append(miniButton().css({padding: 7}).text(name).click(() => { | |
// let result = Array(times ?? 1).fill().map(() => random(n)).join(", "); | |
let r = random(n); | |
if (n === 100) { // result is 0..99 | |
t.fadeOut('fast', () => { | |
t.empty() | |
.append($("<span>") | |
.css({padding:`0 ${r}px`, backgroundColor:"#b74e91"})) | |
.append($("<span>") | |
.css({padding:`0 ${100-r}px`, backgroundColor:"#312450"})) | |
.append(text(r)) | |
.fadeIn('slow'); | |
}); | |
} else { | |
let color = "white"; | |
if ((""+r)[0] == "+") { | |
color = "green" | |
} else if (r == 0) { | |
color = "white" | |
} else { | |
color = "red" | |
} | |
fade(t, r, color); | |
addLog("Dice: " + r); | |
} | |
})) | |
.append(t) | |
} | |
function fade(elem, value, color) { | |
elem.fadeOut('slow', () => { | |
elem.text(value) | |
if (color) { | |
elem.css({color}); | |
} | |
elem.fadeIn('slow'); | |
}); | |
} | |
function card(playerId, cardId, props, removeCallback) { | |
let extraCss = {fontSize: "16px", padding: 0, width: 30, backgroundColor: "#660000"}; | |
let removeButton = miniButton(extraCss).text("X"); | |
let result = $("<div>"); | |
let oneRow = props.filter(p => isNaN(p)).length <= 2; | |
if (!oneRow) result.append( row([ text(cardId, "p"), removeButton ]) ); | |
let counters = []; | |
let cardsPath = playerId + ".cards"; | |
let cardPath = cardsPath + "." + cardId; | |
for (let i=0; i < props.length; i++) { | |
let id = cardPath + "." + props[i]; | |
let val0 = 0; | |
if (!isNaN(props[i+1])) { // next item is the initial value | |
val0 = +props[i+1]; | |
i++; | |
} | |
let cnt = counter(id, oneRow ? "" : undefined, val0); | |
counters.push(cnt); | |
} | |
if (oneRow) { | |
result.append(row([...counters, text(cardId), removeButton])); | |
} else { | |
counters.forEach(cnt => result.append(cnt)); | |
} | |
removeButton.click(() => { | |
result.hide('slow', () => result.remove()); | |
let cardsObj = getDataValue(cardsPath); | |
deleteData(cardsPath, cardsObj, cardId); | |
if (removeCallback) removeCallback(cardId); | |
}) | |
return result | |
} | |
function miniButton(extraCss) { | |
let css = { | |
fontSize: "20px", | |
textAlign: "center", | |
backgroundColor: "#866dc5", | |
margin: 5, padding: "3px 5px", | |
cursor: "pointer" | |
}; | |
Object.assign(css, extraCss || {}) | |
return $("<span>").css(css); | |
} | |
function player(name) { | |
let id = name.toLowerCase(); | |
let locationSelect = select(id + ".location", locationsInData()); | |
locationSelect.find("select").addClass("location"); | |
let actionCounter = counter(id + ".actions"); | |
let resCounter = counter(id + ".resources", ""); | |
let extraCss = {fontSize: "16px", padding: "3px 5px"}; | |
let maxActions = 3; | |
let nextBtn = miniButton(extraCss).text("keep-up (" + maxActions + ")"); | |
nextBtn.click(() => { | |
let actionInput = actionCounter.find("input"); | |
let actions = +actionInput.val(); | |
if (actions === 0) { | |
toastr.info('Take 1 card'); | |
let resInput = resCounter.find("input"); | |
actionInput.val(maxActions).change(); | |
resInput.val(+resInput.val() + 1).change(); | |
} else { | |
maxActions = actions; | |
nextBtn.text("keep-up (" + maxActions + ")") | |
} | |
}) | |
let result = $("<div>") | |
.append( text(name, "h2") ) | |
.append( locationSelect ) | |
.append( row([ actionCounter, nextBtn ]) ) | |
.append( row([ resCounter, counter(id + ".clues", "") ]) ) | |
.append( row([ counter(id + ".damage", ""), counter(id + ".horror", "") ]) ) | |
//.append( counter(id + ".resources") ) | |
//.append( counter(id + ".clues") ) | |
//.append( counter(id + ".damage") ) | |
//.append( counter(id + ".horror") ) | |
.append( cardAdder(id, "#122a67", | |
() => "e.g. " + random(["dog 3 1", "gun 3"]), | |
[["uses"], ["damage", "horror"]]) | |
) | |
return result | |
} | |
function select(id, opts, callback, title) { | |
let field = $("<select>").css({backgroundColor:getColor(id, "#312450")}); | |
field.append($('<option>', {value:"-", text:"-"})); | |
for (let op of opts) { | |
field.append($('<option>', {value:op, text:op})); | |
} | |
let value = getDataValue(id); | |
field.val(value); | |
field.change(() => { | |
updateData(id, field.val()); | |
if (callback) callback(field.val()); | |
}); | |
let extraCss = {fontSize: "16px", padding: "3px 5px"}; | |
let nextBtn = miniButton(extraCss).text("next"); | |
nextBtn.click(() => { | |
let opts = field.find("option").toArray().map(x => x.value); | |
let i = opts.indexOf(field.val()); | |
let iNext = (i+1) % opts.length; | |
field.val(opts[iNext]).change(); | |
}) | |
if (title === "") { | |
return row([ field, nextBtn ]) | |
} | |
return row([ field, text(title || suffix(id)), nextBtn ]) | |
} | |
function locationsInData() { | |
let cardsObj = getDataValue("locations.cards"); | |
if (!cardsObj) return []; | |
return Object.keys(cardsObj); | |
} | |
function cardAdder(baseId, bgColor, hint, defPropsVariants, callback) { | |
let input = $("<input>") | |
.attr("size", 30) | |
.attr('placeholder', hint()) | |
.css({backgroundColor: bgColor}) | |
let mainDiv = $("<div>"); | |
let cardsDiv = $("<div>"); | |
// Create cards from data | |
let cardsObj = getDataValue(baseId + ".cards"); | |
if (cardsObj) { | |
for (let cardId of Object.keys(cardsObj)) { | |
let cardData = cardsObj[cardId]; | |
let removeCallback = callback ? (x => callback(false, cardId)) : null; | |
cardsDiv.append( card(baseId, cardId, Object.keys(cardData), removeCallback) ); | |
} | |
} | |
let intputAndButton = $("<div>").css({display:"flex", alignItems:"center"}) | |
.append(input) | |
.append(miniButton().css({padding: "3px 10px"}).text("+").click(() => { | |
let cleanValue = input.val().trim(); | |
if (!cleanValue) return; | |
let parts = cleanValue.split(/ +/); | |
if (parts.length == 1) { | |
parts = [parts[0], ...defPropsVariants.at(-1)]; | |
} | |
let cardId = parts.shift(); | |
let attrs = expandAttrs(parts); | |
// if all attrs are numbers, they're values for default props | |
// e.g. [2, 1] might be converted to [damage, 2, horror, 1] | |
if (attrs.filter(a => isNaN(a)).length == 0) { | |
for (let defProps of defPropsVariants) { | |
if (attrs.length == defProps.length) { | |
attrs = _.flatten(_.zip(defProps, attrs)); | |
break; | |
} | |
} | |
} | |
let removeCallback = callback ? (x => callback(false, cardId)) : null; | |
let c = card(baseId, cardId, attrs, removeCallback).css({display: "none"}) | |
cardsDiv.append(c); | |
c.show('slow'); | |
input.val("").attr('placeholder', hint()) | |
if (callback) callback(true, cardId); | |
})) | |
return $("<div>").append(cardsDiv).append(intputAndButton); | |
} | |
function expandAttrs(attrs) { | |
let result = []; | |
let knownAttrs = Object.keys(colors); | |
for (let a of attrs) { | |
result.push(a); | |
for (let ka of knownAttrs) { | |
if (ka.startsWith(a)) { | |
result.pop(); | |
result.push(ka); | |
break; | |
} | |
} | |
} | |
return result; | |
} | |
function now() { | |
return new Date().toJSON().substr(0, 19).split('T').join(' ') | |
} | |
function text(str, tag) { | |
return $("<" + (tag || "span") + ">").css({margin: 7}).text(str) | |
} | |
function suffix(str) { | |
if (str.lastIndexOf(".") > 0) { | |
str = str.substr(str.lastIndexOf(".") + 1) | |
} | |
return str | |
} | |
function row(elems) { | |
let result = $("<div>").css({display:"flex", alignItems:"center"}); | |
elems.forEach(e => result.append(e)); | |
return result; | |
} | |
function counter(id, title, val0) { | |
let value = getDataValue(id); | |
if (value == undefined) { | |
value = val0 ?? 0; | |
updateData(id, value); | |
} | |
function counterColor(v) { | |
if (v < 0) return "#5e42a6"; | |
if (v === 0) return "#866dc5"; | |
return getColor(id, "#312450"); | |
} | |
let input = $("<input>") | |
.attr("size", 3) | |
.css({ | |
padding: 3, fontSize: 20, textAlign: "center", | |
color: "#ddd", backgroundColor: counterColor(value) | |
}) | |
.val(value) | |
.change(() => { | |
let v = +input.val(); | |
updateData(id, v); | |
input.css({backgroundColor: counterColor(v)}); | |
}); | |
let buttonCss = {width: 35, padding: "3px 0"}; | |
let result = $("<div>").css({display:"flex", alignItems:"center"}) | |
.append(miniButton().css(buttonCss).text("-").click(() => { | |
input.val(+input.val() - 1).change() | |
})) | |
.append(input) | |
.append(miniButton().css(buttonCss).text("+").click(() => { | |
input.val(+input.val() + 1).change() })) | |
if (title === "") { | |
return result; | |
} | |
return result.append(text(title || suffix(id))); | |
} | |
function input(id, title, hint) { | |
let value = getDataValue(id); | |
let field = $("<input>") | |
.attr("size", 30) | |
.css({backgroundColor:getColor(id, "#312450")}).val(value) | |
.attr('placeholder', hint || "") | |
.change(() => { | |
updateData(id, field.val()) | |
}); | |
if (title === "") { | |
return field; | |
} | |
return row([ field, text(title || suffix(id)) ]) | |
} | |
function textarea(id, hint) { | |
let value = getDataValue(id); | |
let field = $("<textarea>") | |
.val(value) | |
.attr('rows', 6) | |
.attr('placeholder', hint || suffix(id)) | |
.css({backgroundColor:"#312450"}) | |
field.change(function() { | |
updateData(id, field.val()) | |
}); | |
return field | |
} | |
function dataInput() { | |
let field = $("<textarea>").attr('rows', 5).css({backgroundColor:"#312450"}) | |
field.change(() => { | |
console.log("dataInput changed") | |
let storageItem = field.val() || "" | |
window.localStorage.setItem("arkham", storageItem) | |
savedDate = now() | |
init(); // DOM will be recreated | |
}); | |
return field; | |
} | |
function playersInput() { | |
return input("players").each(function() { | |
$(this).find("input") | |
.css({color: "#ad91f4", backgroundColor: "#5e42a6"}) | |
.attr('placeholder', "e.g. Roland Skids") | |
.change(() => { | |
init(); // DOM will be recreated | |
}); | |
}); | |
} | |
function loadStorageData() { | |
let storageItem = window.localStorage.getItem("arkham"); | |
console.log("Got value from localStorage: " + storageItem) | |
if (!storageItem) { | |
data = { scenario: {act: 1, agenda: 1, doom: 0 }, phase: "investigation" }; | |
storageItem = JSON.stringify(data); | |
} | |
rawDataInput.val(storageItem); | |
data = JSON.parse(storageItem) | |
} | |
function saveStorageData() { | |
let storageItem = JSON.stringify(data) | |
rawDataInput.val(storageItem); | |
window.localStorage.setItem("arkham", storageItem) | |
savedDateElem.text(now()) | |
} | |
function updateData(path, value, omitLog) { | |
if (!omitLog) addLog(path + " = " + JSON.stringify(value)); | |
setDataValue(path, value); | |
saveStorageData(); | |
} | |
function deleteData(path, obj, id) { | |
delete obj[id]; | |
updateData(path, obj, true); | |
addLog("deleted " + path + "." + id); | |
} | |
function addLog(str) { | |
if (logDiv) { | |
let time = new Date().toISOString().substring(11, 19); | |
logDiv.prepend($("<br>")).prepend($("<span>").text(time + " " + str)); | |
} | |
} | |
function getLastObjAndProp(obj, path) { | |
let parts = path.split("."); | |
let lastObj = parts.slice(0, -1).reduce((o, p) => { | |
if (!o[p]) o[p] = {}; | |
return o[p]; | |
}, obj); | |
return [lastObj, parts[parts.length-1]]; | |
} | |
function getDataValue(path) { | |
let [lastObj, lastProp] = getLastObjAndProp(data, path); | |
return lastObj[lastProp]; | |
} | |
function getColor(path, defaultColor) { | |
let [lastObj, lastProp] = getLastObjAndProp(data, path); | |
if (colors[lastProp]) { | |
return colors[lastProp]; | |
} | |
return defaultColor; | |
} | |
function setDataValue(path, value) { | |
let [lastObj, lastProp] = getLastObjAndProp(data, path); | |
lastObj[lastProp] = value; | |
} | |
// Returns a random integer number between 0 and n-1. | |
// If n is an array, returns a random element of n. | |
// If n is false, returns a random float number between 0 and 0.99. | |
function random(n) { | |
if (!n) { | |
return Math.floor(Math.random() * 100) / 100; | |
} | |
if (Array.isArray(n)) { | |
return n[random(n.length)] | |
} | |
return Math.floor(Math.random() * n) | |
} | |
function link(str, url) { | |
return $("<a>").attr('target', '_blank').attr('href', url) | |
.css({color: "#55dd55"}) | |
.text(str); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment