Skip to content

Instantly share code, notes, and snippets.

@fmaylinch
Last active March 17, 2024 16:40
Show Gist options
  • Save fmaylinch/c511180f974da8d352663d4f2e499438 to your computer and use it in GitHub Desktop.
Save fmaylinch/c511180f974da8d352663d4f2e499438 to your computer and use it in GitHub Desktop.
Arkham Horror for fmaylinch.github.io
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