Fork of https://github.com/modality/charred-black. Short term, has some fixes. Long term, may include a tool to create and edit stock/lifepath/skill/trait data. http://charred.obscuritus.ca:8080/#/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
charred-gold/src/public/js/burning.js

3556 lines
113 KiB

/**
This is the version of the serialized character structure used by convertCurrentCharacterToStruct
and loadCurrentCharacterFromStruct.
TODO: the appropriate weapons choices need to be saved with the character!
*/
var serializeVersion = 1;
function handleIframeLoad(frameName)
{
var frame = getFrameByName(frameName);
if ( frame != null )
{
result = frame.document.getElementsByTagName("pre")[0].innerHTML;
// The form's onload handler gets called when the main page is first loaded as well.
// We detect this condition by checking if the iframes contents are not empty.
if ( result.length > 0 ){
// At this point we've uploaded a character data file and the server has sent the response (being the JSON-encoded character data)
// into the file upload iframe. We need to get the BurningCtrl $scope and call loadCurrentCharacterFromStruct with the data.
var scope = angular.element($("#accordion_main")).scope();
try{
var charStruct = angular.fromJson(result);
if( ! characterStructValid(charStruct) ){
scope.$apply(
function(){
scope.addAlert('tools', "That is not a valid character file.");
});
}
scope.$apply(
function(){
scope.loadCurrentCharacterFromStruct(charStruct);
}
);
}
catch(e){
console.log("Loading character failed: " + e);
scope.$apply(
function(){
scope.addAlert('tools', "That is not a valid character file.");
});
}
}
}
}
/**** Angular ****/
var burningModule = angular.module('burning',['ngRoute', 'ui.bootstrap']);
// Define the settings service
burningModule.service('settings', Settings);
burningModule.service('appropriateWeapons', AppropriateWeaponsService);
burningModule.service('characterStorage', CharacterStorageService);
burningModule.service('burningData', BurningDataService);
burningModule.service('weaponOfChoice', WeaponOfChoiceService);
/* Set up URL routes. Different URLs map to different HTML fragments (templates)*/
burningModule.config(function($routeProvider) {
$routeProvider.
when('/', {controller: BurningCtrl, templateUrl:'/main_partial'}).
when('/config', {controller: ConfigCtrl, templateUrl:'/config_partial'}).
when('/help', {controller: ConfigCtrl, templateUrl:'/help_partial'}).
otherwise({redirectTo:'/'});
});
/* On the help page we need to use in-page links for the table of contents.
To make this work with Angular we need the following (see http://stackoverflow.com/questions/14712223/how-to-handle-anchor-hash-linking-in-angularjs)
*/
burningModule.run(function($rootScope, $location, $anchorScroll, $routeParams) {
//when the route is changed scroll to the proper element.
$rootScope.$on('$routeChangeSuccess', function(newRoute, oldRoute) {
if($routeParams.scrollTo){
$location.hash($routeParams.scrollTo);
$anchorScroll();
}
});
});
function BurningCtrl($scope, $http, $modal, $timeout, settings, appropriateWeapons, weaponOfChoice, characterStorage, burningData){
$scope.basicAttributeNames = ["Mortal Wound", "Reflexes", "Health", "Steel", "Hesitation", "Stride", "Circles", "Resources"];
/**
This function is used when calculating the hierarchical select lists of gear or property
to display to the user.
Parameters:
listForSelect: two-dimensional array, where the first index picks the level in the hierarchy,
and the second index is an item in that level of the hierarchy.
currentItem: An array indexed by level in the hierarchy, whos value is the currently
selected item at that level.
index: The starting level in the hierarchy for which we should recalculate the lower levels
of the lists.
*/
$scope.calculateHierarchyListForSelectN = function(listForSelect, currentItem, index){
if(index < 1)
return;
while(index < 3){
if(!currentItem[index-1] || !currentItem[index-1].resources) {
listForSelect[index] = [];
currentItem[index] = {};
}
else {
listForSelect[index] = currentItem[index-1].resources;
currentItem[index] = listForSelect[index][0];
}
index++;
}
}
// Initialize the controller variables to the default empty state, except for the passed parameters which override defaults.
$scope.initialize = function(stock){
$scope.enforceLifepathReqts = settings.enforceLifepathReqts
$scope.enforcePointLimits = settings.enforcePointLimits
/* A list of DisplayLifepath. */
$scope.selectedLifepaths = [];
$scope.statNames = ["Will", "Perception", "Power", "Forte", "Agility", "Speed"];
$scope.stats = [];
$scope.statsByName = {};
// Skill exponent calculations require all the stats, and the Hatred attribute.
$scope.statsForSkillCalc = {};
for(var i = 0; i < $scope.statNames.length; i++){
var n = $scope.statNames[i];
var stat = new DisplayStat(n);
$scope.stats.push(stat);
$scope.statsByName[n] = stat;
$scope.statsForSkillCalc[n] = stat;
}
$scope.statsForSkillCalc["Hatred"] = {
"exp": function(){
// Note: this function can't be called until $scope.attribute is defined!
return $scope.attribute("Hatred").exp;
},
"calcshade": function(){
return $scope.attribute("Hatred").shade;
}
};
$scope.statsForSkillCalc["Ancestral Taint"] = {
"exp": function(){
// Note: this function can't be called until $scope.attribute is defined!
return $scope.attribute("Ancestral Taint").exp;
},
"calcshade": function(){
return $scope.attribute("Ancestral Taint").shade;
}
};
$scope.statsForSkillCalc["Spite"] = {
"exp": function(){
// Note: this function can't be called until $scope.attribute is defined!
return $scope.attribute("Spite").exp;
},
"calcshade": function(){
return $scope.attribute("Spite").shade;
}
};
// Setting names for use in the Add Lifepath section
$scope.settingNames = [];
$scope.currentSettingLifepathNames = [];
// The currently selected lifepath
$scope.currentSettingLifepath = "Loading...";
$scope.currentRelationshipDesc = "";
$scope.currentRelationshipImportance = "minor";
$scope.currentRelationshipIsImmedFam = false;
$scope.currentRelationshipIsOtherFam = false;
$scope.currentRelationshipIsRomantic = false;
$scope.currentRelationshipIsForbidden = false;
$scope.currentRelationshipIsHateful = false;
$scope.relationships = {};
$scope.currentGearDesc = "";
$scope.currentGearCost = "";
$scope.gear = {};
$scope.currentPropertyDesc = "";
$scope.currentPropertyCost = "";
$scope.property = {};
$scope.currentAffiliationDesc = "";
$scope.currentAffiliationImportance = "small";
$scope.affiliations = {};
$scope.currentReputationDesc = "";
$scope.currentReputationImportance = "local";
$scope.reputations = {};
// Hash containing total stat points categorized by
// type (physical, mental, either)
$scope.totalStatPoints = {"physical" : 0, "mental" : 0, "either" : 0}
$scope.unspentStatPoints = {"physical" : 0, "mental" : 0, "either" : 0}
$scope.totalSkillPoints = {"lifepath" : 0, "general" : 0}
$scope.unspentSkillPoints = {"lifepath" : 0, "general" : 0}
// Character name
$scope.name = "";
$scope.stock = stock;
// Character id (server side id)
$scope.charid = null;
// Character age
$scope.age = 0;
// Character gender
$scope.gender = "female";
/* Currently seleted setting in the add-lifepath section */
$scope.currentSetting = "(Select Setting)";
/* Custom wise that the user has currently entered. */
$scope.customWiseName = "";
/* Skills that character earned from lifepaths */
$scope.lifepathSkills = {};
/* Skills that character added that use general skill points */
$scope.generalSkills = {};
/* Traits that the character has chosen */
$scope.purchasedTraits = {};
/* Traits that are required from lifepaths */
$scope.requiredTraits = {};
/* Stock common traits */
$scope.commonTraits = {};
/* Traits that are on the character's lifepaths, but not necessarily taken */
$scope.lifepathTraits = {};
/* Modifiers to attributes based on the answers to questions. This applies to Greed, Steel, etc.
The hash is keyed by attribute name, and the value is a list of yes/no questions, their answers,
and the modifier applied for a yes answer.
*/
$scope.attributeModifierQuestionResults = {};
$scope.attributeBonuses = {};
/* Used to keep track of whether the user shade-shifted an attribute, for those attributes that
allow shade shifting */
$scope.attributeShade = {'Steel': 'B', 'Grief' : 'B', 'Greed' : 'B', 'Hatred' : 'B', 'Spite' : 'B'};
$scope.ptgs = new PTGS();
/* List of traits to show in the Choose Special Trait dropdown */
$scope.specialTraitsForDisplay = [];
$scope.totalTraitPoints = 0;
$scope.unspentTraitPoints = 0;
$scope.totalResourcePoints = 0;
$scope.unspentResourcePoints = 0;
$scope.currentGeneralSkill = "Loading....";
$scope.currentLifepathTrait = "Loading....";
$scope.currentSpecialTrait = "Loading...";
// Messages that warn the character of traits they must take to satisfy lifepath requirements
$scope.lifepathTraitWarnings = [];
// Warnings shown when there is an error saving character or creating character sheet.
$scope.alerts = {
'tools' : {},
'resources' : {},
'trait' : {}
};
$scope.characterStorage = characterStorage
$scope.resourceAdderToShow = 'gear';
// If this is true, then the user had added a lifepath to an Orc character that added a
// brutal life trait, and then the character removed that lifepath. According to the rules
// they can never gain more lifepaths after this action.
$scope.brutalLifeWithdrawn = false;
calculateGearSelectionLists($scope, burningData);
calculatePropertySelectionLists($scope, burningData);
$scope.serverSettings = serverSettings;
}
$scope.initialize();
if ( characterStorage.currentCharacter ){
//console.log("Loading current character");
loadCurrentCharacterFromStruct($scope, characterStorage.currentCharacter, burningData, appropriateWeapons);
}
$scope.hashValues = hashValues;
$scope.generateName = function(){
$http.get("/namegen/" + $scope.gender, {'timeout': 3000} ).
success(function(data,status,headers,config){
$scope.name = data;
}).
error(function(data,status,headers,config){
console.log("Error: generating name failed: " + data);
});
}
if ( $scope.name.length == 0 ){
$scope.generateName();
}
$scope.hasTrait = function(traitName){
return (traitName in $scope.commonTraits) || (traitName in $scope.purchasedTraits) || (traitName in $scope.requiredTraits);
}
$scope.attributeNames = function(){
var result = $scope.basicAttributeNames.slice();
if ( $scope.stock == "orc" ){
result.push("Hatred");
}
else if ( $scope.stock == "elf" ){
result.push("Grief");
// Spite must be calculated after Grief
if ( $scope.hasTrait("Spite") ) {
result.push("Spite");
}
}
else if ( $scope.stock == "dwarf" ){
result.push("Greed");
}
else if ( ($scope.stock == "man" || $scope.stock == "roden") && $scope.hasTrait("Faithful") ){
result.push("Faith");
}
else if ( $scope.stock == "wolf" && $scope.hasTrait("Chosen Wolf") ){
result.push("Ancestral Taint");
}
return result;
}
$scope.onGenderChange = function(){
if ($scope.name.length == 0) {
$scope.generateName();
}
calculateCurrentSettingLifepathNames($scope, burningData);
}
$scope.onStockChange = function(){
if(!$scope.stock) return;
if(!$scope.stockSelected) { // Removes 'select a stock' after the first selection
$scope.stocks.shift();
$scope.stockSelected = true;
}
$scope.ensureStockLoaded($scope.stock).then(() => {
var oldName = $scope.name;
// Make a blank character sheet
$scope.initialize($scope.stock);
if ( oldName.length == 0 ){
$scope.generateName();
} else {
$scope.name = oldName;
}
calculateSettingNames($scope, burningData);
calculateCurrentSettingLifepathNames($scope, burningData);
calculateSpecialTraitsForDisplay($scope, burningData);
calculateGearSelectionLists($scope, burningData);
calculatePropertySelectionLists($scope, burningData);
$scope.$digest();
});
}
$scope.ensureStockLoaded = function(stock) {
let loadPromises = [];
if(!burningData.lifepaths[stock]) {
loadPromises.push(burningData.loadLifepathsForStock(stock));
}
if(!burningData.resources[stock]) {
loadPromises.push(burningData.loadResourcesForStock(stock));
}
return Promise.all(loadPromises);
};
$scope.onSettingChange = function(){
calculateCurrentSettingLifepathNames($scope, burningData);
}
$scope.calculateAge = function(){
calculateAge($scope);
}
$scope.onLifepathTimeChange = function(lp){
calculateAge($scope);
lp.calculateResourcePoints(null);
calculateTotalResourcePoints($scope);
calculateUnspentResourcePoints($scope);
lp.calculateGeneralSkillPoints();
calculateTotalSkillPoints($scope);
openRequiredSkills($scope);
calculateUnspentSkillPoints($scope);
removeLifepathSkillsFromGeneralSkills($scope);
calculateUnspentSkillPoints($scope);
}
burningData.whenStocksLoaded.then(() => {
$scope.stocks = [{ name: "Select a stock" }, ...Object.values(burningData.stocks)];
$scope.stockSelected = false;
$scope.$digest();
});
$scope.$on('$locationChangeStart', function(event, nextUrl, currentUrl) {
var changingToMe = nextUrl.substring(nextUrl.length-3) == "/#/";
var changingFromMe = currentUrl.substring(currentUrl.length-3) == "/#/";
// Ignore self transitions
if( changingToMe && changingFromMe ){
changingToMe = false;
changingFromMe = false;
}
if ( changingFromMe ){
//console.log("Storing current character");
characterStorage.currentCharacter = $scope.convertCurrentCharacterToStruct();
}
});
$scope.onAddLifepathClick = function(){
// Find the current lifepath info in the lifepaths
var setting = burningData.lifepaths[$scope.stock][$scope.currentSetting];
if (!setting)
return;
var lifepath = setting[$scope.currentSettingLifepath];
if (!lifepath)
return;
displayLp = new DisplayLifepath($scope.currentSetting, $scope.currentSettingLifepath, lifepath);
// If this lifepath has a negative 'either' stat bonus ask the user to apply it.
var penalty = 0;
for(var i = 0; i < displayLp.stat.length; i++){
var stat = displayLp.stat[i];
if (stat[1] == 'pm' || stat[1] == 'mp') {
if (stat[0] < 0){
penalty += stat[0];
}
}
}
if (penalty < 0) {
$scope.chooseStatPenalties(displayLp, -penalty);
}
// If the lifepath contains 'Appropriate Weapons', ask the
// user to choose those weapons.
if( appropriateWeapons.hasAppropriateWeapons(displayLp) ){
var appropriate = appropriateWeapons.appropriateWeapons[displayLp.name];
if ( ! appropriate ){
// The selectAppropriateWeapons call will replace the appropriate weapons skill
// with what is selected. It will also call the passed function on successful selection.
appropriateWeapons.selectAppropriateWeapons(displayLp, function(){
$scope.addDisplayLifepath(displayLp);
});
return;
}
else {
appropriateWeapons.replaceAppropriateWeapons(displayLp, appropriate);
}
}
// If the lifepath contains 'Weapon Of Choice', ask the
// user to choose the weapon.
if( weaponOfChoice.hasWeaponOfChoice(displayLp) ){
weaponOfChoice.selectWeaponOfChoice(displayLp, function(){
$scope.addDisplayLifepath(displayLp);
});
return;
}
$scope.addDisplayLifepath(displayLp);
}
// Add a DisplayLifepath to the list of selected lifepaths for the character.
$scope.addDisplayLifepath = function(displayLp){
var prevLifepath = null;
if($scope.selectedLifepaths.length > 0)
prevLifepath = $scope.selectedLifepaths[$scope.selectedLifepaths.length-1];
displayLp.calculateResourcePoints(prevLifepath);
displayLp.calculateGeneralSkillPoints(prevLifepath);
displayLp.modifyForDiminishingReturns($scope.selectedLifepaths);
if($scope.stock == "orc"){
displayLp.applyBrutalLife($scope.selectedLifepaths);
}
$scope.selectedLifepaths.push(displayLp);
calculateAge($scope);
calculateTotalStatPoints($scope, burningData);
calculateUnspentStatPoints($scope);
calculateLifepathSkills($scope, burningData, appropriateWeapons);
calculateTotalSkillPoints($scope);
openRequiredSkills($scope);
calculateUnspentSkillPoints($scope);
removeLifepathSkillsFromGeneralSkills($scope);
calculateUnspentSkillPoints($scope);
calculateSettingNames($scope, burningData);
calculateCurrentSettingLifepathNames($scope, burningData);
calculateLifepathTraits($scope, burningData);
setCommonTraits($scope, burningData);
purchaseRequiredTraits($scope, burningData);
calculateUnspentTraitPoints($scope);
addBrutalLifeRequiredTraits($scope, burningData);
calculateTraitWarnings($scope, burningData);
calculateTotalResourcePoints($scope);
calculateUnspentResourcePoints($scope);
applyBonusesFromTraits($scope);
}
$scope.onRemoveLifepathClick = function(dlp){
// Remove this dlp. Remove the last one if there are multiple.
var index = -1;
for(var i = $scope.selectedLifepaths.length-1; i >= 0; i--) {
if ( $scope.selectedLifepaths[i].name == dlp.name ){
index = i;
break;
}
}
// If this lifepath had a Brutal Life trait (DoF was 1) then this character can never take new lifepaths if
// this one is removed.
if ( dlp.brutalLifeTraitName ){
$scope.brutalLifeWithdrawn = true;
}
$scope.selectedLifepaths.splice(index,1);
calculateAge($scope);
calculateTotalStatPoints($scope, burningData);
calculateUnspentStatPoints($scope);
calculateLifepathSkills($scope, burningData, appropriateWeapons);
calculateTotalSkillPoints($scope);
openRequiredSkills($scope);
calculateUnspentSkillPoints($scope);
correctStatPoints($scope);
calculateSettingNames($scope, burningData);
calculateCurrentSettingLifepathNames($scope, burningData);
calculateLifepathTraits($scope, burningData);
setCommonTraits($scope, burningData);
purchaseRequiredTraits($scope, burningData);
calculateUnspentTraitPoints($scope);
addBrutalLifeRequiredTraits($scope, burningData);
calculateTraitWarnings($scope, burningData);
calculateTotalResourcePoints($scope);
calculateUnspentResourcePoints($scope);
applyBonusesFromTraits($scope);
}
$scope.incrementStat = function(stat){
// Man stock has max 8 pts in any stat
if ( $scope.stock == "troll" && (stat.name == "Power" || stat.name == "Forte" ) ) {
if ( stat.exp() == 9 )
return;
} else {
if ( stat.exp() == 8 )
return;
}
var specificStatPoints = 0;
var eitherStatPoints = $scope.unspentStatPoints.either;
if("m" == stat.type){
specificStatPoints = $scope.unspentStatPoints.mental;
}
else if ("p" == stat.type){
specificStatPoints = $scope.unspentStatPoints.physical;
}
else{
console.log("Error: Unknown stat type " + stat.type + " passed to incrementStat for stat " + stat.name);
return;
}
if(specificStatPoints <= 0 && eitherStatPoints <= 0 && $scope.enforcePointLimits)
return;
if(specificStatPoints > 0){
specificStatPoints -= 1;
stat.setSpecificPointsSpent(stat.specificPointsSpent() + 1);
}
else {
eitherStatPoints -= 1;
stat.eitherPointsSpent += 1;
}
$scope.unspentStatPoints.either = eitherStatPoints;
if("m" == stat.type){
$scope.unspentStatPoints.mental = specificStatPoints;
}
else if ("p" == stat.type){
$scope.unspentStatPoints.physical = specificStatPoints;
}
calculatePTGS($scope);
}
$scope.decrementStat = function(stat){
if(stat.exp() <= 0)
return;
// First put back any 'either' category stat points, then the specific stat points.
if ( stat.eitherPointsSpent > 0 ){
stat.eitherPointsSpent -= 1;
$scope.unspentStatPoints.either += 1;
}
else {
var specificStatPoints = 0;
stat.setSpecificPointsSpent(stat.specificPointsSpent() - 1);
if("m" == stat.type){
$scope.unspentStatPoints.mental += 1;
}
else if ("p" == stat.type){
$scope.unspentStatPoints.physical += 1;
}
else{
console.log("Error: Unknown stat type " + stat.type + " passed to decrementStat for stat " + stat.name);
// Undo the decrement
stat.setSpecificPointsSpent(stat.specificPointsSpent() + 1);
return;
}
}
calculatePTGS($scope);
}
$scope.changeStatShade = function(stat){
// This method toggles between Gray and Black shades.
var toGray = function(stat) {
var specificStatPoints = 0;
var eitherStatPoints = $scope.unspentStatPoints.either;
if("m" == stat.type){
specificStatPoints = $scope.unspentStatPoints.mental;
}
else if ("p" == stat.type){
specificStatPoints = $scope.unspentStatPoints.physical;
}
else{
console.log("Error: Unknown stat type " + stat.type + " passed to incrementStat for stat " + stat.name);
return;
}
var cost = 5;
if( specificStatPoints + eitherStatPoints < cost ){
return;
}
if( specificStatPoints >= cost){
specificStatPoints -= cost;
stat.setSpecificPointsSpent(stat.specificPointsSpent() + cost);
cost = 0;
}
else{
stat.setSpecificPointsSpent(stat.specificPointsSpent() + specificStatPoints);
cost = cost - specificStatPoints;
specificStatPoints = 0;
}
eitherStatPoints -= cost;
stat.eitherPointsSpent += cost;
stat.shade = "G";
$scope.unspentStatPoints.either = eitherStatPoints;
if("m" == stat.type){
$scope.unspentStatPoints.mental = specificStatPoints;
}
else if ("p" == stat.type){
$scope.unspentStatPoints.physical = specificStatPoints;
}
}
var toBlack = function(stat){
var specificStatPoints = 0;
var eitherStatPoints = $scope.unspentStatPoints.either;
if("m" == stat.type){
specificStatPoints = $scope.unspentStatPoints.mental;
}
else if ("p" == stat.type){
specificStatPoints = $scope.unspentStatPoints.physical;
}
else{
console.log("Error: Unknown stat type " + stat.type + " passed to incrementStat for stat " + stat.name);
return;
}
var cost = 5;
// Put back 'either' points first.
if( stat.eitherPointsSpent > cost){
eitherStatPoints += cost;
stat.eitherPointsSpent -= cost;
cost = 0;
}
else {
eitherStatPoints += stat.eitherPointsSpent;
cost = cost - stat.eitherPointsSpent;
stat.eitherPointsSpent = 0;
}
specificStatPoints += cost;
stat.setSpecificPointsSpent(stat.specificPointsSpent() - cost);
stat.shade = "B";
$scope.unspentStatPoints.either = eitherStatPoints;
if ("m" == stat.type) {
$scope.unspentStatPoints.mental = specificStatPoints;
}
else if ("p" == stat.type){
$scope.unspentStatPoints.physical = specificStatPoints;
}
}
if(stat.shade == 'B'){
toGray(stat);
}
else if (stat.shade == 'G'){
toBlack(stat);
}
else
console.log("Error: changing shade of stat failed: unknown shade " + stat.shade);
}
$scope.changeAttributeShade = function(attrName){
// This method toggles between Gray and Black shades.
var attr = $scope.attribute(attrName);
if(attr.shade == 'B'){
// Need 5 points to shift.
if(attr.exp < 5)
return;
$scope.attributeShade[attrName] = 'G';
}
else if (attr.shade == 'G'){
$scope.attributeShade[attrName] = 'B';
}
else
console.log("Error: changing shade of attribute failed: unknown shade " + stat.shade);
}
$scope.incrementSkill = function(skill){
var cost = 1;
if (skill.pointsSpent() == 0){
// Magical skills cost 2 points to open.
cost = skill.costToOpen;
}
if ( $scope.unspentSkillPoints["lifepath"] + $scope.unspentSkillPoints["general"] < cost && $scope.enforcePointLimits )
return;
// Draw down from the lifepath specific points first, then from general.
if ( !$scope.isGeneralSkill(skill) && ($scope.unspentSkillPoints["lifepath"] > 0 || ! $scope.enforcePointLimits))
{
var toTake = cost;
if ( toTake > $scope.unspentSkillPoints["lifepath"] )
totake = $scope.unspentSkillPoints["lifepath"];
$scope.unspentSkillPoints["lifepath"] -= toTake;
skill.lifepathPointsSpent += toTake;
cost -= toTake;
}
if ( cost > 0 && ($scope.unspentSkillPoints["general"] > 0 || ! $scope.enforcePointLimits))
{
$scope.unspentSkillPoints["general"] -= cost;
skill.generalPointsSpent += cost;
return;
}
}
$scope.changeTrainingSkill = function($event, skill){
var checkbox = $event.target;
if ( ! checkbox.checked ){
// If this skill is required to be open, do not allow
// unchecking
var required = skillsRequiredToBeOpened($scope.selectedLifepaths)
if (skill.name in required){
checkbox.checked = true;
return;
}
if ( skill.generalPointsSpent > 0 ){
$scope.unspentSkillPoints.general += skill.generalPointsSpent;
skill.generalPointsSpent = 0;
}
if ( skill.lifepathPointsSpent > 0 ){
$scope.unspentSkillPoints.lifepath += skill.lifepathPointsSpent;
skill.lifepathPointsSpent = 0;
}
} else {
// User just checked the box.
if( !$scope.isGeneralSkill(skill) ) {
if( ($scope.unspentSkillPoints.general + $scope.unspentSkillPoints.lifepath < 2) && $scope.enforcePointLimits ){
// Not enough points
checkbox.checked = false;
return;
}
while ( $scope.unspentSkillPoints.lifepath > 0 && skill.pointsSpent() < 2 ){
skill.lifepathPointsSpent += 1;
$scope.unspentSkillPoints.lifepath -= 1;
}
while ( ($scope.unspentSkillPoints.general > 0 || ! $scope.enforcePointLimits) && skill.pointsSpent() < 2 ){
skill.generalPointsSpent += 1;
$scope.unspentSkillPoints.general -= 1;
}
} else {
// This is a general skill. Only general point may be spent.
if( $scope.unspentSkillPoints.general < 2 && $scope.enforcePointLimits ){
// Not enough points
checkbox.checked = false;
return;
}
$scope.unspentSkillPoints.general -= 2;
skill.generalPointsSpent += 2;
}
}
}
/*
// Used by ng-repeat's orderBy filter to sort skills.
$scope.sortSkills = function(skill){
return skill.exp($scope.statsByName);
}
*/
$scope.decrementSkill = function(skill){
// If this skill is required to be open, do not allow
// decrementing below 1.
var required = skillsRequiredToBeOpened($scope.selectedLifepaths)
if ( (skill.name in required) && skill.lifepathPointsSpent == 1 && skill.generalPointsSpent == 0 ){
return;
}
var cost = 1;
if (skill.pointsSpent() == skill.costToOpen){
// Magical skills cost 2 points to open.
cost = skill.costToOpen;
}
// Put back to the general points first, then to lifepath specific
if ( skill.generalPointsSpent > 0 ){
var toGive = cost;
if (toGive > skill.generalPointsSpent)
toGive = skill.generalPointsSpent;
$scope.unspentSkillPoints["general"] += toGive;
skill.generalPointsSpent -= toGive;
cost -= toGive;
}
if ( skill.lifepathPointsSpent > 0 ){
$scope.unspentSkillPoints["lifepath"] += cost;
skill.lifepathPointsSpent -= cost;
return;
}
}
/* Given a skill name, add a general skill */
$scope.addGeneralSkill = function(skillName){
if (skillName in burningData.skills && !(skillName in $scope.lifepathSkills)){
$scope.generalSkills[skillName] = new DisplaySkill(skillName, burningData.skills);
applyBonusesFromTraits($scope);
}
}
/* Given a wise name, add a custom wise.*/
$scope.addCustomWise = function(wiseName){
wiseName = wiseName.toLowerCase();
if(endsWith(wiseName, "-wise")){
wiseName = wiseName.substring(0, wiseName.length-5);
}
wiseName = capitalizeEachWord(wiseName) + "-wise";
$scope.generalSkills[wiseName] = new DisplaySkill(wiseName, burningData.skills);
}
/* Given the passed display skill, determine if it's in the list of
general skills selected by the user */
$scope.isGeneralSkill = function(displaySkill){
return (displaySkill.name in $scope.generalSkills);
}
/* Given the passed display skill, remove it from the list of general skills */
$scope.removeGeneralSkill = function(displaySkill){
delete $scope.generalSkills[displaySkill.name];
calculateUnspentSkillPoints($scope);
}
// Return a hash containing all skills
$scope.allSelectedSkills = function(){
var result = {}
for(var key in $scope.lifepathSkills){
result[key] = $scope.lifepathSkills[key]
}
for(var key in $scope.generalSkills){
result[key] = $scope.generalSkills[key]
}
return result;
}
// Return a list of skill names that the character can choose
// from to add a general skill. This is all skills less the
// skills the character already has, and less the skills that
// are not allowed for the character's stock.
$scope.selectableGeneralSkills = function(){
var result = [];
for(var key in burningData.skills){
if ( !(key in $scope.lifepathSkills) && !(key in $scope.generalSkills) ){
var displaySkill = burningData.skills[key];
if ( !displaySkill.stock || restrictionStockToValidStock(burningData.stocks, displaySkill.stock) == $scope.stock ) {
result.push(key);
}
}
}
// This is hacky, but set the current model value for the dropdown
if(result.length > 0 && result.indexOf($scope.currentGeneralSkill) < 0 ){
$scope.currentGeneralSkill = result[0];
}
return result.sort();
}
// Return a style to be applied to a skill name to indicate whether or not it's open.
$scope.skillStyle = function(skill){
if ( skill.pointsSpent() > 0 ){
return {"font-weight": "bold"};
} else {
return {};
}
}
// Return the value of the attribute with the specified name as a hash of [shade : S, exp : E, modifyable : flag]
$scope.attribute = function(name){
var computeModifiers = function(name){
var result = 0;
var questions = attributeModifyingQuestions($scope, name);
for(var i = 0; i < questions.length; i++){
var q = questions[i];
if ( q.computed ){
result += q.compute();
}
}
var answers = $scope.attributeModifierQuestionResults[name];
if ( answers ){
for(var i = 0; i < answers.length; i++){
var q = answers[i];
if ("answer" in q){
if(q.answer){
if(q.computeModifier){
result += q.compute();
}
else {
result += q.modifier;
}
}
}
}
}
return result;
}
var computeBonus = function(name){
var bonus = 0;
if (name in $scope.attributeBonuses) {
bonus = $scope.attributeBonuses[name];
}
return bonus;
}
var bonus = computeBonus(name)
if( "Mortal Wound" == name ){
var shadeAndExp;
if ( $scope.stock == 'troll' ) {
shadeAndExp = computeStatAverage($scope.statsByName, ["Power", "Forte"], true);
} else {
shadeAndExp = computeStatAverage($scope.statsByName, ["Power", "Forte"]);
}
return {"shade" : shadeAndExp[0], "exp" : shadeAndExp[1] + 6 + bonus};
}
else if ( "Reflexes" == name ){
var shadeAndExp = computeStatAverage($scope.statsByName, ["Perception", "Agility", "Speed"]);
return {"shade" : shadeAndExp[0], "exp" : shadeAndExp[1] + bonus};
}
else if ( "Health" == name ){
var shadeAndExp = computeStatAverage($scope.statsByName, ["Will", "Forte"]);
shadeAndExp[1] += computeModifiers(name);
return {"shade" : shadeAndExp[0], "exp" : shadeAndExp[1] + bonus, "modifyable" : true};
}
else if ( "Steel" == name ){
var steel = 3 + computeModifiers(name) + bonus;
if($scope.attributeShade[name] == 'G'){
steel -= 5;
}
return {"shade" : $scope.attributeShade[name], "exp" : steel, "modifyable" : true};
}
else if ( "Hesitation" == name ){
return {"shade" : "", "exp" : 10 - $scope.statsByName["Will"].exp() + bonus};
}
else if ( "Stride" == name ){
// This is a hack: if stock is unselected, use 0 for stride to not throw error; it shouldn't be displayed anyway
var stride = $scope.stock ? burningData.stocks[$scope.stock].stride : 0;
stride += bonus;
return {"shade" : "", "exp" : stride};
}
else if ( "Circles" == name ){
var v = Math.floor($scope.statsByName["Will"].exp()/2);
if (v < 1)
v = 1;
var sum = 0;
var reputations = hashValues($scope.reputations);
for(var i = 0; i < reputations.length; i++){
sum += reputations[i].cost;
}
var property = hashValues($scope.property);
for(var i = 0; i < property.length; i++){
sum += property[i].cost;
}
if ( sum >= 50 ){
v += 1;
}
v += bonus;
return {"shade" : "B", "exp" : v};
}
else if ( "Resources" == name ){
var sum = 0;
var reputations = hashValues($scope.reputations);
for(var i = 0; i < reputations.length; i++){
sum += reputations[i].cost;
}
var affiliations = hashValues($scope.affiliations);
for(var i = 0; i < affiliations.length; i++){
sum += affiliations[i].cost;
}
var property = hashValues($scope.property);
for(var i = 0; i < property.length; i++){
sum += property[i].cost;
}
return { "shade" : "B", "exp" : Math.floor(sum / 15) + bonus};
}
else if ( "Hatred" == name ){
var hate = computeModifiers(name);
if($scope.attributeShade[name] == 'G'){
hate -= 5;
}
hate += bonus;
return { "shade" : $scope.attributeShade[name], "exp" : hate, "modifyable" : true};
}
else if ( "Greed" == name ){
var greed = computeModifiers(name);
if($scope.attributeShade[name] == 'G'){
greed -= 5;
}
greed += bonus;
return { "shade" : $scope.attributeShade[name], "exp" : greed, "modifyable" : true};
}
else if ( "Grief" == name ){
var grief = computeModifiers(name);
if($scope.attributeShade[name] == 'G'){
grief -= 5;
}
grief += bonus;
return { "shade" : $scope.attributeShade[name], "exp" : grief, "modifyable" : true};
}
else if ( "Spite" == name ){
var spite = computeModifiers(name);
if($scope.attributeShade[name] == 'G'){
spite -= 5;
}
spite += bonus;
return { "shade" : $scope.attributeShade[name], "exp" : spite, "modifyable" : true};
}
else if ( "Faith" == name ){
var faith = 3 + computeModifiers(name);
faith += bonus;
return { "shade" : 'B', "exp" : faith, "modifyable" : true};
}
else if ( "Ancestral Taint" == name ){
// Taint starts +1 for Ancestral Taint (which is automatically given when Chosen Wolf is purchased)
var taint = 1 + computeModifiers(name);
taint += bonus;
return { "shade" : $scope.statsByName["Will"].shade, "exp" : taint, "modifyable" : true};
}
}
$scope.distributeStats = function(){
// Change all stats to black
for(var i = 0; i < $scope.stats.length; i++){
$scope.stats[i].shade = 'B';
}
// Divide 'either' pool evenly between physical and mental
var eitherBuckets = divideIntoBuckets($scope.totalStatPoints.either, 2);
// Divide mental between each mental stat
var mentalBuckets = divideIntoBuckets($scope.totalStatPoints.mental, 2);
var mentalEitherBuckets = divideIntoBuckets(eitherBuckets[0], 2);
// Divide physical between each physical stat
var physicalBuckets = divideIntoBuckets($scope.totalStatPoints.physical, 4);
var physicalEitherBuckets = divideIntoBuckets(eitherBuckets[1], 4);
$scope.statsByName.Will.mentalPointsSpent = mentalBuckets[0];
$scope.statsByName.Will.eitherPointsSpent = mentalEitherBuckets[1];
$scope.statsByName.Perception.mentalPointsSpent = mentalBuckets[1];
$scope.statsByName.Perception.eitherPointsSpent = mentalEitherBuckets[0];
$scope.statsByName.Power.physicalPointsSpent = physicalBuckets[0];
$scope.statsByName.Power.eitherPointsSpent = physicalEitherBuckets[3];
$scope.statsByName.Speed.physicalPointsSpent = physicalBuckets[1]
$scope.statsByName.Speed.eitherPointsSpent = physicalEitherBuckets[2]
$scope.statsByName.Agility.physicalPointsSpent = physicalBuckets[2]
$scope.statsByName.Agility.eitherPointsSpent = physicalEitherBuckets[1]
$scope.statsByName.Forte.physicalPointsSpent = physicalBuckets[3]
$scope.statsByName.Forte.eitherPointsSpent = physicalEitherBuckets[0]
// Sanity check
var sum = 0;
for(var i = 0; i < $scope.stats.length; i++){
sum += $scope.stats[i].exp() - $scope.stats[i].bonus;
}
if ( sum != $scope.totalStatPoints.either + $scope.totalStatPoints.mental + $scope.totalStatPoints.physical ) {
console.log("Error: Calculation in distributeStats is incorrect.");
for(var i = 0; i < $scope.stats.length; i++){
$scope.stats[i].physicalPointsSpent = 0;
$scope.stats[i].mentalPointsSpent = 0;
$scope.stats[i].eitherPointsSpent = 0;
}
}
calculateUnspentStatPoints($scope);
calculatePTGS($scope);
}
$scope.lifepathTraitsForDisplay = function(){
var list = [];
var traits = hashValues($scope.lifepathTraits);
for(var i = 0; i < traits.length; i++){
if ( ! (traits[i].name in $scope.purchasedTraits) && ! (traits[i].name in $scope.requiredTraits) ){
list.push(traits[i].name + ": 1pt");
}
}
if(list.length > 0 && list.indexOf($scope.currentLifepathTrait) < 0 ){
$scope.currentLifepathTrait = list[0];
}
return list;
}
$scope.addLifepathTrait = function(traitName){
if ( $scope.unspentTraitPoints < 1 && $scope.enforcePointLimits )
return;
traitName = traitName.split(":")[0];
if ( !(traitName in $scope.purchasedTraits) && !(traitName in $scope.requiredTraits)){
$scope.purchasedTraits[traitName] = new DisplayTrait(traitName, burningData.traits);
$scope.unspentTraitPoints -= 1;
}
calculateTraitWarnings($scope, burningData);
applyBonusesFromTraits($scope);
}
/* Given the passed display trait, remove it from the list of purchased traits*/
$scope.removeTrait = function(trait){
delete $scope.purchasedTraits[trait.name];
calculateUnspentTraitPoints($scope);
calculateTraitWarnings($scope, burningData);
applyBonusesFromTraits($scope);
}
$scope.addSpecialTrait = function(trait){
traitName = trait.name;
if ( !(traitName in $scope.purchasedTraits) && !(traitName in $scope.requiredTraits)){
if ( trait.cost <= $scope.unspentTraitPoints || !$scope.enforcePointLimits){
$scope.purchasedTraits[traitName] = trait;
$scope.unspentTraitPoints -= trait.cost;
}
else {
$scope.addAlert('trait', "You don't have enough trait points for that.");
}
}
calculateTraitWarnings($scope, burningData);
applyBonusesFromTraits($scope);
}
$scope.convertCurrentCharacterToStruct = function(){
return convertCurrentCharacterToStruct($scope, appropriateWeapons);
}
$scope.loadCurrentCharacterFromStruct = function(charStruct){
loadCurrentCharacterFromStruct($scope, charStruct, burningData, appropriateWeapons);
}
$scope.convertCurrentCharacterToStructForCharSheet = function(){
// Format:
// {
// :name => string,
// :stock => string,
// :age => integer,
// :stock => string,
// :lifepaths => [string, string,...]
// :stats => { :will => ["B", 5], ...}
// :attributes => { :health => ["B", 5], ...}
// :skills => [
// [name, shade, exponent]
// ]
// :traits => [
// [name, type]
// ]
// :ptgs => { :su => ["B1"], ... }
// }
// Structure:
var chardata = {
"name" : $scope.name,
"age" : $scope.age,
"stock" : $scope.stock
};
var lifepaths = [];
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
var displayLp = $scope.selectedLifepaths[i];
lifepaths.push(displayLp.displayName);
}
chardata.lifepaths = lifepaths;
var stats = {};
for(var i = 0; i < $scope.stats.length; i++){
var displayStat = $scope.stats[i];
stats[displayStat.name.toLowerCase()] = [displayStat.shade, displayStat.exp()];
}
chardata.stats = stats;
var attrs = {};
var attributeNames = $scope.attributeNames();
for(var i = 0; i < attributeNames.length; i++){
var attr = $scope.attribute(attributeNames[i]);
attrs[attributeNames[i].toLowerCase()] = [attr.shade, attr.exp];
}
chardata.attributes = attrs;
var skills = [];
for(var key in $scope.lifepathSkills){
var displaySkill = $scope.lifepathSkills[key];
if( displaySkill.pointsSpent() > 0 )
skills.push([displaySkill.name, displaySkill.shade($scope.statsForSkillCalc), displaySkill.exp($scope.statsForSkillCalc), displaySkill.isTraining]);
}
for(var key in $scope.generalSkills){
var displaySkill = $scope.generalSkills[key];
if( displaySkill.pointsSpent() > 0 )
skills.push([displaySkill.name, displaySkill.shade($scope.statsForSkillCalc), displaySkill.exp($scope.statsForSkillCalc), displaySkill.isTraining]);
}
chardata.skills = skills
var traits = [];
for(var key in $scope.purchasedTraits){
var displayTrait = $scope.purchasedTraits[key];
traits.push([displayTrait.name, displayTrait.type]);
}
for(var key in $scope.requiredTraits){
var displayTrait = $scope.requiredTraits[key];
traits.push([displayTrait.name, displayTrait.type]);
}
for(var key in $scope.commonTraits){
var displayTrait = $scope.commonTraits[key];
traits.push([displayTrait.name, displayTrait.type]);
}
chardata.traits = traits;
// Resources
var serializeResource = function(resourceHash, coder){
var resourceList = hashValues(resourceHash);
var res = [];
for(var i = 0; i < resourceList.length; i++){
var display = resourceList[i];
res.push( coder(display) );
}
return res;
}
var res = serializeResource( $scope.gear, function(display){
return display.desc
});
chardata.gear = res;
var res = serializeResource( $scope.property, function(display){
return display.desc
});
chardata.property = res;
res = serializeResource( $scope.relationships, function(display){
return display.desc
});
chardata.relationships = res;
res = serializeResource( $scope.reputations, function(display){
return display.desc + " " + display.dice + "D";
});
chardata.reputations = res;
res = serializeResource( $scope.affiliations, function(display){
return display.desc + " " + display.dice + "D";
});
chardata.affiliations = res;
chardata.ptgs = {
"su": $scope.ptgs.su,
"li": $scope.ptgs.li,
"mi": $scope.ptgs.mi,
"se": $scope.ptgs.se,
"tr": $scope.ptgs.tr,
"mo": $scope.ptgs.mo,
};
chardata.attr_mod_questions = convertAttributeModifierQuestionResultsForCharsheet($scope);
return chardata;
}
$scope.makeCharsheetForCurrentCharacter = function(){
var json = angular.toJson($scope.convertCurrentCharacterToStructForCharSheet(), true);
$http.post("/charsheet", json).
success(function(data,status,headers,config){
console.log("huzzah, making charsheet succeeded. File URL: " + data);
var frame = document.getElementById("downloadframe");
if ( frame ){
frame.src = data;
}
}).
error(function(data,status,headers,config){
console.log("boo, making charsheet failed: " + data);
$scope.addAlert('tools', "Generating character sheet failed: " + data);
});
}
// Launch a download for the current character. Since Javascript can't really
// launch a download using data from javascript, we need to pass the current character
// to the server which sends a filename back to a hidden iframe which then launches the download.
$scope.downloadCurrentCharacter = function(){
var nameWarn = "The character must have a name before it can be downloaded.";
if( $scope.name.length == 0 ){
$scope.addAlert('tools', nameWarn);
return;
}
else {
$scope.removeAlert('tools', nameWarn);
}
var json = angular.toJson($scope.convertCurrentCharacterToStruct(), true);
$http.post("/download_charfile", json).
success(function(data,status,headers,config){
console.log("huzzah, making character data file succeeded. File URL: " + data);
var frame = document.getElementById("downloadframe");
if ( frame ){
frame.src = data;
}
}).
error(function(data,status,headers,config){
console.log("boo, making character data file failed: " + data);
$scope.addAlert('tools', "Generating character file failed: " + data);
});
}
$scope.downloadCharacterModel = function(){
var json = angular.toJson($scope.convertCurrentCharacterToStructForCharSheet(), true);
$http.post("/model", json).
success(function(data,status,headers,config){
console.log("huzzah, making model succeeded. File URL: " + data);
var frame = document.getElementById("downloadframe");
if ( frame ){
frame.src = data;
}
}).
error(function(data,status,headers,config){
console.log("boo, making model failed: " + data);
$scope.addAlert('tools', "Generating character model failed: " + data);
});
}
$scope.downloadWikiSheet = function(){
var json = angular.toJson($scope.convertCurrentCharacterToStructForCharSheet(), true);
$http.post("/wiki", json).
success(function(data,status,headers,config){
console.log("huzzah, making wiki sheet succeeded. File URL: " + data);
var frame = document.getElementById("downloadframe");
if ( frame ){
frame.src = data;
}
}).
error(function(data,status,headers,config){
console.log("boo, making wiki sheet failed: " + data);
$scope.addAlert('tools', "Generating Wiki character sheet failed: " + data);
});
}
$scope.saveCurrentCharacterToServer = function(){
var nameWarn = "The character must have a name before it can be saved.";
if( $scope.name.length == 0 ){
$scope.addAlert('tools', nameWarn);
return;
}
else {
$scope.removeAlert('tools', nameWarn);
}
var json = angular.toJson($scope.convertCurrentCharacterToStruct(), true);
var url = "/create_char/user1/" + $scope.name;
if ( $scope.charid != null ){
url = "/update_char/user1/" + $scope.charid + "/" + $scope.name;
}
$http.post(url, json).
success(function(data,status,headers,config){
if ( $scope.charid == null ){
// Create
console.log("huzzah, creating character succeeded. Character id = " + data['id']);
$scope.charid = data['id']
characterStorage.loadCharacterNames();
}
else {
console.log("huzzah, updating character succeeded. Character id = " + data['id']);
}
$scope.addAlert('tools', "Character was successfully saved.", 'succ');
}).
error(function(data,status,headers,config){
console.log("boo, saving character failed: " + data);
$scope.addAlert('tools', data);
});
}
$scope.loadCharacterFromServer = function(characterIdAndNameToLoad){
if( characterIdAndNameToLoad == null ){
$scope.addAlert('tools', "Select a character to load.");
return;
}
$http.get("/get_char/user1/" + characterIdAndNameToLoad[0], {'timeout': 3000} ).
success(function(data,status,headers,config){
console.log("Loaded character");
loadCurrentCharacterFromStruct($scope, data, burningData, appropriateWeapons);
$scope.charid = characterIdAndNameToLoad[0];
}).
error(function(data,status,headers,config){
console.log("Error: Loading saved character from server failed: HTTP code " + status + ": " + data);
});
}
$scope.deleteCharacterOnServer = function(characterIdAndNameToLoad){
if( characterIdAndNameToLoad == null ){
$scope.addAlert('tools', "Select a character to delete.");
return;
}
$http.get("/delete_char/user1/" + characterIdAndNameToLoad[0], {'timeout': 3000} ).
success(function(data,status,headers,config){
console.log("Deleted character");
characterStorage.loadCharacterNames();
$scope.addAlert('tools', "Character was successfully deleted.", 'succ');
}).
error(function(data,status,headers,config){
console.log("Error: Deleting saved character from server failed: HTTP code " + status + ": " + data);
});
}
$scope.addResource = function(type){
var resource = null;
var resourceHash = null;
if(type == 'relationship'){
resource = new DisplayRelationship(
$scope.currentRelationshipDesc,
$scope.currentRelationshipImportance,
$scope.currentRelationshipIsImmedFam,
$scope.currentRelationshipIsOtherFam,
$scope.currentRelationshipIsRomantic,
$scope.currentRelationshipIsForbidden,
$scope.currentRelationshipIsHateful
);
resourceHash = $scope.relationships;
}
else if (type == 'gear'){
var cost = parseInt($scope.currentGearCost);
if ( isNaN(cost) )
return;
resource = new DisplayGear (
$scope.currentGearDesc,
cost
);
resourceHash = $scope.gear;
}
else if (type == 'property'){
var cost = parseInt($scope.currentPropertyCost);
if ( isNaN(cost) )
return;
resource = new DisplayGear (
$scope.currentPropertyDesc,
cost
);
resourceHash = $scope.property;
}
else if (type == 'affiliation'){
resource = new DisplayAffiliation(
$scope.currentAffiliationDesc,
$scope.currentAffiliationImportance
);
resourceHash = $scope.affiliations;
}
else if (type == 'reputation'){
resource = new DisplayReputation(
$scope.currentReputationDesc,
$scope.currentReputationImportance
);
resourceHash = $scope.reputations;
}
// If this already exists, ignore the new entry.
if ( resourceHash[resource.desc] ){
$scope.addAlert('resources', "You already have that " + type + ".");
return;
}
if ( resource.desc.length == 0 ){
return;
}
if ( resource.cost > $scope.unspentResourcePoints && $scope.enforcePointLimits){
$scope.addAlert('resources', "You don't have enough resource points for that.");
return;
}
resourceHash[resource.desc] = resource;
calculateUnspentResourcePoints($scope);
}
$scope.addSelectListGear = function(){
var name = "";
var cost = 0;
for(var i = 2; i >= 0; i--){
if($scope.currentSelectListGear[i]){
var gear = $scope.currentSelectListGear[i];
if(gear.cost){
cost = gear.cost;
}
if(gear.name){
if(name.length > 0){
name = gear.name + ", " + name;
}
else{
name = gear.name;
}
}
}
}
$scope.currentGearDesc = name;
$scope.currentGearCost = cost;
$scope.addResource('gear');
}
$scope.addSelectListProperty = function(){
var name = "";
var cost = 0;
for(var i = 2; i >= 0; i--){
if($scope.currentSelectListProperty[i]){
var property = $scope.currentSelectListProperty[i];
if(property.cost){
cost = property.cost;
}
if(property.name){
if(name.length > 0){
name = property.name + ", " + name;
}
else{
name = property.name;
}
}
}
}
$scope.currentPropertyDesc = name;
$scope.currentPropertyCost = cost;
$scope.addResource('property');
}
$scope.removeResource = function(type, display){
var resourceHash = null;
if(type == 'relationship'){
resourceHash = $scope.relationships;
}
else if (type == 'gear'){
resourceHash = $scope.gear;
}
else if (type == 'property'){
resourceHash = $scope.property;
}
else if (type == 'affiliation'){
resourceHash = $scope.affiliations;
}
else if (type == 'reputation'){
resourceHash = $scope.reputations;
}
$scope.resourceAdderToShow = type;
populateUiFieldsFromDisplayResource($scope, type, resourceHash[display.desc]);
delete resourceHash[display.desc];
calculateUnspentResourcePoints($scope);
}
$scope.addAlertNoTimeout = function(section, desc, type){
if(!type)
type = 'warn';
var h = $scope.alerts[section];
h[desc] = new Alert(desc, type);
$scope.alerts[section] = h;
}
$scope.addAlert = function(section, desc, type){
$scope.addAlertNoTimeout(section, desc, type);
$timeout(function(){
$scope.removeAlert(section, desc);
}, 4000);
}
$scope.removeAlert = function(section, desc) {
var h = $scope.alerts[section];
delete h[desc];
$scope.alerts[section] = h;
}
$scope.alertsOfType = function(section, type) {
var h = $scope.alerts[section];
var rc = [];
for(key in h){
var a = h[key];
if(a.type == type)
rc.push(a.desc);
}
return rc;
}
$scope.answerEmotionalAttributeQuestions = function (attributeName){
// If the character already has some or all of the questions answered, pass those in.
// Otherwise generate new ones.
var questions = attributeModifyingQuestions($scope, attributeName);
if ( ! questions)
return;
if ( attributeName in $scope.attributeModifierQuestionResults ){
questions = $scope.attributeModifierQuestionResults[attributeName];
}
var modalInstance = $modal.open({
templateUrl: '/emotional_attr_questions_partial',
controller: EmotionalAttributeModalCtrl,
resolve: {
attributeName: function() {
return attributeName;
},
questions: function () {
return questions;
},
displayEmotionalMath: function () {
return serverSettings.displayAttrMath;
},
}
});
modalInstance.result.then(function (selected) {
$scope.attributeModifierQuestionResults[attributeName] = selected;
}, function () {
console.log("Modal: User cancelled");
});
}
$scope.chooseCharacterToLoad = function() {
var modalInstance = $modal.open({
templateUrl: '/choose_character_partial',
controller: ChooseCharacterModalCtrl,
resolve: {
characterStorage: function() {
return $scope.characterStorage;
},
message: function() {
return "Choose Character to Load";
}
}
});
modalInstance.result.then(function (selected) {
console.log("Modal: User selected:");
console.log(selected);
for(var i = 0; i < selected.length; i++){
console.log(selected[i]);
}
$scope.loadCharacterFromServer(selected);
}, function () {
console.log("Modal: User cancelled");
});
}
$scope.chooseCharacterToDelete = function() {
var modalInstance = $modal.open({
templateUrl: '/choose_character_partial',
controller: ChooseCharacterModalCtrl,
resolve: {
characterStorage: function() {
return $scope.characterStorage;
},
message: function() {
return "Choose Character to Delete";
}
}
});
modalInstance.result.then(function (selected) {
console.log("Modal: User selected:");
console.log(selected);
for(var i = 0; i < selected.length; i++){
console.log(selected[i]);
}
$scope.deleteCharacterOnServer(selected);
}, function () {
console.log("Modal: User cancelled");
});
}
$scope.chooseTraitUsingAdvancedSearch = function() {
var modalInstance = $modal.open({
templateUrl: '/choose_trait_partial',
controller: TraitSearchModalCtrl,
resolve: {
specialTraitsForDisplay: function() {
return $scope.specialTraitsForDisplay;
},
totalTraitPoints: function(){
return $scope.totalTraitPoints;
},
unspentTraitPoints: function(){
return $scope.unspentTraitPoints;
}
}
});
modalInstance.result.then(function (selected) {
console.log("TraitSearchModalCtrl: User selected:");
console.log(selected);
$scope.addSpecialTrait(selected);
}, function () {
console.log("Modal: User cancelled");
});
}
$scope.showUploadCharacterModal = function (){
var modalInstance = $modal.open({
templateUrl: '/upload_character_partial',
controller: UploadCharacterModalCtrl
});
modalInstance.result.then(function () {
console.log("Modal: Uploaded character");
}, function () {
console.log("Modal: User cancelled");
});
}
$scope.chooseStatPenalties = function(lifepath, amount) {
var modalInstance = $modal.open({
templateUrl: '/choose_stat_penalty_partial',
controller: StatPenaltyModalCtrl,
resolve: {
lifepath: function() {
return lifepath;
},
amount: function() {
return amount;
}
}
});
modalInstance.result.then(function (pool) {
console.log("Modal: User selected ", pool);
// Now we want to modify the lifepath so that the 'either'
// penalty is removed and replaced with the physical and mental
// penalties the user selected. The returned penalties are positive.
var p = -pool.physical;
var m = -pool.mental;
for(var i = 0; i < displayLp.stat.length; i++){
var stat = displayLp.stat[i];
if(stat[1] == 'p'){
p += stat[0]
}
else if (stat[1] == 'm'){
m += stat[0]
}
}
displayLp.stat = [ [p, 'p'], [m, 'm'] ];
calculateTotalStatPoints($scope, burningData);
calculateUnspentStatPoints($scope);
}, function () {
console.log("Modal: User cancelled");
});
}
}
function ConfigCtrl($scope, settings, appropriateWeapons) {
$scope.enforceLifepathReqts = settings.enforceLifepathReqts
$scope.enforcePointLimits = settings.enforcePointLimits
$scope.appropriateWeapons = appropriateWeapons.appropriateWeapons;
// Export the hashKeys function
$scope.hashKeys = hashKeys;
$scope.currentAppropriateWeaponLifepath = null;
$scope.applySettings = function(){
settings.enforceLifepathReqts = $scope.enforceLifepathReqts;
settings.enforcePointLimits = $scope.enforcePointLimits;
}
$scope.editAppropriateWeapons = function(){
if ( $scope.currentAppropriateWeaponLifepath ){
appropriateWeapons.selectAppropriateWeaponsByLifepathName($scope.currentAppropriateWeaponLifepath, null);
}
}
$scope.deleteAppropriateWeapons = function(){
if ( $scope.currentAppropriateWeaponLifepath ){
delete appropriateWeapons.appropriateWeapons[$scope.currentAppropriateWeaponLifepath];
}
}
}
function onLifepathsLoad($scope, burningData){
calculateSettingNames($scope, burningData);
$scope.onSettingChange();
calculateSpecialTraitsForDisplay($scope, burningData);
}
function calculateAge($scope){
var age = 0;
var lastSetting = null;
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
var displayLp = $scope.selectedLifepaths[i];
if ( lastSetting && lastSetting != displayLp.setting && $scope.stock != "wolf"){
// New setting. Increase age by 1. Wolves don't suffer this penalty.
age += 1;
}
lastSetting = displayLp.setting;
age += displayLp.time;
}
$scope.age = age;
}
function calculateSettingNames($scope, burningData){
var settingNames = null;
var lastCurrentSetting = $scope.currentSetting;
if ( ! $scope.enforceLifepathReqts ) {
// Display all settings and subsettings
settingNames = [];
for(key in burningData.lifepaths[$scope.stock]){
settingNames.push(key);
}
}
else if ( $scope.selectedLifepaths.length == 0 ){
// All settings are allowed. Subsettings have no Born lifepath so don't include them.
settingNames = [];
for(key in burningData.lifepaths[$scope.stock]){
if( key.toLowerCase().indexOf("subsetting") < 0 ){
settingNames.push(key);
}
}
}
else {
// Only settings that are leads from the last lifepath are allowed
var lastLifepath = $scope.selectedLifepaths[$scope.selectedLifepaths.length-1];
settingNames = [];
var all = Object.keys(burningData.lifepaths[$scope.stock]);
for(var i = 0; i < all.length; i++){
//console.log("calculateSettingNames: checking if '"+all[i]+"' is allowed");
var setting = all[i];
if ( lastLifepath.setting == setting ){
settingNames.push(setting);
continue;
}
if ( lastLifepath.leads ){
for(var j = 0; j < lastLifepath.keyLeads.length; j++){
var lead = lastLifepath.keyLeads[j];
//console.log("calculateSettingNames: checking lead: '"+lead+"' is allowed");
if( setting == lead ){
settingNames.push(setting);
}
}
}
}
}
$scope.settingNames = settingNames;
var currentSettingNeedsUpdate = true;
for(var i = 0; i < $scope.settingNames.length; i++){
if( $scope.settingNames[i] == lastCurrentSetting){
currentSettingNeedsUpdate = false;
break;
}
}
if ( currentSettingNeedsUpdate && $scope.settingNames.length > 0 ){
$scope.currentSetting = $scope.settingNames[0];
}
}
function calculatePTGS($scope) {
$scope.ptgs.calculate($scope.statsByName['Forte'].exp(), $scope.attribute("Mortal Wound").exp)
}
function isBornLifepath(lifepathName) {
return lifepathName.indexOf("Born") >= 0 ||
lifepathName == "Son Of A Gun" ||
lifepathName == "Gifted Child";
}
function calculateCurrentSettingLifepathNames($scope, burningData){
var currentSettingLifepathNames = null;
if($scope.enforceLifepathReqts){
//console.log("calculateCurrentSettingLifepathNames: enforce lifepath requirements is enabled");
currentSettingLifepathNames = [];
var all = Object.keys(burningData.lifepaths[$scope.stock][$scope.currentSetting])
// Filter out the names that are not allowed based on the character's lifepaths.
if ( $scope.selectedLifepaths.length == 0 ){
// Only "Born" lifepaths are allowed
for(var i = 0; i < all.length; i++){
if ( isBornLifepath(all[i]) ){
currentSettingLifepathNames.push(all[i]);
}
}
}
else {
var lifepathNames = Object.keys(burningData.lifepaths[$scope.stock][$scope.currentSetting])
for(var j = 0; j < lifepathNames.length; j++){
var lifepathName = lifepathNames[j];
if ( isBornLifepath(lifepathName) )
continue;
var rexpr = burningData.lifepaths[$scope.stock][$scope.currentSetting][lifepathName].requires_expr
if (rexpr){
var result = areLifepathRequirementsSatisfied($scope, rexpr);
//console.log(settingName + ":" + lifepathName + " allowed: " + (result[0] ? "yes" : "no"));
//console.log("rexpr: " + rexpr);
if(result[0]){
//console.log("calculateCurrentSettingLifepathNames: added because lifepath has reqts, which are met: " + lifepathName);
currentSettingLifepathNames.push(lifepathName);
}
else {
//console.log("calculateCurrentSettingLifepathNames: not added because lifepath has reqts, which not are met: " + lifepathName);
}
}
else {
currentSettingLifepathNames.push(lifepathName);
}
}
}
}
else {
currentSettingLifepathNames = Object.keys(burningData.lifepaths[$scope.stock][$scope.currentSetting]);
}
$scope.currentSettingLifepathNames = currentSettingLifepathNames;
var currentSettingLifepathIsPresent = false;
if ( $scope.currentSettingLifepathNames.length > 0 ){
for(var i = 0; i < $scope.currentSettingLifepathNames.length; i++){
var name = $scope.currentSettingLifepathNames[i];
if ( name == $scope.currentSettingLifepath ){
currentSettingLifepathIsPresent = true;
break;
}
}
if ( ! currentSettingLifepathIsPresent )
$scope.currentSettingLifepath = $scope.currentSettingLifepathNames[0];
}
}
function calculateTotalStatPoints($scope, burningData){
var totalStatPoints = {"physical" : 0, "mental" : 0, "either" : 0};
$scope.totalStatPoints = {"physical" : 0, "mental" : 0, "either" : 0};
var mp = burningData.startingStatPts[$scope.stock].lookup($scope.age);
totalStatPoints.mental = mp[0];
totalStatPoints.physical = mp[1];
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
var displayLp = $scope.selectedLifepaths[i];
for(var j = 0; j < displayLp.stat.length; j++){
var stat = displayLp.stat[j]
if(stat[1] == 'p'){
totalStatPoints.physical += stat[0];
}
else if (stat[1] == 'm'){
totalStatPoints.mental += stat[0];
}
else if (stat[1] == 'pm' || stat[1] == 'mp'){
totalStatPoints.either += stat[0];
}
}
}
$scope.totalStatPoints = totalStatPoints;
}
/*
Preconditions: totalStatPoints is up to date.
*/
function calculateUnspentStatPoints($scope){
var unspentStatPoints = {
"physical" : $scope.totalStatPoints.physical,
"mental" : $scope.totalStatPoints.mental,
"either" : $scope.totalStatPoints.either
}
for(var i = 0; i < $scope.stats.length; i++){
var stat = $scope.stats[i];
unspentStatPoints.mental -= stat.mentalPointsSpent;
unspentStatPoints.physical -= stat.physicalPointsSpent;
unspentStatPoints.either -= stat.eitherPointsSpent;
}
$scope.unspentStatPoints = unspentStatPoints;
}
/*
Based on the chosen lifepaths, make a set of skills that the character
can or must take.
*/
function calculateLifepathSkills($scope, burningData, appropriateWeapons){
var lifepathSkills = {};
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
var displayLp = $scope.selectedLifepaths[i];
appropriateWeapons.replaceAppropriateWeaponsUsingSaved(displayLp);
displayLp.replaceWeaponOfChoice();
for(var j = 0; j < displayLp.skills.length; j++){
var name = displayLp.skills[j];
if ( name != "General"){
lifepathSkills[name] = new DisplaySkill(name, burningData.skills);
}
}
}
$scope.lifepathSkills = lifepathSkills;
}
function calculateTotalSkillPoints($scope){
var totalSkillPoints = {"lifepath" : 0, "general" : 0}
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
var displayLp = $scope.selectedLifepaths[i];
totalSkillPoints.lifepath += displayLp.lifepathSkillPts;
totalSkillPoints.general += displayLp.generalSkillPts;
}
$scope.totalSkillPoints = totalSkillPoints
}
function calculateUnspentSkillPoints($scope){
var unspentSkillPoints = {
"lifepath" : $scope.totalSkillPoints.lifepath,
"general" : $scope.totalSkillPoints.general
}
for(var key in $scope.lifepathSkills){
var skill = $scope.lifepathSkills[key]
unspentSkillPoints.lifepath -= skill.lifepathPointsSpent;
unspentSkillPoints.general -= skill.generalPointsSpent;
}
for(var key in $scope.generalSkills){
var skill = $scope.generalSkills[key]
unspentSkillPoints.lifepath -= skill.lifepathPointsSpent;
unspentSkillPoints.general -= skill.generalPointsSpent;
}
$scope.unspentSkillPoints = unspentSkillPoints;
}
function openRequiredSkills($scope){
var required = skillsRequiredToBeOpened($scope.selectedLifepaths);
var unspentSkillPoints = {
"lifepath" : $scope.unspentSkillPoints.lifepath,
"general" : $scope.unspentSkillPoints.general
}
for(var key in required){
// Do nothing if the skill is already opened
if($scope.lifepathSkills[key].pointsSpent() > 0 ){
continue;
}
if($scope.lifepathSkills[key].exp($scope.statsForSkillCalc) == 0){
if ( $scope.lifepathSkills[key].isTraining || $scope.lifepathSkills[key].isMagic ){
$scope.lifepathSkills[key].lifepathPointsSpent += 2;
unspentSkillPoints.lifepath -= 2;
} else {
$scope.lifepathSkills[key].lifepathPointsSpent += 1;
unspentSkillPoints.lifepath -= 1;
}
}
}
$scope.unspentSkillPoints = unspentSkillPoints;
}
/*
Return a hash of skill names that are required to be open
due to lifepaths.
*/
function skillsRequiredToBeOpened(lifepaths){
var skillHash = {};
for(var i = 0; i < lifepaths.length; i++){
for(var j = 0; j < lifepaths[i].skills.length; j++){
var skillName = lifepaths[i].skills[j];
if ( ! (skillName in skillHash) ){
skillHash[skillName] = 1;
break;
}
}
}
return skillHash;
}
/*
When a lifepath is removed, the stat points spent may be more than the available,
leading to a negative amount available. This method attempts to correct the situation
by lowering the spent points.
*/
function correctStatPoints($scope){
correctStatPointsHelperLowerPointsOfType($scope, 'physical', 'physicalPointsSpent');
correctStatPointsHelperLowerPointsOfType($scope, 'mental', 'mentalPointsSpent');
correctStatPointsHelperLowerPointsOfType($scope, 'either', 'eitherPointsSpent');
}
/*
Helper function used by correctStatPoints. This function tries to correct the deficit in
$scope.unspentStatPoints for the specified 'unspentStatField' (one of physical, mental, or either)
by unspending points from the stats, using the field 'displayStatField'
(one of physicalPointsSpent, mentalPointsSpent, eitherPointsSpent)
*/
function correctStatPointsHelperLowerPointsOfType($scope, unspentStatField, displayStatField){
if ( $scope.unspentStatPoints[unspentStatField] < 0 ){
for(var i = 0; i < $scope.stats.length; i++){
var needed = -$scope.unspentStatPoints[unspentStatField];
if(needed == 0){
break;
}
if( $scope.stats[i][displayStatField] > 0 ){
if ( $scope.stats[i][displayStatField] >= needed ){
$scope.stats[i][displayStatField] -= needed;
$scope.unspentStatPoints[unspentStatField] += needed;
}
else {
$scope.unspentStatPoints[unspentStatField] += $scope.stats[i][displayStatField];
$scope.stats[i][displayStatField] = 0;
}
lowered = true;
}
}
}
}
/* If the user adds some general skills to the character and then adds a lifepath that has those skills,
then those skills should be removed from the general list, and the points spent on them refunded. */
function removeLifepathSkillsFromGeneralSkills($scope){
for(var key in $scope.lifepathSkills){
if ( key in $scope.generalSkills ){
var displaySkill = $scope.generalSkills[key];
$scope.unspentStatPoints.physical += displaySkill.physicalPointsSpent;
$scope.unspentStatPoints.mental += displaySkill.mentalPointsSpent;
$scope.unspentStatPoints.either += displaySkill.eitherPointsSpent;
delete $scope.generalSkills[key]
}
}
}
function calculateLifepathTraits($scope, burningData){
var lifepathTraits = {};
var totalTraitPoints = 0;
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
var displayLp = $scope.selectedLifepaths[i];
totalTraitPoints += displayLp.traitPts;
for(var j = 0; j < displayLp.traits.length; j++){
var name = displayLp.traits[j];
lifepathTraits[name] = new DisplayTrait(name, burningData.traits);
}
}
$scope.lifepathTraits = lifepathTraits;
$scope.totalTraitPoints = totalTraitPoints;
$scope.unspentTraitPoints = totalTraitPoints;
}
function setCommonTraits($scope, burningData){
var commonTraits = {}
if( $scope.selectedLifepaths.length == 0 )
return;
var common = burningData.stocks[$scope.stock].common_traits;
if(common.length > 0){
for(var j = 0; j < common.length; j++){
var name = common[j];
commonTraits[name] = new DisplayTrait(name, burningData.traits);
}
}
$scope.commonTraits = commonTraits;
}
function purchaseRequiredTraits($scope, burningData){
var requiredTraits = {};
var unspentTraitPoints = $scope.unspentTraitPoints;
var required = traitsRequiredToBeOpened($scope.selectedLifepaths);
for(name in required){
requiredTraits[name] = new DisplayTrait(name, burningData.traits);
unspentTraitPoints -= 1;
}
$scope.unspentTraitPoints = unspentTraitPoints;
$scope.requiredTraits = requiredTraits;
}
function addBrutalLifeRequiredTraits($scope, burningData){
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
var lp = $scope.selectedLifepaths[i];
if( lp.brutalLifeTraitName ){
var trait = new DisplayTrait(lp.brutalLifeTraitName, burningData.traits);
$scope.requiredTraits[lp.brutalLifeTraitName] = trait;
}
}
}
/*
Return a hash of trait names that are required to be open
due to lifepaths.
*/
function traitsRequiredToBeOpened(lifepaths){
var traitHash = {};
for(var i = 0; i < lifepaths.length; i++){
for(var j = 0; j < lifepaths[i].traits.length; j++){
var traitName = lifepaths[i].traits[j];
if ( ! (traitName in traitHash) ){
traitHash[traitName] = 1;
break;
}
}
}
return traitHash;
}
function calculateUnspentTraitPoints($scope){
var unspentTraitPoints = $scope.totalTraitPoints;
var required = hashValues($scope.requiredTraits);
unspentTraitPoints -= required.length;
var purchased = hashValues($scope.purchasedTraits);
for(var i = 0; i < purchased.length; i++){
var trait = purchased[i];
if ( trait.name in $scope.lifepathTraits ){
unspentTraitPoints -= 1;
} else {
unspentTraitPoints -= trait.cost;
}
}
$scope.unspentTraitPoints = unspentTraitPoints;
}
/*
rexpr are the requires_expr. Returns a two-element list.
The first element is true if the requirements are satisifed, false
otherwise. The second element are any extra conditions if the first
element is true. These extra conditions semantically descibe extra conditions
that must _later_ be met, for example "the requirements are satisfied as long as
the character takes the trait 'your grace' "
The extra conditions supported so far are only a list of trait names.
*/
function areLifepathRequirementsSatisfied($scope, rexpr){
// make lookup tables
var selectedLifepathsByName = {}
for(var i = 0; i < $scope.selectedLifepaths.length; i++) {
selectedLifepathsByName[$scope.selectedLifepaths[i].name.toLowerCase()] = true;
}
var selectedLifepathsBySettingAndName = {}
for(var i = 0; i < $scope.selectedLifepaths.length; i++) {
selectedLifepathsBySettingAndName[$scope.selectedLifepaths[i].setting.toLowerCase() + ":" + $scope.selectedLifepaths[i].name.toLowerCase()] = true;
}
var checkHasLifepathIn = function(rexpr){
// This is a [+has_lifepath_in, lp1, lp2, ...] OR [lifepath, lifepath] array.
for(var i = 0; i < rexpr.length; i++){
var s = rexpr[i]
if(i == 0 && s.substring(0,1) == "+")
continue;
// Does the requirement list the required lifepath as setting:name or just name?
if( s.indexOf(":") < 0 ){
if( selectedLifepathsByName[s] )
return [true,[]];
}
else {
if( selectedLifepathsBySettingAndName[s] )
return [true,[]];
}
}
return [false,[]];
}
var checkHasSex = function(rexpr){
if (rexpr.length < 2){
console.log("Error in areLifepathRequirementsSatisfied when evaluating expression: sex predicate is length < 2 when it must be 2");
return [false, []];
}
return [rexpr[1].toLowerCase() == $scope.gender.toLowerCase(), []];
}
var checkHasNLifepathsIn = function(rexpr){
if (rexpr.length < 3){
console.log("Error in areLifepathRequirementsSatisfied when evaluating expression: has_n_lifepaths_in predicate is length < 3 when it must be 3");
return [false, []];
}
var requiredCount = rexpr[1];
var actualCount = 0;
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
var lifepath = $scope.selectedLifepaths[i]
for(var j = 2; j < rexpr.length; j++){
var s = rexpr[j]
// Does the requirement list the required lifepath as setting:name or just name?
var lpName = null;
if( s.indexOf(":") < 0 ){
// simple name
lpName = lifepath.name.toLowerCase();
} else {
lpName = lifepath.setting.toLowerCase() + ":" + lifepath.name.toLowerCase();
}
if(lpName == s){
actualCount += 1;
break;
}
}
}
return [actualCount >= requiredCount, []];
}
var checkHasNLifepathsOrMore = function(rexpr){
if (rexpr.length < 2){
console.log("Error in areLifepathRequirementsSatisfied when evaluating expression: has_n_lifepaths_or_more predicate is length < 2 when it must be 2");
return [false, []];
}
return [$scope.selectedLifepaths.length >= rexpr[1], []];
}
var checkHasNLifepathsOrLess = function(rexpr){
if (rexpr.length < 2){
console.log("Error in areLifepathRequirementsSatisfied when evaluating expression: has_n_lifepaths_or_less predicate is length < 2 when it must be 2");
return [false, []];
}
return [$scope.selectedLifepaths.length <= rexpr[1], []];
}
var checkAgeLessThan = function(rexpr){
if (rexpr.length < 2){
console.log("Error in areLifepathRequirementsSatisfied when evaluating expression: age_less_than predicate is length < 2 when it must be 2");
return [false, []];
}
return [$scope.age < rexpr[1], []];
}
var checkAgeGreaterThan = function(rexpr){
if (rexpr.length < 2){
console.log("Error in areLifepathRequirementsSatisfied when evaluating expression: age_greater_than predicate is length < 2 when it must be 2");
return [false, []];
}
return [$scope.age > rexpr[1], []];
}
var checkTrait = function(rexpr){
if (rexpr.length < 2){
console.log("Error in areLifepathRequirementsSatisfied when evaluating expression: trait predicate is length < 2 when it must be 2");
return [false, []];
}
return [true, [rexpr[1]]]
}
var checkExpr = function(type, rexpr){
if ( type != "or" && type != "and" ){
console.log("Error in areLifepathRequirementsSatisfied when evaluating expression: checkExpr called with neither 'or' nor 'and'");
return [false, []];
}
if (rexpr.length < 2){
console.log("Error in areLifepathRequirementsSatisfied when evaluating expression: '"+type+"' expression is length < 2 when it must be 2 or more");
return [false, []];
}
var result;
if ( type == "or" ){
result = false;
} else {
result = true;
}
var conditions = [];
for(var i = 1; i < rexpr.length; i++){
// Each element in an expression is itself an expression or a predicate.
var newRexpr = rexpr[i];
var evalResult = areLifepathRequirementsSatisfied($scope, newRexpr);
if ( type == "or" ){
if ( evalResult[0] ){
result = true;
conditions = evalResult[1];
break;
}
} else {
// and
if ( ! evalResult[0] ){
result = false;
break;
}
// Append any returned conditions in newRexpr.
for(var j = 0; j < evalResult[1].length; j++){
conditions.push(evalResult[1][j]);
}
}
}
if ( ! result ){
conditions = [];
}
return [result, conditions];
}
var checkNotExpr = function(rexpr){
if (rexpr.length != 2){
console.log("Error in areLifepathRequirementsSatisfied when evaluating expression: '"+type+"' expression is length "+rexpr.length+" when it must exactly 2");
return [false, []];
}
var evalResult = areLifepathRequirementsSatisfied($scope, rexpr[1]);
return [!evalResult[0], evalResult[1]];
}
// evaluate expression
if ( rexpr.length < 1 ){
console.log("Error in areLifepathRequirementsSatisfied when evaluating expression: expression is length 0!");
return [false,[]];
}
var type = rexpr[0]
if( type == "+has_lifepath_in" || type.substring(0,1) != "+"){
return checkHasLifepathIn(rexpr);
} else if( type == "+sex"){
return checkHasSex(rexpr);
} else if( type == "+has_n_lifepaths_in"){
return checkHasNLifepathsIn(rexpr);
} else if( type == "+has_n_lifepaths_or_more"){
return checkHasNLifepathsOrMore(rexpr);
} else if( type == "+has_n_lifepaths_or_less"){
return checkHasNLifepathsOrLess(rexpr);
} else if( type == "+age_less_than"){
return checkAgeLessThan(rexpr);
} else if( type == "+age_greater_than"){
return checkAgeGreaterThan(rexpr);
} else if( type == "+trait"){
return checkTrait(rexpr);
} else if( type == "+and"){
return checkExpr("and", rexpr)
} else if( type == "+or"){
return checkExpr("or", rexpr);
} else if( type == "+not"){
return checkNotExpr(rexpr);
} else {
console.log("No support for lifepath requirement expression " + type);
return [false,[]];
}
}
function weaponSkillsNames(){
return ["Axe", "Bow", "Cudgel", "Crossbow", "Firearms", "Firebombs", "Hammer", "Knives", "Lance", "Mace", "Polearm", "Spear", "Staff", "Sword"]
}
// Perform some simple validation on the char struct to ensure it seems mostly legit.
function characterStructValid(charStruct){
if(!("serialize_version" in charStruct))
return false;
if(!("name" in charStruct))
return false;
if(!("gender" in charStruct))
return false;
if(!("stock" in charStruct))
return false;
if(!("lifepaths" in charStruct))
return false;
if(!("stats" in charStruct))
return false;
if(!("skills" in charStruct))
return false;
if(!("traits" in charStruct))
return false;
if(!("gear" in charStruct))
return false;
if(!("property" in charStruct))
return false;
if(!("relationships" in charStruct))
return false;
if(!("reputations" in charStruct))
return false;
if(!("affiliations" in charStruct))
return false;
return true;
}
function calculateTraitWarnings($scope, burningData){
// Make lookup maps of traits using lower-case trait names
var allTakenTraitNames = {};
for(var key in $scope.purchasedTraits){
allTakenTraitNames[key.toLowerCase()] = 1;
}
for(var key in $scope.requiredTraits){
allTakenTraitNames[key.toLowerCase()] = 1;
}
var traitWarnings = [];
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
var selectedLifepath = $scope.selectedLifepaths[i];
// Check the requirements for this lifepath to see if there are extra conditions.
var rexpr = burningData.lifepaths[$scope.stock][selectedLifepath.setting][selectedLifepath.name].requires_expr
if(rexpr){
var result = areLifepathRequirementsSatisfied($scope, rexpr);
for(var k = 0; k < result[1].length; k++){
var trait = result[1][k];
if( ! (trait in allTakenTraitNames) ){
traitWarnings.push("You must take the '"+trait+"' trait to satisfy the '"+selectedLifepath.name+"' lifepath requirements.");
}
}
}
}
$scope.lifepathTraitWarnings = traitWarnings;
}
function applyBonusesFromTraits($scope) {
var traitBonuses = new TraitBonuses();
//**** Calculate bonuses
for(var key in $scope.purchasedTraits){
var displayTrait = $scope.purchasedTraits[key];
traitBonuses.addTrait(key, displayTrait);
}
for(var key in $scope.requiredTraits){
var displayTrait = $scope.requiredTraits[key];
traitBonuses.addTrait(key, displayTrait);
}
for(var key in $scope.commonTraits){
var displayTrait = $scope.commonTraits[key];
traitBonuses.addTrait(key, displayTrait);
}
//**** Apply bonuses
//** Skills
for(var key in $scope.lifepathSkills) {
var displaySkill = $scope.lifepathSkills[key];
displaySkill.bonus = traitBonuses.getAddBonusesForSkill(key);
displaySkill.roundUp = traitBonuses.getRoundUpBonusForSkill(displaySkill);
}
for(var key in $scope.generalSkills) {
var displaySkill = $scope.generalSkills[key];
displaySkill.bonus = traitBonuses.getAddBonusesForSkill(key);
displaySkill.roundUp = traitBonuses.getRoundUpBonusForSkill(displaySkill);
}
//** Attributes
var attrNames = $scope.attributeNames();
for(var i = 0; i < attrNames.length; i++) {
var attrName = attrNames[i];
$scope.attributeBonuses[attrName] = traitBonuses.getAddBonusesForAttr(attrName);
}
//** Stats
for(var i = 0; i < $scope.stats.length; i++) {
var statName = $scope.stats[i].name;
$scope.stats[i].bonus = traitBonuses.getAddBonusesForStat(statName);
}
}
/*
Compute which traits the user can add as special traits. This value depends on character stock so
this function should be called when stock changes.
*/
function calculateSpecialTraitsForDisplay($scope, burningData){
var list = [];
for(var traitName in burningData.traits) {
var trait = burningData.traits[traitName];
if ('restrict' in trait){
if ( trait.restrict.indexOf(validStockToRestrictionStock(burningData.stocks, $scope.stock)) >= 0 &&
(trait.restrict.indexOf("special") >= 0 || trait.restrict.indexOf("character") >= 0) ){
list.push(new DisplayTrait(traitName, burningData.traits));
}
} else {
// No restriction! As long as cost > 0 (cost 0 is for traits with no cost; not purchaseable)
if ( trait.cost > 0 ) {
list.push(new DisplayTrait(traitName, burningData.traits));
}
}
}
if(list.length > 0 && list.indexOf($scope.currentSpecialTrait) < 0 ){
$scope.currentSpecialTrait = list[0];
}
$scope.specialTraitsForDisplay = list;
}
function calculateTotalResourcePoints($scope){
var totalResourcePoints = 0;
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
var displayLp = $scope.selectedLifepaths[i];
totalResourcePoints += displayLp.resourcePts;
}
$scope.totalResourcePoints = totalResourcePoints;
}
function calculateUnspentResourcePoints($scope){
var unspentResourcePoints = $scope.totalResourcePoints;
var reduce = function(vals){
for(var i = 0; i < vals.length; i++){
var display = vals[i];
unspentResourcePoints -= display.cost;
}
}
reduce(hashValues($scope.relationships));
reduce(hashValues($scope.gear));
reduce(hashValues($scope.property));
reduce(hashValues($scope.affiliations));
reduce(hashValues($scope.reputations));
$scope.unspentResourcePoints = unspentResourcePoints;
}
function restrictionStockToValidStock(stocks, stockAdjective){
return Object.values(stocks).findLast(s => s.adjective == stockAdjective).key;
}
function validStockToRestrictionStock(stocks, stockName){
return stocks[stockName].adjective;
}
function attributeModifyingQuestions($scope, attribute)
{
var result = [];
var ageMod = function(age){
return function(){
return ($scope.age > age ? 1 : 0);
}
}
if ( attribute == "Greed" )
{
var willMod = function(){
return ($scope.statsByName["Will"].exp() <= 4 ? 1 : 0);
}
var resMod = function(){
return Math.floor($scope.totalResourcePoints/60);
}
var twohundredMod = function(){
return ($scope.age > 200 ? 1 : 0);
}
var fourhundredMod = function(){
return ($scope.age > 400 ? 1 : 0);
}
var lpMod = function(){
var greedyLps = {"trader":1, "mask bearer":1, "master of arches":1, "master of forges":1, "master engraver":1, "treasurer":1, "quartermaster":1, "seneschal":1, "prince":1 }
var greed = 0;
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
if($scope.selectedLifepaths[i].name.toLowerCase() in greedyLps){
greed++;
}
}
return greed;
}
var relMod = function(){
var greed = 0;
var rels = hashValues($scope.relationships);
for(var i = 0; i < rels.length; i++){
if ( rels[i].isHateful ){
greed++;
if ( rels[i].isImmedFam )
greed++;
}
else if ( rels[i].isRomantic ) {
greed--;
}
}
return greed;
}
result.push(
{question: "+1 Greed if Will exponent is 4 or lower.", computed: true, compute: willMod},
{question: "+1 Greed for every 60 resource points.", computed: true, compute: resMod},
{question: "+1 Greed for each of the following lifepaths: Trader, Mask-Bearer, Master of Arches, Master of Forges, Master Engraver, Treasurer, Quartermaster, Seneschal or Prince.", computed: true, compute: lpMod},
{question: "Has your character coveted something owned by another?", math_label: "(+1 Greed)", modifier: 1},
{question: "Has your character ever stolen something he coveted?", math_label: "(+1 Greed)", modifier: 1},
{question: "Has your character ever had his prized treasure stolen from him?", math_label: "(+1 Greed)", modifier: 1},
{question: "Has your character ever been in the presence of the master craftsmanship of the Dwarven Fathers?", math_label: "(+1 Greed)", modifier: 1},
{question: "Has your character witnessed an outsider (Elf, Man, Orc, Roden, etc.) in possession of a work of Dwarven Art?", math_label: "(+1 Greed)", modifier: 1},
{question: "+1 Greed if the character is over 200 years old.", computed: true, compute: ageMod(200)},
{question: "+1 Greed if the character is over 400 years old.", computed: true, compute: ageMod(400)},
{question: "Each romantic relationship is -1 Greed. Each hateful relationship is +1 Greed. A hateful immediate family member is +2 Greed.", computed: true, compute: relMod}
);
}
else if ( attribute == "Health" )
{
var stockMod = function(){
return ($scope.stock == "orc" || $scope.stock == "dwarf" || $scope.stock == "elf" ? 1 : 0);
}
result.push(
{question: "Does the character live in squalor and filth?", math_label: "(-1 Health)", modifier: -1},
{question: "Is the character frail or sickly?", math_label: "(-1 Health)", modifier: -1},
{question: "Was the character severely wounded in the past?", math_label: "(-1 Health)", modifier: -1},
{question: "Has the character been tortured and enslaved?", math_label: "(-1 Health)", modifier: -1},
{question: "+1 Health if the character is a Dwarf, Elf, or Orc.", computed: true, compute: stockMod},
{question: "Is the character athletic and active?", math_label: "(+1 Health)", modifier: 1},
{question: "Does the character live in a really clean and happy place, like the hills in the Sound of Music?", math_label: "(+1 Health)", modifier: 1}
);
}
else if ( attribute == "Steel" )
{
var lpMod = function()
{
// "herald":1, "bannerman":1, "scout":1, "sergeant":1, "veteran":1, "cavalryman":1, "captain":1, "military order":1}
var steelyLps = {"conscript":1, "squire":1, "knight":1, "bandit":1, "pirate":1, "military order":1, "sword singer":1};
var steelySettings = {"professional soldier subsetting":1, "black legion subsetting":1, "dwarven host subsetting":1, "protector subsetting":1};
var steel = 0;
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
if($scope.selectedLifepaths[i].name.toLowerCase() in steelyLps || $scope.selectedLifepaths[i].setting.toLowerCase() in steelySettings){
steel = 1;
break;
}
}
return steel;
}
var woundMod = function()
{
if( lpMod() > 0 ){
return 1;
}
else
{
return -1;
}
}
var tortureMod = function()
{
var will = $scope.statsByName['Will'].exp();
if( will > 4 )
return 1;
else if ( will < 4 )
return -1;
else
return 0;
}
var traitMod = function()
{
return ($scope.hasTrait("Gifted") || $scope.hasTrait("Faithful") ? 1 : 0);
}
var percMod = function()
{
return ($scope.statsByName['Perception'].exp() >= 6 ? 1 : 0);
}
var willMod = function()
{
var will = $scope.statsByName['Will'].exp();
var mod = 0;
if( will > 5 )
mod++;
if( will > 7 )
mod++;
return mod;
}
var forteMod = function()
{
return ($scope.statsByName['Forte'].exp() >= 6 ? 1 : 0);
}
result.push(
{question: "+1 Steel if the character taken a conscript, soldier, bandit, squire or knight type lifepath.", computed: true, compute: lpMod},
{question: "Has the character ever been severely wounded?", math_label: "(+1 Steel if combat lifepath taken/-1 Steel if not)", computeModifier: true, compute: woundMod},
{question: "Has the character ever murdered or killed with his own hand more than once?", math_label: "(+1 Steel)", modifier: 1},
{question: "Has the character been tortured, enslaved or beaten terribly over time?", math_label: "(+1 Steel if Will is > 4, -1 Steel if Will < 4, +0 if Will is 4)", computeModifier: true, compute: tortureMod},
{question: "Has the character lead a sheltered life, free of violence and pain?", math_label: "(-1 Steel)", modifier: -1},
{question: "Has the character been raised in a competitive (but non-violent) culture - sports, debate, strategy games, courting?", math_label: "(+1 Steel)", modifier: 1},
{question: "Has the character given birth to a child?", math_label: "(+1 Steel)", modifier: 1},
{question: "+1 Steel if the character Gifted, Faithful or an equivalent.", computed: true, compute: traitMod},
{question: "+1 Steel if the character's Perception exponent is 6 or higher.", computed: true, compute: percMod},
{question: "+1 Steel if the character's Will exponent is 5 or higher; an additional +1 if it's 7 or higher.", computed: true, compute: willMod},
{question: "+1 Steel if the character's Forte exponent is 6 or higher.", computed: true, compute: forteMod}
);
}
else if ( attribute == "Health" )
{
var stockMod = function(){
return ($scope.stock == "orc" || $scope.stock == "dwarf" || $scope.stock == "elf" ? 1 : 0);
}
result.push(
{question: "Does the character live in squalor and filth?", math_label: "(-1 Health)", modifier: -1},
{question: "Is the character frail or sickly?", math_label: "(-1 Health)", modifier: -1},
{question: "Was the character severely wounded in the past?", math_label: "(-1 Health)", modifier: -1},
{question: "Has the character been tortured and enslaved?", math_label: "(-1 Health)", modifier: -1},
{question: "+1 Health if the character is a Dwarf, Elf, or Orc.", computed: true, compute: stockMod},
{question: "Is the character athletic and active?", math_label: "(+1 Health)", modifier: 1},
{question: "Does the character live in a really clean and happy place, like the hills in the Sound of Music?", math_label: "(+1 Health)", modifier: 1}
);
}
else if ( attribute == "Grief" )
{
var protectMod = function(){
var grief = 0;
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
if($scope.selectedLifepaths[i].setting.toLowerCase() == "protector subsetting"){
grief = 1;
break;
}
}
return grief;
}
var lpMod1 = function(){
var lps = {"lancer":1, "lieutenant":1, "captain":1};
var lps2 = {"lord protector":1, "soother":1};
var grief1 = 0;
var grief2 = 0;
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
if($scope.selectedLifepaths[i].name.toLowerCase() in lps)
{
grief1 = 1;
if( grief2 > 0)
break;
}
if($scope.selectedLifepaths[i].name.toLowerCase() in lps2)
{
grief2 = 1;
if( grief1 > 0)
break;
}
}
return grief1 + grief2;
}
var etharchMod = function(){
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
if($scope.selectedLifepaths[i].name.toLowerCase() == "born etharch"){
return 1;
}
}
return 0;
}
var lpMod2 = function(){
var lps = {"loremaster":1, "adjutant":1, "althing":1};
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
if($scope.selectedLifepaths[i].name.toLowerCase() in lps)
return 1;
}
return 0;
}
var elderMod = function(){
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
if($scope.selectedLifepaths[i].name.toLowerCase() == "elder"){
return 1;
}
}
return 0;
}
var lamentMod = function(){
var skills = $scope.allSelectedSkills();
for(key in skills)
{
var skill = skills[key];
// Dark Elves have no laments, therefore we check if the skill is a lament BEFORE we calculate
// the exponent because we don't want to it to get sent into an infinite loop, i.e.
// calculate spite -> calc grief -> calc spite skill exponent -> spite -> grief -> ...
if( beginsWith(key.toLowerCase(), "lament") && skill.exp($scope.statsForSkillCalc) > 0 )
return 0;
}
return 1;
}
var steelMod = function(){
var steel = $scope.attribute('Steel').exp;
var mod = 0;
if ( steel > 5 )
mod = steel - 5;
return mod;
}
var percMod = function(){
return ($scope.statsByName['Perception'].exp() > 5 ? 1 : 0);
}
result.push(
{question: "+1 Grief if the character has taken any Protector lifepath.", computed: true, compute: protectMod},
{question: "+1 Grief if the character has been a Lancer, Lieutenant or Captain; Additional +1 if the character has been a Lord Protector or Soother", computed: true, compute: lpMod1},
{question: "+1 Grief if the character was Born Etharch", computed: true, compute: etharchMod},
{question: "+1 Grief if the character has been a Loremaster, Adjutant or Althing", computed: true, compute: lpMod2},
{question: "+1 Grief if the character has taken the Elder lifepath", computed: true, compute: elderMod},
{question: "+1 Grief if the character doesn't know any Lamentations", computed: true, compute: lamentMod},
{question: "Does the character's history include tragedy?", math_label: "(+1 Grief)", modifier: 1},
{question: "Has the character lived among non-Elven people?", math_label: "(+1 Grief)", modifier: 1},
{question: "+1 Grief for each point of Steel above 5.", computed: true, compute: steelMod},
{question: "+1 Grief if the characters Perception is above 5.", computed: true, compute: percMod},
{question: "+1 Grief if the character is over 500 years old.", computed: true, compute: ageMod(500)},
{question: "+1 Grief if the character is over 750 years old.", computed: true, compute: ageMod(750)},
{question: "+1 Grief if the character is over 1000 years old.", computed: true, compute: ageMod(1000)}
);
}
else if ( attribute == "Spite" )
{
var griefMod = function() {
var grief = $scope.attribute("Grief").exp
return grief;
}
var traitsMod = function(){
var val = 0;
if($scope.hasTrait('Slayer'))
val++;
if($scope.hasTrait('Exile'))
val++;
if($scope.hasTrait('Feral'))
val++;
if($scope.hasTrait('Murderous'))
val++;
if($scope.hasTrait('Saturnine'))
val++;
if($scope.hasTrait('Femme Fatale/Homme Fatale'))
val++;
if($scope.hasTrait('Cold'))
val++;
if($scope.hasTrait('Bitter'))
val++;
return val;
}
var bitterMod = function() {
var darkElfGear = [
'Bitter Poison',
'Spiteful Poison',
'Lock Picks',
'Long Knife',
'Barbed Javelins',
'Garrote',
'Caltrops',
'Tools Of The Trade',
'Cloak Of Darkness',
'Climbing Claws',
'Remote Refuge',
'Morlin Armor',
'Morlin Weapons'
]
var bitterRps = 0;
var isBitter = function(gear) {
for (var i = 0; i < darkElfGear.length; i++) {
if (gear.desc.startsWith(darkElfGear[i])) {
return false;
}
}
return true;
}
for (var k in $scope.gear) {
var gear = $scope.gear[k];
if (isBitter(gear)) {
bitterRps += gear.cost;
}
}
for (var k in $scope.property) {
var prop = $scope.property[k];
if (isBitter(prop)) {
bitterRps += prop.cost;
}
}
return Math.floor(bitterRps/10);
}
result.push(
{question: "+1 Spite for every point of Grief", computed: true, compute: griefMod},
{question: "+1 Spite for each of several spiteful traits", computed: true, compute: traitsMod},
{question: "+1 Spite for every 10 rps spent on Elven resources", computed: true, compute: bitterMod},
{question: "Has the character been betrayed by their friends?", math_label: "(+1 Spite)", modifier: 1},
{question: "Is the character lovesick or broken hearted?", math_label: "(+1 Spite)", modifier: 1},
{question: "Has the character been abandoned by those they held dear?", math_label: "(+1 Spite)", modifier: 1},
{question: "Has the character been abused or tortured?", math_label: "(+1 Spite)", modifier: 1},
{question: "Does the character still respect or admire someone on the other side?", math_label: "(-1 Spite)", modifier: -1},
{question: "Does the character still love someone on the other side?", math_label: "(-2 Spite)", modifier: -2}
);
}
else if ( attribute == "Hatred" )
{
var brutalMod = function(){
var count = 0;
for(var i = 0; i < $scope.selectedLifepaths.length; i++){
if ( $scope.selectedLifepaths[i].brutalLifeTraitName ){
count++;
}
}
return count;
}
var willMod = function()
{
var will = $scope.statsByName['Will'].exp();
return (will <= 2 ? 1 : 0);
}
var steelMod = function(){
var steel = $scope.attribute('Steel').exp;
return (steel >= 5 ? 1 : 0);
}
var percMod = function(){
return ($scope.statsByName['Perception'].exp() >= 6 ? 1 : 0);
}
var traitsMod = function(){
var val = 0;
if($scope.hasTrait('Kicking The Beast'))
val++;
if($scope.hasTrait('Yowling'))
val++;
if($scope.hasTrait("Where There's A Whip, There's A Way"))
val++;
if($scope.hasTrait("Charging Blindly"))
val++;
if($scope.hasTrait("Cry Of Doom"))
val++;
if($scope.hasTrait("Unrelenting Savagery"))
val++;
if($scope.hasTrait("Humility"))
val++;
if($scope.hasTrait("Life Is Death"))
val++;
if($scope.hasTrait("Pain Life"))
val++;
if($scope.hasTrait("Intense Hatred"))
val++;
if($scope.hasTrait("Silent Hatred"))
val++;
if($scope.hasTrait("Savage Consequences"))
val++;
if($scope.hasTrait("Unrelenting Hatred"))
val++;
if($scope.hasTrait("Naked Hatred"))
val++;
return val;
}
result.push(
{question: "Was the character ever horribly wounded?", math_label: "(+1 Hatred)", modifier: 1},
{question: "+1 Hatred for each 1 rolled on the Brutal Life table.", computed: true, compute: brutalMod},
{question: "Has the character been tortured?", math_label: "(+1 Hatred)", modifier: 1},
{question: "Has the character ever been a slave to another?", math_label: "(+1 Hatred)", modifier: 1},
{question: "Has the character ever killed his superior or parents?", math_label: "(+1 Hatred)", modifier: 1},
{question: "Has the character ever attempted to command a unit of goblins in battle?", math_label: "(+1 Hatred)", modifier: 1},
{question: "+1 Hatred if the character's Will is 2 or lower", computed: true, compute: willMod},
{question: "+1 Hatred if the character's Steel is 5 or higher", computed: true, compute: steelMod},
{question: "+1 Hatred if the character's Perception is 6 or higher", computed: true, compute: percMod},
{question: "+1 Hatred for each of several hatred-related traits", computed: true, compute: traitsMod}
);
}
else if ( attribute == "Faith" )
{
result.push(
{question: "Is God who you trust the most?", math_label: "(+1 Faith)", modifier: 1},
{question: "When in danger, do you consult God for aid?", math_label: "(+1 Faith)", modifier: 1},
{question: "Is it only through God that you best serve your allies?", math_label: "(+1 Faith)", modifier: 1}
);
}
else if ( attribute == "Ancestral Taint" )
{
var skills = $scope.allSelectedSkills();
var primalBarkMod = function() {
return ("Primal Bark" in skills) ? 1 : 0;
}
var ancestralJawMod = function() {
return ("Ancestral Jaw" in skills) ? 1 : 0;
}
var grandfathersSongMod = function() {
return ("Grandfather's Song" in skills) ? 1 : 0;
}
var stinkOfAncientsMod = function() {
return ($scope.hasTrait("Stink Of The Ancient") ? 1 : 0);
}
var spiritNoseMod = function() {
return ($scope.hasTrait("Spirit Nose") ? 1 : 0);
}
result.push(
{question: "+1 Ancestral Taint if the character has the Primal Bark skill", computed: true, compute: primalBarkMod},
{question: "+1 Ancestral Taint if the character has the Ancestral Jaw skill", computed: true, compute: ancestralJawMod},
{question: "+1 Ancestral Taint if the character has the Grandfather's Song skill", computed: true, compute: grandfathersSongMod},
{question: "+1 Ancestral Taint if the character has the Stink Of The Ancients trait", computed: true, compute: stinkOfAncientsMod},
{question: "+1 Ancestral Taint if the character has the Spirit Nose trait", computed: true, compute: spiritNoseMod}
);
}
else {
//console.log("Error: attribute "+attribute+" doesn't have any modifying questions");
return null;
}
return result;
}
function convertAttributeModifierQuestionResultsForSave($scope){
// Save only the non-computed questions that were answered.
var result = {};
for (key in $scope.attributeModifierQuestionResults){
var list = [];
var questions = $scope.attributeModifierQuestionResults[key];
for(var i = 0; i < questions.length; i++){
var q = questions[i];
if ( ("answer" in q) && (!q.computed)){
list.push({question: q.question, answer: q.answer});
}
}
result[key] = list;
}
return result;
}
function convertAttributeModifierQuestionResultsForCharsheet($scope){
// Save only the non-computed questions, and generate the questions for all attributes.
var result = {};
var questions = $scope.attributeModifierQuestionResults;
// Generate the full set of questions for each attribute the user actually has.
var attributeNames = $scope.attributeNames();
for(var j = 0; j < attributeNames.length; j++){
var attribute = attributeNames[j];
var fullQuestions = attributeModifyingQuestions($scope, attribute)
if(! fullQuestions){
// Not an attribute with questions
continue;
}
var answerHash = {};
var answers = questions[attribute];
if(answers){
for(var i = 0; i < answers.length; i++){
answerHash[answers[i].question] = answers[i].answer;
}
}
var resultQuestions = [];
for(var i = 0; i < fullQuestions.length; i++){
var q = fullQuestions[i];
if ( q.computed )
continue;
if(q.question in answerHash){
q.answer = answerHash[q.question];
}
resultQuestions.push(q);
}
result[attribute] = resultQuestions;
}
return result;
}
function loadAttributeModifierQuestionResultsFromSave($scope, questions)
{
var result = {};
// For each attribute for which questions were saved, generate the
// full set of questions, then add in the answers from the save.
// The reason we do this rather than save all questions and answers
// is 1) to save space, and 2) because we can't save the compute function
// to the server, so it needs to be added on load anyhow.
for(attribute in questions){
var fullQuestions = attributeModifyingQuestions($scope, attribute)
var answers = questions[attribute];
var answerHash = {};
for(var i = 0; i < answers.length; i++){
answerHash[answers[i].question] = answers[i].answer;
}
for(var i = 0; i < fullQuestions.length; i++){
var q = fullQuestions[i];
if(q.question in answerHash){
q.answer = answerHash[q.question];
}
}
result[attribute] = fullQuestions;
}
return result;
}
/**
A number of skills are not defined because they are a specific instance of a general skill; for example
ancient history is a type of history, and has the same roots. This function returns the parent skill for
a specific skill.
*/
function getGeneralSkillNameFor(skillName){
if ( endsWith(skillName, "History") )
return "History";
else if( endsWith(skillName, "Doctrine"))
return "Doctrine";
else if( endsWith(skillName, "Ritual"))
return "Ritual";
else if( skillName == "Flute" || skillName == "Drum" || skillName == "Lyre" )
return "Musical Instrument";
else if( endsWith(skillName, "Husbandry") )
return "Animal Husbandry";
else
return skillName;
}
// Given n stat names, average the stats taking into account shade. Return the resulting [shade, exp] tuple.
var computeStatAverage = function(statsByName, statNames, roundUp){
var sum = 0;
var shade = 'B';
var allGray = true;
var stats = [];
var getShade = function(stat){
var s = stat.shade;
if(! s){
s = stat.calcshade();
}
return s;
}
if(statNames.length == 1){
var stat = statsByName[statNames[0]];
return [getShade(stat), stat.exp()];
}
// Convert names to actual DisplayStat structs.
for(var i = 0; i < statNames.length; i++){
stats[i] = statsByName[statNames[i]];
}
for(var i = 0; i < stats.length; i++){
if(getShade(stats[i]) == 'B'){
allGray = false;
break;
}
}
if(allGray)
shade = 'G';
for(var i = 0; i < stats.length; i++){
if(getShade(stats[i]) == 'G' && !allGray){
sum += 2;
}
sum += stats[i].exp();
}
var exp = Math.floor( sum / stats.length );
if ( roundUp ) {
exp = Math.ceil( sum / stats.length );
}
return [shade, exp];
}
function calculateItemsFromList(resources, type){
var rc = [];
for(var i = 0; i < resources.length; i++){
var resource = resources[i];
if(! resource.type || resource.type == type){
if(resource.resources){
rc.push({ "displayName" : resource.name + "...", "name": resource.name, "resources": calculateItemsFromList(resource.resources) });
}
else
{
rc.push({ "displayName" : resource.name + " (" + resource.rp + " rps)", "name": resource.name, "cost" : resource.rp} );
}
}
}
return rc;
}
function calculateHierarchyListForSelect($scope, burningData, type) {
var rc = [];
var resources = burningData.resources[$scope.stock];
if(! resources )
{
resources = [];
}
return calculateItemsFromList(resources, type);
}
function calculateGearSelectionLists($scope, burningData) {
$scope.currentSelectListGear = [];
$scope.gearListForSelect = [];
for(var i = 0; i < 3; i++)
$scope.currentSelectListGear.push({});
$scope.gearListForSelect[0] = calculateHierarchyListForSelect($scope, burningData, 'gear');
if($scope.gearListForSelect[0].length > 0)
$scope.currentSelectListGear[0] = $scope.gearListForSelect[0][0];
$scope.calculateHierarchyListForSelectN($scope.gearListForSelect, $scope.currentSelectListGear, 1);
}
function calculatePropertySelectionLists($scope, burningData) {
$scope.currentSelectListProperty = [];
$scope.propertyListForSelect = [];
for(var i = 0; i < 3; i++)
$scope.currentSelectListProperty.push({});
$scope.propertyListForSelect[0] = calculateHierarchyListForSelect($scope, burningData, 'property');
if($scope.propertyListForSelect[0].length > 0)
$scope.currentSelectListProperty[0] = $scope.propertyListForSelect[0][0];
$scope.calculateHierarchyListForSelectN($scope.propertyListForSelect, $scope.currentSelectListProperty, 1);
}
function populateUiFieldsFromDisplayResource($scope, type, resource) {
if(type == 'relationship'){
$scope.currentRelationshipDesc = resource.desc;
$scope.currentRelationshipImportance = resource.importance;
$scope.currentRelationshipIsImmedFam = resource.isImmedFam;
$scope.currentRelationshipIsOtherFam = resource.isOtherFam;
$scope.currentRelationshipIsRomantic = resource.isRomantic;
$scope.currentRelationshipIsForbidden = resource.isForbidden;
$scope.currentRelationshipIsHateful = resource.isHateful;
}
else if (type == 'gear'){
$scope.currentGearCost = resource.cost;
$scope.currentGearDesc = resource.desc;
}
else if (type == 'property'){
$scope.currentPropertyCost = resource.cost;
$scope.currentPropertyDesc = resource.desc;
}
else if (type == 'affiliation'){
$scope.currentAffiliationDesc = resource.desc;
$scope.currentAffiliationImportance = resource.importance;
}
else if (type == 'reputation'){
$scope.currentReputationDesc = resource.desc;
$scope.currentReputationImportance = resource.importance;
}
}