window.APP = window.angular.module('main', []).controller('MainCtrl', function($scope) { // for debugging window._scope = $scope; $scope.characters = window.Betrayal.characters; // state is what's persisted in localStorage resetState(); function resetState() { $scope.state = { explorers: [], currentTurnIndex: -1, selectTraitIndex: -1, eventDeck: shuffled(Object.keys(window.Betrayal.events)), itemDeck: shuffled(Object.keys(window.Betrayal.items)), omenDeck: shuffled(Object.keys(window.Betrayal.omens)), bigBarValue: -1, // which means hidden showDiceRollBar: false, }; getElementById("outstandingActionItems").innerHTML = ""; } // for iteration purposes $scope.traitIndexes = [0,1,2,3]; // backwards so that they grow upward $scope.healthValues = [8,7,6,5,4,3,2,1,0]; $scope.bigBarValues = [0,1,2,3,4,5,6,7,8,9,10,11,12]; // same order as traits $scope.upgradeRooms = ["Gymnasium", "Larder", "Chapel", "Library"]; var traitList = ["Speed", "Might", "Sanity", "Knowl"]; var SPEED = 0; var MIGHT = 1; var SANITY = 2; var KNOWL = 3; $scope.explorerClass = function(explorer) { return explorer === $scope.state.explorers[$scope.state.currentTurnIndex] ? "currentTurn" : ""; }; $scope.onCharacterSelect = function(explorer) { if (explorer.character) { // a character is selected. initialize all the values. initExplorer(explorer); } fixupExplorerList(); saveState(); }; function initExplorer(explorer) { explorer.health = []; explorer.traitUpgraded = []; for (var t = 0; t < $scope.traitIndexes.length; t++) { explorer.health[t] = $scope.character(explorer).traits[t].start || 0; explorer.traitUpgraded[t] = false; } explorer.inventory = []; } $scope.character = function(explorer) { return $scope.characters[explorer.character] || {}; }; $scope.traitTable = function(explorer) { return $scope.character(explorer).traits || []; }; $scope.traitColumnTitle = function(explorer, t) { if (!explorer.character) return ""; if (!explorer.traitUpgraded[t]) { return "Click when upgraded at the " + $scope.upgradeRooms[t]; } else { return "Upgraded at the " + $scope.upgradeRooms[t]; } }; $scope.onTraitColumnClick = function(explorer, t) { // toggle upgraded state explorer.traitUpgraded[t] = !explorer.traitUpgraded[t]; // increment/decrement stat var delta = explorer.traitUpgraded[t] ? 1 : -1; modifyHealthAndClamp(explorer, t, delta); saveState(); }; var upArrow = String.fromCharCode(0x2191); $scope.traitColumnText = function(explorer, t) { if (!explorer.character) return ""; var result = $scope.traitTable(explorer)[t].name; if (explorer.traitUpgraded[t]) result = result + upArrow; return result; }; $scope.modifyHealth = function(explorer, t, delta) { modifyHealthAndClamp(explorer, t, delta); saveState(); }; function modifyHealthAndClamp(explorer, t, delta) { var healths = explorer.health; healths[t] = clamp(healths[t] + delta, 0, window.Infinity); } $scope.traitCellClass = function(explorer, t, h) { if (!explorer.character) return ""; var classes = []; if(clampHealth(explorer.health[t]) === h) { classes.push("current"); classes.push("special"); if ($scope.traitTable(explorer)[t].start === h) { classes.push("starting"); } } else if ($scope.traitTable(explorer)[t].start === h) { classes.push("starting"); } else { if(("" + $scope.traitTable(explorer)[t].values[h]) === "0") { classes.push("skull"); } else { classes.push($scope.character(explorer).colorClass); } classes.push("cell"); } return classes.join(" "); }; $scope.traitButtonClass = function(explorer, t) { if ($scope.state.explorers[$scope.state.currentTurnIndex] !== explorer) return ""; if ($scope.state.selectTraitIndex !== t) return ""; return "selectedTrait"; }; $scope.traitCellTitle = function(explorer, t) { if (!explorer.character) return ""; var trait = $scope.traitTable(explorer)[t]; var traitName = $scope.traitTable(explorer)[t].name; var traitValue = trait.values[clampHealth(explorer.health[t])]; return "Open Dice Roller for " + trait.name + " " + traitValue; }; $scope.traitCell = function(explorer, t, h) { if (!explorer.character) return ""; var value = "" + $scope.traitTable(explorer)[t].values[h]; if (value === "0") { if ($scope.character(explorer).colorClass === "monster") { // 0 means not applicable value = "-"; } else { // 0 means death. use a unicode skull. value = String.fromCharCode(0x2620); } } var currentHealth = explorer.health[t]; if (currentHealth === h) { value = "[ " + value + " ]"; } else if (clampHealth(currentHealth) === h) { // overflow health var overflow = currentHealth - h; value = "[" + value + "]+" + overflow; } return value; }; $scope.drawEvent = function(explorer) { drawKeepCard(explorer, "Event"); }; $scope.drawItem = function(explorer) { drawKeepCard(explorer, "Item"); }; $scope.drawOmen = function(explorer) { drawKeepCard(explorer, "Omen"); }; function drawKeepCard(explorer, type) { var cards = Object.keys(getDeckInfo(type)); cards.sort(); $scope.getCardDialog = { type: type, cards: cards, drawTopCard: function() { var name = getCardDeck(type).pop(); gainSpecificCard(name); }, specificCard: "", getSpecificCard: function() { var name = $scope.getCardDialog.specificCard; if (getDeckInfo(type)[name] == null) return; var deck = getCardDeck(type); var index = deck.indexOf(name); if (index !== -1) { deck.splice(index, 1); // otherwise, we're duplicating it. whatever. } gainSpecificCard(name); }, }; showThisDialog("getCardDialog"); window.setTimeout(function() { document.getElementById("drawTopCardButton").focus(); }, 0); function gainSpecificCard(name) { var item = { name: name, type: type }; if (shouldKeepCard(item)) { gainItem(explorer, item); } closeDialog(); saveState(); var doItFunction = getDoCardFunction(item); var doItButtonName = "Do It"; if (doItFunction === closeDialog) doItButtonName = "OK"; if (doItFunction == null) { doItButtonName = "Do It Yourself"; doItFunction = function() { writeActionItem(getCardInfo(item).summary); closeDialog(); }; } $scope.doCardDialog = { name: name, summary: getCardInfo(item).summary, doItName: doItButtonName, doIt: function() { doItFunction(item); $scope.doCardDialog.doItName = "OK"; $scope.doCardDialog.doIt = closeDialog; saveState(); }, discard: function() { $scope.discard(explorer, item); closeDialog(); }, keep: function() { if (!shouldKeepCard(item)) { gainItem(explorer, item); saveState(); } closeDialog(); }, }; getElementById("doStuffLog").innerHTML = ""; getElementById("outstandingActionItems").innerHTML = ""; showThisDialog("doCardDialog"); setTimeout(function() { getElementById("doItButton").focus(); }, 0); } } function writeToDoStuffLog(html) { var node = document.createElement("li"); node.innerHTML = html; getElementById("doStuffLog").appendChild(node); } $scope.discard = function(explorer, item) { loseItem(explorer, item); saveState(); }; function gainItem(explorer, item) { explorer.inventory.push(item); var cardInfo = getCardInfo(item); if (cardInfo.onGain != null) { for (var trait in cardInfo.onGain) { var delta = cardInfo.onGain[trait]; var t = traitList.indexOf(trait); modifyHealthAndClamp(explorer, t, delta); } } } function loseItem(explorer, item) { var index = explorer.inventory.indexOf(item); if (index === -1) return; explorer.inventory.splice(index, 1); var cardInfo = getCardInfo(item); if (cardInfo.onLose != null) { for (var trait in cardInfo.onLose) { var delta = cardInfo.onLose[trait]; var t = traitList.indexOf(trait); modifyHealthAndClamp(explorer, t, delta); } } } function getCardInfo(card) { return getDeckInfo(card.type)[card.name]; } function getDeckInfo(type) { switch (type) { case "Event": return window.Betrayal.events; case "Item": return window.Betrayal.items; case "Omen": return window.Betrayal.omens; } throw new Error(); } function getCardDeck(type) { switch (type) { case "Event": return $scope.state.eventDeck; case "Item": return $scope.state.itemDeck; case "Omen": return $scope.state.omenDeck; } throw new Error(); } function shouldKeepCard(card) { if (card.type !== "Event") return true; switch (card.name) { case "Debris": case "Grave Dirt": case "It is Meant to Be": case "Lights Out": case "Webs": return true; } return false; } function getDoCardFunction(card) { var explorer = $scope.state.explorers[$scope.state.currentTurnIndex]; switch (card.name) { case "Book": case "Crystal Ball": case "Dog": case "Girl": case "Holy Symbol": case "Madman": case "Mask": case "Medallion": case "Ring": case "Skull": case "Spear": case "Spirit Board": return doHauntRoll; case "Bite": return function() { doAnonymousMight4Attack(explorer); doHauntRoll(); }; case "Adrenaline Shot": case "Amulet of the Ages": case "Angel Feather": case "Armor": case "Axe": case "Bell": case "Blood Dagger": case "Bottle": case "Candle": case "Dark Dice": case "Dynamite": case "Healing Salve": case "Idol": case "Lucky Stone": case "Medical Kit": case "Music Box": case "Pickpocket's Gloves": case "Puzzle Box": case "Rabbit's Foot": case "Revolver": case "Sacrificial Dagger": case "Smelling Salts": return closeDialog; case "A Moment of Hope": return closeDialog; case "Angry Being": return function() { var result = traitRollAndLog(explorer, SPEED); if (result >= 5) { modifyHealthAndLog(explorer, SPEED, 1); } else if (result >= 2) { logDiceOfDamage(explorer, "Physical", 1); } else { logDiceOfDamage(explorer, "Physical", 1); logDiceOfDamage(explorer, "Mental", 1); } }; case "Burning Man": return function() { var result = traitRollAndLog(explorer, SANITY); if (result >= 4) { modifyHealthAndLog(explorer, SANITY, 1); } else if (result >= 2) { writeActionItem(formatExplorer(explorer) + " moves to the Entrance Hall"); } else { logDiceOfDamage(explorer, "Physical", 1); logDiceOfDamage(explorer, "Mental", 1); } }; case "Bloody Vision": return function() { var result = traitRollAndLog(explorer, SANITY); if (result >= 4) { modifyHealthAndLog(explorer, SANITY, 1); } else if (result >= 2) { modifyHealthAndLog(explorer, SANITY, -1); } else { writeActionItem(formatExplorer(explorer) + " attacks something in the same room (with the lowest Might)"); } }; case "Closet Door": return closeDialog; case "Creepy Crawlies": return function() { var result = traitRollAndLog(explorer, SANITY); if (result >= 5) { modifyHealthAndLog(explorer, SANITY, 1); } else if (result >= 1) { modifyHealthAndLog(explorer, SANITY, -1); } else { modifyHealthAndLog(explorer, SANITY, -2); } }; case "Creepy Puppet": return function() { if (doAnonymousMight4Attack(explorer) > 0) { // might bonus to spear holder var playerCount = $scope.state.explorers.length - 1; for (var i = 0; i < playerCount; i++) { var otherExplorer = $scope.state.explorers[i]; if (explorer === otherExplorer) continue; var spears = otherExplorer.inventory.filter(function(item) { return item.name === "Spear"; }); if (spears.length === 0) continue; modifyHealthAndLog(otherExplorer, MIGHT, 2); } } }; case "Debris": return function(item) { var result = traitRollAndLog(explorer, SPEED); var keepIt = true; if (result >= 3) { modifyHealthAndLog(explorer, SPEED, 1); $scope.discard(explorer, item); keepIt = false; } else if (result >= 1) { logDiceOfDamage(explorer, "Physical", 1); } else { logDiceOfDamage(explorer, "Physical", 2); } if (keepIt) { writeActionItem(formatExplorer(explorer) + " is trapped under the debris"); } }; case "Disquieting Sounds": return function() { var result = rollDice(6); var omenCount = getOmenCount(); var renderedOmenCount = formatOmenCount(omenCount); writeToDoStuffLog("Roll 6d vs " + renderedOmenCount + ": " + result); if (result >= omenCount) { modifyHealthAndLog(explorer, SANITY, 1); } else { logDiceOfDamage(explorer, "Mental", 1); } }; case "Drip ... Drip ... Drip ...": return closeDialog; case "Footsteps": return function() { // assume never in the Chapel var result = rollDice(1); writeToDoStuffLog("Roll 1d: " + result); if (result >= 2) { modifyHealthAndLog(explorer, SANITY, -1); } else if (result >= 1) { modifyHealthAndLog(explorer, SPEED, -1); } else { writeActionItem("Everyone loses 1 in a trait of their choice"); } }; case "Funeral": return function() { var result = traitRollAndLog(explorer, SANITY); if (result >= 4) { modifyHealthAndLog(explorer, SANITY, 1); } else if (result >= 2) { modifyHealthAndLog(explorer, SANITY, -1); } else { modifyHealthAndLog(explorer, SANITY, -1); modifyHealthAndLog(explorer, MIGHT, -1); writeActionItem(formatExplorer(explorer) + " moves to the Crypt or Graveyard if possible"); } }; case "Grave Dirt": return function(item) { if (traitRollAndLog(explorer, MIGHT) >= 4) { modifyHealthAndLog(explorer, MIGHT, 1); $scope.discard(explorer, item); } else { writeActionItem(formatExplorer(explorer) + " is covered in grave dirt"); } }; case "Groundskeeper": return function() { if (traitRollAndLog(explorer, KNOWL) >= 4) { gainItemAndLog(explorer); } else { doAnonymousMight4Attack(explorer); } }; case "Hanged Men": return function() { var allPass = true; for (var t = 0; t < traitList.length; t++) { var result = traitRollAndLog(explorer, t); if (result < 2) { modifyHealthAndLog(explorer, t, -1); allPass = false; } } if (allPass) { writeActionItem("Gain 1 in a trait of your choice"); } }; case "Hideous Shriek": return function() { var playerCount = $scope.state.explorers.length - 1; for (var i = 0; i < playerCount; i++) { var explorer = $scope.state.explorers[i]; writeToDoStuffLog(formatExplorer(explorer) + ":"); var result = traitRollAndLog(explorer, SANITY); if (result >= 4) { logNothingHappens(); } else if (result >= 1) { logDiceOfDamage(explorer, "Mental", 1); } else { logDiceOfDamage(explorer, "Mental", 2); } } }; case "Image in the Mirror (give)": return function() { var didAnything = false; var playerCount = $scope.state.explorers.length - 1; for (var i = 0; i < playerCount; i++) { var explorer = $scope.state.explorers[($scope.state.currentTurnIndex + i) % playerCount]; var items = getItemsInInventory(explorer); if (items.length === 0) continue; var item = items[Math.floor(Math.random() * items.length)]; loseItem(explorer, item); writeToDoStuffLog(formatExplorer(explorer) + " puts an Item back: " + item.name); var deck = $scope.state.itemDeck; deck.push(item.name); writeToDoStuffLog("Shuffle the Item deck"); $scope.state.itemDeck = shuffled(deck); didAnything = true; modifyHealthAndLog(explorer, KNOWL, 1); break; } if (!didAnything) { logNothingHappens(); } }; case "Image in the Mirror (take)": return function() { gainItemAndLog(explorer); }; case "It is Meant to Be": return function() { var result = rollDice(4); writeToDoStuffLog("Roll 4d: " + result); // TODO: keep track of that? }; case "Jonah's Turn": return function() { var playerCount = $scope.state.explorers.length - 1; for (var i = 0; i < playerCount; i++) { var otherExplorer = $scope.state.explorers[($scope.state.currentTurnIndex + i) % playerCount]; var puzzleBoxes = otherExplorer.inventory.filter(function(item) { return item.name === "Puzzle Box"; }); if (puzzleBoxes.length === 0) continue; loseItem(otherExplorer, puzzleBoxes[0]); writeToDoStuffLog(formatExplorer(otherExplorer) + " loses the Puzzle Box"); gainItemAndLog(otherExplorer); modifyHealthAndLog(explorer, SANITY, 1); return; } logDiceOfDamage(explorer, "Mental", 1); }; case "Lights Out": return closeDialog; case "Locked Safe": return closeDialog; case "Mists from the Walls": return null; case "Mystic Slide": return function() { if (traitRollAndLog(explorer, MIGHT) >= 5) { writeActionItem(formatExplorer(explorer) + " moves to any room below"); } else { writeActionItem(formatExplorer(explorer) + " moves to new basement room"); logDiceOfDamage(explorer, "Physical", 1); } }; case "Night View": return function() { if (traitRollAndLog(explorer, KNOWL) >= 5) { modifyHealthAndLog(explorer, KNOWL, 1); } else { logNothingHappens(); } }; case "Phone Call": return function() { var result = rollDice(2); writeToDoStuffLog("Roll 2d: " + result); if (result >= 4) { modifyHealthAndLog(explorer, SANITY, 1); } else if (result >= 3) { modifyHealthAndLog(explorer, KNOWL, 1); } else if (result >= 1) { logDiceOfDamage(explorer, "Mental", 1); } else { logDiceOfDamage(explorer, "Physical", 2); } }; case "Possession": return function() { var trait = chooseHighestTrait(explorer, $scope.traitIndexes); if (traitRollAndLog(explorer, trait) >= 4) { writeActionItem(formatExplorer(explorer) + " gains 1 in any trait"); } else { writeToDoStuffLog(formatExplorer(explorer) + " reduces " + traitList[trait] + " to its lowest value"); explorer.health[trait] = 1; saveState(); } }; case "Revolving Wall": return closeDialog; case "Rotten": return function() { var result = traitRollAndLog(explorer, SANITY); if (result >= 5) { modifyHealthAndLog(explorer, SANITY, 1); } else if (result >= 2) { modifyHealthAndLog(explorer, MIGHT, -1); } else if (result >= 1) { modifyHealthAndLog(explorer, MIGHT, -1); modifyHealthAndLog(explorer, SPEED, -1); } else { modifyHealthAndLog(explorer, MIGHT, -1); modifyHealthAndLog(explorer, SPEED, -1); modifyHealthAndLog(explorer, SANITY, -1); modifyHealthAndLog(explorer, KNOWL, -1); } }; case "Secret Passage": return function() { var result = rollDice(3); writeToDoStuffLog("Roll 3d: " + result); if (result >= 6) { writeActionItem("Leads to any room"); } else if (result >= 4) { writeActionItem("Leads to an upper floor room"); } else if (result >= 2) { writeActionItem("Leads to a ground floor room"); } else { writeActionItem("Leads to a basement room"); } writeActionItem("You may move there right now, even without movement points left"); }; case "Secret Stairs": return null; case "Shrieking Wind": return null; case "Silence": return null; case "Skeletons": return function() { logDiceOfDamage(explorer, "Mental", 1); writeActionItem("place Skeletons here"); }; case "Smoke": return closeDialog; case "Something Hidden": return function() { if (traitRollAndLog(explorer, KNOWL) >= 4) { gainItemAndLog(explorer); } else { modifyHealthAndLog(explorer, SANITY, -1); } }; case "Something Slimy": return function() { var result = traitRollAndLog(explorer, SPEED); if (result >= 4) { modifyHealthAndLog(explorer, SPEED, 1); } else if (result >= 1) { modifyHealthAndLog(explorer, MIGHT, -1); } else { modifyHealthAndLog(explorer, MIGHT, -1); modifyHealthAndLog(explorer, SPEED, -1); } }; case "Spider": return function() { var trait = chooseHighestTrait(explorer, [SANITY, SPEED]); var result = traitRollAndLog(explorer, trait); if (result >= 4) { modifyHealthAndLog(explorer, trait, 1); } else if (result >= 1) { logDiceOfDamage(explorer, "Physical", 1); } else { logDiceOfDamage(explorer, "Physical", 2); } }; case "The Beckoning": return null; case "The Lost One": return function() { if (traitRollAndLog(explorer, KNOWL) >= 5) { modifyHealthAndLog(explorer, KNOWL, 1); } else { var total = rollDice(3); writeToDoStuffLog("Roll 3: " + total); if (total >= 6) { writeActionItem("Move to Entrance Hall"); } else if (total >= 4) { writeActionItem("Move to Upper Landing"); } else if (total >= 2) { writeActionItem("Move to a new Upper Floor room"); } else { writeActionItem("Move to a new Basement room"); } } }; case "The Voice": return function() { if (traitRollAndLog(explorer, KNOWL) >= 4) { gainItemAndLog(explorer); } else { logNothingHappens(); } }; case "The Walls": return null; case "Webs": return function(item) { if (traitRollAndLog(explorer, MIGHT) >= 4) { modifyHealthAndLog(explorer, MIGHT, 1); $scope.discard(explorer, item); } else { writeActionItem(formatExplorer(explorer) + " is caught in webs"); } }; case "What the...?": return null; case "Whoops!": return function() { var items = getItemsInInventory(explorer); if (items.length === 0) { logNothingHappens(); } else { var item = items[Math.floor(Math.random() * items.length)]; loseItem(explorer, item); writeToDoStuffLog(formatExplorer(explorer) + " loses a random Item: " + item.name); } }; } throw new Error(); } function logNothingHappens() { writeToDoStuffLog("Nothing happens."); } function rollDice(diceCount) { var total = 0; for (var i = 0; i < diceCount; i++) { total += Math.floor(Math.random() * 3); } return total; } function chooseHighestTrait(explorer, traits) { var highestTrait = traits[0]; traits.forEach(function(t) { if (getTraitValue(explorer, t) > getTraitValue(explorer, highestTrait)) { highestTrait = t; } }); return highestTrait; } function traitRollAndLog(explorer, t) { var traitValue = getTraitValue(explorer, t); var total = rollDice(traitValue); writeToDoStuffLog(traitList[t] + " Roll (" + traitValue + "d): " + total); return total; } function modifyHealthAndLog(explorer, t, delta) { $scope.modifyHealth(explorer, t, delta); var gainsLoses = delta < 0 ? "loses" : "gains"; writeToDoStuffLog(formatExplorer(explorer) + " " + gainsLoses + " " + Math.abs(delta) + " " + traitList[t]); } function logDiceOfDamage(explorer, mentalOrPhysical, diceCount) { var total = rollDice(diceCount); logDamage(explorer, mentalOrPhysical, total); } function logDamage(explorer, mentalOrPhysical, damage) { var renderedPoints = damage + " point" + (damage === 1 ? "" : "s"); var html = formatExplorer(explorer) + " takes " + renderedPoints + " of " + mentalOrPhysical + " damage"; if (damage !== 0) { writeActionItem(html); } else { writeToDoStuffLog(html); } } function gainItemAndLog(explorer) { var name = getCardDeck("Item").pop(); gainItem(explorer, {type:"Item", name:name}); writeToDoStuffLog(formatExplorer(explorer) + " gains an Item: " + name); } function getOmenCount() { return 13 - getCardDeck("Omen").length; } function formatOmenCount(omenCount) { return omenCount + " Omen" + (omenCount === 1 ? "" : "s"); } function doHauntRoll() { var omenCount = getOmenCount(); var renderedOmenCount = formatOmenCount(omenCount); var result = rollDice(6); writeToDoStuffLog("Haunt Roll (6d) with " + renderedOmenCount + ": " + result); if (result >= omenCount) { logNothingHappens(); } else { writeActionItem("The Haunt is revealed!"); } } function doAnonymousMight4Attack(explorer) { var attackDice = 4; var attackPower = rollDice(attackDice); writeToDoStuffLog("Might " + attackDice + " attack: " + attackPower); var defenseDice = getTraitValue(explorer, MIGHT); var defensePower = rollDice(defenseDice); writeToDoStuffLog("Might " + defenseDice + " defense: " + defensePower); var damage = attackPower - defensePower; if (damage > 0) { logDamage(explorer, "Physical", damage); } else { logNothingHappens(); } return damage; } function writeActionItem(html) { html = '' + html + ''; writeToDoStuffLog(html); var node = document.createElement("li"); node.innerHTML = html; getElementById("outstandingActionItems").appendChild(node); } function formatExplorer(explorer) { var color = window.Betrayal.characters[explorer.character].colorClass; return '' + explorer.character + ''; } function getItemsInInventory(explorer) { return explorer.inventory.filter(function(card) { return card.type === "Item"; }); } $scope.eventDeckDisplay = function() { return $scope.state.eventDeck.length + "/" + Object.keys(window.Betrayal.events).length; }; $scope.itemDeckDisplay = function() { return $scope.state.itemDeck.length + "/" + Object.keys(window.Betrayal.items).length; }; $scope.omenDeckDisplay = function() { return $scope.state.omenDeck.length + "/" + Object.keys(window.Betrayal.omens).length; }; $scope.itemClass = function(item) { return item.type; } $scope.modifyBigBar = function(delta) { $scope.state.bigBarValue = clamp($scope.state.bigBarValue + delta, 0, 12); saveState(); }; $scope.showHideBigBar = function() { var newValue = getElementById("showBigBar").checked; $scope.state.bigBarValue = newValue ? 0 : -1; saveState(); }; $scope.bigBarClass = function(i) { var classes = ["monospace"]; if (i === $scope.state.bigBarValue) classes.push("current"); return classes.join(" "); }; $scope.bigBarCell = function(i) { var nbsp = String.fromCharCode(0xa0); if (i === $scope.state.bigBarValue) { return nbsp + "[" + i + "]" + nbsp; } else { return nbsp + " " + i + " " + nbsp; } }; $scope.showDialog = null; function showThisDialog(dialogId) { var document = window.document; var modalMaskDiv = getElementById("modalMask"); $scope.showDialog = dialogId; var modalDialogDiv = getElementById(dialogId); modalDialogDiv.style.top = Math.floor(document.body.offsetHeight / 10) + "px"; modalDialogDiv.style.left = Math.floor(document.body.offsetWidth / 10) + "px"; // also, this selection shouldn't persist through any dialog opening. $scope.state.selectTraitIndex = -1; } document.addEventListener("keydown", documentKeyListener); function documentKeyListener(event) { switch (event.keyCode) { case 27: // Escape if (closeDialog()) break; if ($scope.state.selectTraitIndex !== -1) $scope.state.selectTraitIndex = -1; else $scope.state.currentTurnIndex = -1; break; case 32: // Space if ($scope.showDialog != null) return; if ($scope.state.selectTraitIndex !== -1) { $scope.showDiceRollerForTrait($scope.state.explorers[$scope.state.currentTurnIndex], $scope.state.selectTraitIndex); break; } var playerCount = $scope.state.explorers.length - 1; if (playerCount !== 0) { $scope.state.currentTurnIndex = ($scope.state.currentTurnIndex + 1) % playerCount; } else { $scope.state.currentTurnIndex = -1; } break; // numbers above qwerty. numpad. case "1".charCodeAt(0): case 97: $scope.showDiceRoller(1); break; case "2".charCodeAt(0): case 98: $scope.showDiceRoller(2); break; case "3".charCodeAt(0): case 99: $scope.showDiceRoller(3); break; case "4".charCodeAt(0): case 100: $scope.showDiceRoller(4); break; case "5".charCodeAt(0): case 101: $scope.showDiceRoller(5); break; case "6".charCodeAt(0): case 102: $scope.showDiceRoller(6); break; case "7".charCodeAt(0): case 103: $scope.showDiceRoller(7); break; case "8".charCodeAt(0): case 104: $scope.showDiceRoller(8); break; case 37: // Left var delta = -1; case 39: // Right if (delta == null) delta = 1; if ($scope.showDialog === "diceRollerDialog") { $scope.modifyDice(delta); } else if ($scope.showDialog == null && $scope.state.currentTurnIndex !== -1) { // select trait if ($scope.state.selectTraitIndex === -1) { // from blank, select middle two $scope.state.selectTraitIndex = 1.5 + delta / 2; } else { $scope.state.selectTraitIndex = clamp($scope.state.selectTraitIndex + delta, 0, 3); } } else return; break; case 40: // Down var delta = -1; case 38: // Up if (delta == null) delta = 1; if ($scope.showDialog === "diceRollerDialog") { $scope.modifyDice(delta); } else if ($scope.showDialog == null && $scope.state.currentTurnIndex !== -1) { if ($scope.state.selectTraitIndex !== -1) { // modify trait $scope.modifyHealth($scope.state.explorers[$scope.state.currentTurnIndex], $scope.state.selectTraitIndex, delta); } } else return; break; case "P".charCodeAt(0): var traitIndex = 0; case "M".charCodeAt(0): if (traitIndex == null) traitIndex = 1; case "A".charCodeAt(0): if (traitIndex == null) traitIndex = 2; case "K".charCodeAt(0): if (traitIndex == null) traitIndex = 3; if ($scope.showDialog == null && $scope.state.currentTurnIndex !== -1) { // select trait $scope.state.selectTraitIndex = traitIndex; } else return; break; case "E".charCodeAt(0): var cardType = "Event"; case "I".charCodeAt(0): if (cardType == null) cardType = "Item"; case "O".charCodeAt(0): if (cardType == null) cardType = "Omen"; if ($scope.showDialog == null && $scope.state.currentTurnIndex !== -1) { drawKeepCard($scope.state.explorers[$scope.state.currentTurnIndex], cardType); } else return; break; case "D".charCodeAt(0): if ($scope.showDialog == null) { $scope.showDiceRoller(6); } else return; break; default: return; } event.preventDefault(); saveState(); $scope.$apply(); } function closeDialog() { if ($scope.showDialog != null) { $scope.showDialog = null; return true; } return false; } $scope.dice = []; $scope.diceTotal = ""; $scope.reroll = []; function getTraitValue(explorer, t) { return $scope.traitTable(explorer)[t].values[clampHealth(explorer.health[t])]; } $scope.showDiceRollerForTrait = function(explorer, t) { var traitValue = getTraitValue(explorer, t); $scope.showDiceRoller(traitValue); }; $scope.showDiceRoller = function(traitValue) { showThisDialog("diceRollerDialog"); setNumberOfDice(traitValue); window.setTimeout(function() { getElementById("rollButton").focus(); }); }; $scope.modifyDice = function(delta) { setNumberOfDice($scope.dice.length + delta); }; function setNumberOfDice(numberOfDice) { $scope.dice = []; $scope.reroll = []; for (var i = 0; i < numberOfDice; i++) { $scope.dice.push("?"); $scope.reroll.push(false); } $scope.diceTotal = "?"; } $scope.dieClass = function(i) { return $scope.reroll[i] ? "reroll" : ""; }; $scope.selectDieForRerolling = function(i) { // don't toggle unrolled dice if (isNaN(parseInt($scope.dice[i], 10))) return; $scope.reroll[i] = !$scope.reroll[i]; }; function rerollCount() { var count = 0; for (var i = 0; i < $scope.dice.length; i++) { if ($scope.reroll[i]) count++; } return count; } $scope.rollButtonTitle = function () { var count = rerollCount(); if (count === 0) return "Roll"; return "Reroll " + count + " Dice"; }; $scope.rollDice = function() { var rollAll = rerollCount() === 0; var total = 0; var value; for (var i = 0; i < $scope.dice.length; i++) { if (rollAll || $scope.reroll[i]) { $scope.dice[i] = Math.floor(Math.random() * 3); $scope.reroll[i] = false; } total += $scope.dice[i]; } $scope.diceTotal = total; }; $scope.saveState = saveState; function saveState() { localStorage.betrayalState = window.angular.toJson($scope.state); } function loadState() { var cachedState = localStorage.betrayalState; if (cachedState) $scope.state = window.angular.fromJson(cachedState); } function fixupExplorerList() { var explorers = $scope.state.explorers; if (explorers.length === 0 || explorers[explorers.length - 1].character) { // add a dummy object to the end. this becomes the -- select character -- control. explorers.push({}); } else { // reduce duplicate blanks from the end for (var i = explorers.length - 2; i >= 0; i--) { if (explorers[i].character) break; // another blank. delete the last one. // keep the earlier blank one in existence so that keboard focus doesn't disappear. explorers.splice(i + 1, 1); } } } function clampHealth(h) { return clamp(h, 0, $scope.healthValues.length - 1); } function clamp(v, min, max) { return v < min ? min : v > max ? max : v; } $scope.newGameFullRandom = function() { if (!window.confirm("Reset everything and start a random game?")) return; resetState(); var colorToCharacters = {}; for (var name in window.Betrayal.characters) { var color = window.Betrayal.characters[name].colorClass; if (color === "monster") continue; if (colorToCharacters[color] == null) colorToCharacters[color] = []; colorToCharacters[color].push(name); } var colors = shuffled(Object.keys(colorToCharacters)); var playerCount = 3 + Math.floor(Math.random() * 4); for (var i = 0; i < playerCount; i++) { var name = colorToCharacters[colors[i]][Math.floor(Math.random() * 2)]; var explorer = {character: name}; initExplorer(explorer); $scope.state.explorers.push(explorer); } $scope.state.currentTurnIndex = Math.floor(Math.random() * playerCount); fixupExplorerList(); saveState(); } loadState(); fixupExplorerList(); }); function getElementById(id) { return window.document.getElementById(id); } function maybeClearState() { if (!window.confirm("Reset everything?")) return; delete localStorage.betrayalState; // refresh page window.location.href = window.location.href; } function shuffled(array) { array = array.slice(0); for (var i = array.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); if (i === j) continue; var tmp = array[i]; array[i] = array[j]; array[j] = tmp; } return array; }