mirror of
https://github.com/RussellChamp/GrimoireJS.git
synced 2026-10-27 22:42:32 +00:00
1407 lines
58 KiB
JavaScript
1407 lines
58 KiB
JavaScript
//GrimoireJS v0.2
|
|
//by Russell Champoux
|
|
//Grimoire magic item generator for the Pathfinder roleplaying game
|
|
/*global $:false, _:false, Chance:false, console:true */
|
|
|
|
'use strict';
|
|
|
|
var TYPES = {
|
|
ARMOR_AND_SHIELDS: 'Armor and Shields',
|
|
POTIONS: 'Potions',
|
|
RINGS: 'Rings',
|
|
RODS: 'Rods',
|
|
SCROLLS: 'Scrolls',
|
|
STAVES: 'Staves',
|
|
WANDS: 'Wands',
|
|
WEAPONS: 'Weapons',
|
|
WONDROUS_ITEMS: 'Wondrous Items',
|
|
SPELLS: 'Spells'
|
|
};
|
|
var SPELL_TYPES = {
|
|
ARCANE: 'Arcane',
|
|
DIVINE: 'Divine'
|
|
};
|
|
var ENERGY_TYPES = {
|
|
ACID: 'Acid',
|
|
COLD: 'Cold',
|
|
ELEC: 'Electricity',
|
|
FIRE: 'Fire',
|
|
SONIC: 'Sonic'
|
|
};
|
|
var QUALITIES = {
|
|
MINOR:'minor',
|
|
MEDIUM: 'medium',
|
|
MAJOR: 'major'
|
|
};
|
|
var WEIGHTS = {};
|
|
WEIGHTS[QUALITIES.MINOR] =
|
|
{
|
|
ARMOR_AND_SHIELDS: 4,
|
|
WEAPONS: 5,
|
|
POTIONS: 35,
|
|
RINGS: 2,
|
|
RODS: 0,
|
|
SCROLLS: 35,
|
|
STAVES: 0,
|
|
WANDS: 10,
|
|
WONDROUS_ITEMS: 9,
|
|
};
|
|
WEIGHTS[QUALITIES.MEDIUM] =
|
|
{
|
|
ARMOR_AND_SHIELDS: 10,
|
|
WEAPONS: 10,
|
|
POTIONS: 10,
|
|
RINGS: 10,
|
|
RODS: 10,
|
|
SCROLLS: 15,
|
|
STAVES: 3,
|
|
WANDS: 15,
|
|
WONDROUS_ITEMS: 17,
|
|
};
|
|
WEIGHTS[QUALITIES.MAJOR] = {
|
|
ARMOR_AND_SHIELDS: 10,
|
|
WEAPONS: 10,
|
|
POTIONS: 5,
|
|
RINGS: 10,
|
|
RODS: 10,
|
|
SCROLLS: 10,
|
|
STAVES: 20,
|
|
WANDS: 5,
|
|
WONDROUS_ITEMS: 20,
|
|
};
|
|
|
|
|
|
//Contains all the source info
|
|
var Grimoire = function(config) { // jshint ignore:line
|
|
var self = this;
|
|
config = config || {};
|
|
this.chance = (config && config.chance ? config.chance : config && config.seed ? new Chance(config.seed) : new Chance());
|
|
|
|
this.SOURCES = [];
|
|
$.ajaxSetup({
|
|
async: (config.async !== undefined) ? config.async : true
|
|
//A project that works now is better than one that doesn't exist.
|
|
//I MUST come back and revisit this with promises later
|
|
});
|
|
var baseDir = config.baseDir ? config.baseDir : './sources/';
|
|
$.getJSON(baseDir + 'sources.json', function(sources) {
|
|
sources.sources.forEach(function(url) {
|
|
$.getJSON(url, function(source) {
|
|
console.log('loaded '+ url);
|
|
source.data = {};
|
|
//construct the data containers we will insert later data into
|
|
source.types.forEach(function(type) {
|
|
source.data[type.name] = {};
|
|
source.data[type.name].enabled = true;
|
|
if(type.source) {
|
|
$.getJSON(type.source, function(typeData) {
|
|
source.data[type.name].data = typeData;
|
|
console.log('loaded ' + type.source);
|
|
})
|
|
.fail(function(d, textStatus, error) {
|
|
console.error('getJSON failed to load '+type.source+ ', status: ' + textStatus + ', error: '+error);
|
|
});
|
|
}
|
|
});
|
|
self.SOURCES.push(source);
|
|
})
|
|
.fail(function(d, textStatus, error) {
|
|
console.error('getJSON failed to load '+url+ ', status: ' + textStatus + ', error: '+error);
|
|
});
|
|
});
|
|
sources.other.forEach(function(url) {
|
|
$.getJSON(url, function(data) {
|
|
var dataName = url.replace(/.*\/(.*)\.json/, '$1');
|
|
self[dataName] = data;
|
|
console.log('loaded ' + url + ' as ' + dataName);
|
|
})
|
|
.fail(function(d, textStatus, error) {
|
|
console.error('getJSON failed to load '+url+', status: '+textStatus+', error: '+error);
|
|
});
|
|
});
|
|
//LOADED ALL SOURCES AND DATA
|
|
})
|
|
.fail(function(err) {
|
|
console.error('Failed to load sources list:', err);
|
|
});
|
|
$.ajaxSetup({
|
|
async: true
|
|
});
|
|
|
|
//options.sources manually select which sources you want to use
|
|
// will not use sources that are marked as disabled
|
|
//options.types manually specify what types you want to get
|
|
this.getItems = function(quality, count, options) {
|
|
options = options ? _.clone(options) : {};
|
|
var sources = options.sources ? options.sources : self.SOURCES;
|
|
var types = options.types ? options.types : _.values(TYPES);
|
|
|
|
//Error checking block
|
|
if(!sources || sources.length === 0) { console.error('No sources given!'); return items; }
|
|
if(!types || types == 'any' || types.length === 0) {
|
|
types = _.values(TYPES); //set to all types
|
|
}
|
|
for(var i in types) {
|
|
if(_.values(TYPES).indexOf(types[i]) == -1) { console.error('Invalid type ' + types[i]); return items; }
|
|
}
|
|
if(_.values(QUALITIES).indexOf(quality) == -1) { console.error('Invalid quality ' + quality); return items; }
|
|
if(!count || count < 1) { count = 1; }
|
|
|
|
|
|
//initialize the chance engine, using a seed if provided (for testing purposes)
|
|
//we do this now instead of reinitializing the Chance engine each time we generate an item
|
|
self.chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
|
|
var items = [];
|
|
|
|
_.times(count, function() {
|
|
var item = self.getItem(quality, options);
|
|
if(item) {
|
|
items.push(item);
|
|
//console.log(item);
|
|
}
|
|
});
|
|
return items;
|
|
};
|
|
|
|
var selectSource = function(sources, quality, type, options) {
|
|
options = options ? options : {};
|
|
//this is a concern when "getPotions/getWands" calls "getSpell" which calls "selectSource"
|
|
type = options.typeOverride ? options.typeOverride : type;
|
|
quality = quality ? quality : options.quality;
|
|
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
|
|
var source = {};
|
|
var sourceWeights = {};
|
|
|
|
//***UNIQUES FOR WEAPONS AND ARMORS***
|
|
//************************************
|
|
if(type == 'Uniques') {
|
|
if(!options.itemType || !options.uniqueType) {
|
|
console.error('itemType or uniqueType not provided', options);
|
|
}
|
|
sourceWeights = _.map(sources, function(source) {
|
|
var items = source.data[options.itemType];
|
|
if(items && items.enabled && items.data[options.uniqueType]) {
|
|
return _.filter(items.data[options.uniqueType],
|
|
function(special) {
|
|
return special.weight[quality] > 0;
|
|
}).length; //return a count of all specials that have a quality weight above 0
|
|
}
|
|
else {
|
|
return 0; //either itemType not found or not enabled or uniqueType not found
|
|
}
|
|
});
|
|
|
|
}
|
|
//***SPECIALS FOR WEAPONS AND ARMORS***
|
|
//*************************************
|
|
else if(type == 'Specials') {
|
|
if(!options.itemType || !options.specialType) {
|
|
console.error('itemType or specialType not provided', options);
|
|
}
|
|
sourceWeights = _.map(sources, function(source) {
|
|
if(source.data[options.itemType] && source.data[options.itemType].enabled &&
|
|
source.data[options.itemType].data[options.specialType]) {
|
|
return _.filter(source.data[options.itemType].data[options.specialType],
|
|
function(special) {
|
|
return special.weight[quality] > 0;
|
|
}).length; //return a count of all specials that have a quality weight above 0
|
|
}
|
|
else {
|
|
return 0; //either itemType not found or not enabled or specialType not found
|
|
}
|
|
});
|
|
}
|
|
//***SPELLS AND SCROLLS AND WANDS***
|
|
//**********************************
|
|
else if(type == TYPES.SCROLLS || type == TYPES.SPELLS || type == TYPES.WANDS) {
|
|
if(!options.spellType || options.spellLevel > 9 || options.spellLevel < 0) {
|
|
console.error('Error selecting source for Spells. Invalid options', type, options);
|
|
}
|
|
sourceWeights = _.map(sources, function(source) {
|
|
//Check that we have a spells section
|
|
//AND that the specific item type we are looking for is enabled
|
|
if(source.data[TYPES.SPELLS] && source.data[type] && source.data[type].enabled) {
|
|
if(source.data[TYPES.SPELLS].data[options.spellType]) {
|
|
return source.data[TYPES.SPELLS].data[options.spellType][options.spellLevel].length;
|
|
}
|
|
else { //source did not have my desired spell type
|
|
return 0;
|
|
}
|
|
}
|
|
else { //source has no spells
|
|
return 0;
|
|
}
|
|
|
|
});
|
|
}
|
|
//***POTIONS***
|
|
//*************
|
|
else if(type == TYPES.POTIONS) {
|
|
sourceWeights = _.map(sources,
|
|
function(source) {
|
|
if(source.data[type] && source.data[type].enabled && source.data[type].data[options.potionLevel]) {
|
|
return source.data[type].data[options.potionLevel].length;
|
|
}
|
|
else {
|
|
return 0; //either no data, disabled, or not items of that quality
|
|
}
|
|
});
|
|
}
|
|
//***WONDROUS ITEMS***
|
|
//********************
|
|
else if(type == TYPES.WONDROUS_ITEMS) {
|
|
sourceWeights = _.map(sources,
|
|
function(source) {
|
|
if(source.data[type] && source.data[type].enabled && source.data[type].data[quality]) {
|
|
return source.data[type].data[quality].length;
|
|
}
|
|
else {
|
|
return 0; //either no data, disabled, or not items of that quality
|
|
}
|
|
});
|
|
}
|
|
//***EVERYTHING ELSE***
|
|
//*********************
|
|
else {
|
|
if((type == TYPES.RODS || type == TYPES.STAVES) && quality == QUALITIES.MINOR) {
|
|
console.error('Error! There are no ' + QUALITIES.MINOR + ' ' + type);
|
|
return;
|
|
}
|
|
//Determine which source to use
|
|
//get a count from each source of what we're looking for
|
|
sourceWeights = _.map(sources,
|
|
function(source) {
|
|
//only choose from sources that HAVE that type of item
|
|
//AND are enabled
|
|
//AND have at least one item of that quality with a probability weight
|
|
if(source.data[type] && source.data[type].enabled) {
|
|
return _.filter(source.data[type].data,
|
|
function(item) {
|
|
return item.weight[quality] > 0;
|
|
}).length;
|
|
}
|
|
else {
|
|
return 0;
|
|
}
|
|
});
|
|
}
|
|
if(_.sum(sourceWeights) <= 0) {
|
|
console.error('Unable to find valid ', quality, type, options.itemType, options.uniqueType, ' within sources!', sources);
|
|
return;
|
|
}
|
|
//chance.weighted will DESTRUCTIVELY modify paramaters that have a weight of zero
|
|
//in this case, that means sources that do not contain the selected type
|
|
//or sources that have types we want but with no weight
|
|
//because of that, I create a temp array that I don't care if chance mangles
|
|
source = sources[chance.weighted(_.range(sources.length), sourceWeights)];
|
|
return source;
|
|
};
|
|
|
|
|
|
this.getItem = function(quality, options) {
|
|
options = options ? options : {};
|
|
var sources = options.sources ? options.sources : self.SOURCES;
|
|
var types = options.types ? options.types : _.values(TYPES);
|
|
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
|
|
var itemType = '';
|
|
if(types.length == 1) {
|
|
itemType = types[0];
|
|
}
|
|
else {
|
|
//Create a weighted-average map based on the desired types
|
|
var localWeights = _.clone(WEIGHTS[quality]); //copy the default weighted-average for this quality
|
|
for(var key in localWeights) {
|
|
if(_.values(types).indexOf(TYPES[key]) == -1) { //remove any items that were not selected
|
|
delete localWeights[key];
|
|
}
|
|
}
|
|
//Determine the type of item to generate
|
|
itemType = TYPES[chance.weighted(_.keys(localWeights), _.values(localWeights))];
|
|
}
|
|
|
|
var item = {};
|
|
var source = {};
|
|
|
|
//GENERATE ITEM
|
|
//SCROLLS
|
|
if(itemType == TYPES.SCROLLS) {
|
|
item = self.getScroll(quality, options);
|
|
}
|
|
//POTIONS
|
|
else if(itemType == TYPES.POTIONS) {
|
|
item = self.getPotion(quality, options);
|
|
}
|
|
//WANDS
|
|
else if(itemType == TYPES.WANDS) {
|
|
item = self.getWand(quality, options);
|
|
}
|
|
//WONDROUS ITEMS
|
|
else if(itemType == TYPES.WONDROUS_ITEMS) {
|
|
item = self.getWondrousItem(quality, options);
|
|
}
|
|
//WEAPONS
|
|
else if(itemType == TYPES.WEAPONS) {
|
|
item = self.getWeapon(quality, options);
|
|
}
|
|
//ARMOR AND SHIELDS
|
|
else if(itemType == TYPES.ARMOR_AND_SHIELDS) {
|
|
item = self.getArmorOrShield(quality, options);
|
|
}
|
|
//RINGS
|
|
else if(itemType == TYPES.RINGS) {
|
|
item = self.getRing(quality, options);
|
|
}
|
|
//RODS
|
|
else if(itemType == TYPES.RODS) {
|
|
item = self.getRod(quality, options);
|
|
}
|
|
//STAVES
|
|
else if(itemType == TYPES.STAVES) {
|
|
item = self.getStaff(quality, options);
|
|
}
|
|
//EVERYTHING ELSE NOT LISTED.
|
|
//All TYPES should have their own 'get' method but this allows for additional types to be added
|
|
// as long as they follow the data structure standard of source.data.type and contain weights
|
|
else {
|
|
//Select the item source based on the quality and item type
|
|
source = selectSource(sources, quality, itemType, options);
|
|
|
|
item = self.getWeightedItem(source.data[itemType].data, quality);
|
|
item.source = source.shortName;
|
|
item.itemType = itemType;
|
|
}
|
|
|
|
if(!options.disableClue && chance.d100() <= (options.clueChance || 20)) {
|
|
item.description = 'with a clue to it\'s purpose! ' + (item.description ? item.description : '');
|
|
}
|
|
item.quality = quality;
|
|
return item;
|
|
};
|
|
|
|
//options.maxRoll - override the maximul value that can be rolled for these items
|
|
// default is the sum of the quality weights
|
|
this.getWeightedItem = function(itemData, quality, options) {
|
|
options = options ? options : {};
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
|
|
var maxRoll = options.maxRoll || _.sum(_.map(itemData, function(item) {
|
|
return item.weight[quality]; }));
|
|
//console.log('itemData', itemData, 'quality', quality, 'maxRoll', maxRoll);
|
|
var roll = chance.integer({min: 1, max: maxRoll});
|
|
var item = {};
|
|
|
|
for(var i in itemData) { //check all 'itemType' items in the source
|
|
if(roll <= itemData[i].weight[quality]) { //if we roll under their weighted probability
|
|
item = _.clone(itemData[i]);
|
|
delete item.weight;
|
|
return item;
|
|
}
|
|
else { //decrease the roll value by the current item's weighted probability
|
|
roll -= itemData[i].weight[quality];
|
|
}
|
|
}
|
|
console.error('Could not find valid '+quality+'item in',itemData);
|
|
};
|
|
|
|
//options.type can be 'Arcane' or 'Divine'
|
|
this.getSpells = function(level, count, options) {
|
|
options = options ? options : {};
|
|
|
|
var spells = [];
|
|
self.chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
|
|
_.times(count, function() {
|
|
spells.push(self.getSpell(level, options));
|
|
});
|
|
|
|
return spells;
|
|
};
|
|
|
|
//options.type can be 'Arcane' or 'Divine'
|
|
this.getSpell = function(level, options) {
|
|
options = options ? options : {};
|
|
var sources = options.sources ? options.sources : self.SOURCES;
|
|
var type = options.type;
|
|
|
|
//initialize the chance engine, using a seed if provided (for testing purposes)
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
|
|
var spellType = type ? type : chance.pick(_.values(SPELL_TYPES));
|
|
options.spellType = spellType;
|
|
options.spellLevel = level;
|
|
var source = selectSource(sources, options.quality, TYPES.SPELLS, options);
|
|
//console.log('Generating a level ' + level + ' ' + spellType + ' spell from ' + source.shortName);
|
|
|
|
var spell = {
|
|
name: chance.pick(source.data[TYPES.SPELLS].data[spellType][level]),
|
|
level: level,
|
|
type: spellType,
|
|
source: source.shortName
|
|
};
|
|
|
|
return spell;
|
|
};
|
|
|
|
//options.level manually override the scroll spell level
|
|
this.getScroll = function(quality, options) {
|
|
options = options ? options : {};
|
|
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
|
|
var cost = [12.5, 25, 150, 375, 700, 1125, 1650, 2275, 3000, 3825];
|
|
var spellLevel = options.scrollLevel;
|
|
|
|
if(!spellLevel || spellLevel < 0 || spellLevel > 9) {
|
|
var weights = {};
|
|
weights[QUALITIES.MINOR] = [5, 45, 45, 5, 0, 0, 0, 0, 0, 0];
|
|
weights[QUALITIES.MEDIUM] = [0, 0, 5, 60, 30, 5, 0, 0, 0, 0];
|
|
weights[QUALITIES.MAJOR] = [0, 0, 0, 0, 5, 45, 20, 15, 10, 5];
|
|
|
|
spellLevel = chance.weighted([0,1,2,3,4,5,6,7,8,9], weights[quality]);
|
|
}
|
|
|
|
options.quality = quality;
|
|
var spell = self.getSpell(spellLevel, options);
|
|
|
|
return {
|
|
'name': 'Scroll of ' + spell.name,
|
|
'description': '(' + spell.type + ', spell level ' + spellLevel + ', caster level ' + _.max([1, (spellLevel*2-1)]) + ')',
|
|
'cost': cost[spellLevel],
|
|
'source': spell.source,
|
|
'itemType': TYPES.SCROLLS
|
|
};
|
|
};
|
|
|
|
this.getSpellbook = function(casterLevel, options) {
|
|
options = options ? options : {};
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
if(casterLevel) {
|
|
casterLevel = _.min([casterLevel, 20]);
|
|
var spells = [];
|
|
//there's really no precident for this.
|
|
//I'm just going to assume Wizard only.
|
|
//Assume ALL cantrips
|
|
//Wizards know 3 + Int level 1 spells at level 1
|
|
//Ever level, a wizard adds two new spells to his book
|
|
//The minimum CL for a spell is (spell * 2 - 1)
|
|
//We will just take a shot and say the following
|
|
//One they qualify for that spell level, a wizard will get two at that level
|
|
// and two more on the next level. Past that, a wizard will gain an additional
|
|
// spell of each spell level he knows every few levels.
|
|
var slvl = [2,4,5,5,6,6,6,7,7,7,7,8,8,8,8,9,9,9,9,10];
|
|
//console.log("At Caster Level " + String(casterLevel) + " you get:");
|
|
for(var spellLevel = 0; spellLevel <= 9; spellLevel++) {
|
|
var numSpells = 0;
|
|
var levelSpells = [];
|
|
if(casterLevel >= spellLevel * 2 - 1) { //minimum requirement
|
|
numSpells = slvl[casterLevel - (spellLevel*2-1)];
|
|
if(spellLevel === 0) {
|
|
numSpells = 10;
|
|
}
|
|
|
|
if(spellLevel === 1) {
|
|
numSpells += 4; //since you start with 3 + Int first level spells
|
|
}
|
|
//console.log(String(numSpells) + " level " + String(spellLevel) + " spells");
|
|
for(var i = 0; i < numSpells; i++) {
|
|
var newSpell = {};
|
|
do {
|
|
newSpell = self.getSpell(spellLevel, {type: 'Arcane'});
|
|
} while(_.chain(levelSpells)
|
|
.map(function(s) { return s.name; })
|
|
.contains(newSpell.name)
|
|
.value()
|
|
); //duplicate
|
|
levelSpells.push(newSpell);
|
|
|
|
}
|
|
spells.push(levelSpells);
|
|
}
|
|
else {
|
|
break; //no point checking the higher levels
|
|
}
|
|
}
|
|
//contains[[SL0 spells], [SL1 spells], ...]
|
|
return spells;
|
|
}
|
|
};
|
|
|
|
this.getPotion = function(quality, options) {
|
|
options = options ? options : {};
|
|
var sources = options.sources ? options.sources : self.SOURCES;
|
|
var level = options.potionLevel;
|
|
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
var cost = [25, 50, 300, 750];
|
|
|
|
if(!level || level < 0 || level > 3) {
|
|
var weights = {};
|
|
weights[QUALITIES.MINOR] = [20, 40, 40, 0];
|
|
weights[QUALITIES.MEDIUM] = [ 0, 20, 40, 40];
|
|
weights[QUALITIES.MAJOR] = [ 0, 0, 20, 80];
|
|
|
|
level = chance.weighted([0,1,2,3], weights[quality]);
|
|
}
|
|
options.typeOverride = TYPES.POTIONS;
|
|
options.potionLevel = level;
|
|
var source = selectSource(sources, quality, TYPES.POTIONS, options);
|
|
|
|
return {
|
|
'name': 'Potion/Oil of ' + chance.pick(source.data[TYPES.POTIONS].data[level]),
|
|
'description': '(spell level ' + level + ', caster level ' + _.max([1, (level*2-1)]) + ')',
|
|
'cost': cost[level],
|
|
'source': source.shortName,
|
|
'itemType': TYPES.POTIONS
|
|
};
|
|
};
|
|
|
|
this.getWand = function(quality, options) {
|
|
options = options ? options : {};
|
|
var level = options.wandLevel;
|
|
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
var cost = [375, 750, 4500, 11250, 21000];
|
|
|
|
|
|
if(!level || level < 0 || level > 4) {
|
|
var weights = {};
|
|
weights[QUALITIES.MINOR] = [ 5, 55, 40, 0, 0];
|
|
weights[QUALITIES.MEDIUM] = [ 0, 0, 60, 40, 0];
|
|
weights[QUALITIES.MAJOR] = [ 0, 0, 0, 60, 40];
|
|
|
|
level = chance.weighted([0,1,2,3,4], weights[quality]);
|
|
}
|
|
|
|
options.wandLevel = level;
|
|
options.typeOverride = TYPES.WANDS;
|
|
options.spellType = SPELL_TYPES.ARCANE;
|
|
var spell = self.getSpell(level, options);
|
|
return {'name': 'Wand of ' + spell.name,
|
|
'description': '(spell level ' + level + ', caster level ' + _.max([1, (level*2-1)]) + ', 50 charges)',
|
|
'cost': cost[level],
|
|
'source': spell.source,
|
|
'itemType': TYPES.WANDS
|
|
};
|
|
};
|
|
|
|
this.getWondrousItem = function(quality, options) {
|
|
options = options ? options : {};
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
|
|
//Select the item source based on the quality and item type
|
|
var sources = options.sources ? options.sources : self.SOURCES;
|
|
var source = selectSource(sources, quality, TYPES.WONDROUS_ITEMS, options);
|
|
|
|
//wondrous items of the same quality have equal probability weight
|
|
var item = _.clone(chance.pick(source.data[TYPES.WONDROUS_ITEMS].data[quality]));
|
|
item.source = source.shortName;
|
|
item.itemType = TYPES.WONDROUS_ITEMS;
|
|
|
|
//consumable items should NEVER be intelligent. I don't have a perfect filter, but here's a start
|
|
//I may instead add an attribute for 'consumable'
|
|
if(!/\b(dust|token|tome|elixir|salve|ointment|polish)\b/i.test(item.name)) {
|
|
//make an intelligent item if we either want ALL intelligent items
|
|
//OR intelligent items are NOT disabled AND we roll under the chance threshold
|
|
if(options.allIntelligent || (!options.disableIntelligent && chance.d100() <= (options.intelligenceChance || 1))) {
|
|
item.intelligence = self.getIntelligence(item.cost, options); //'WOOHOO I AM INTELLIGENT!';
|
|
item.cost += item.intelligence.cost;
|
|
}
|
|
}
|
|
return item;
|
|
};
|
|
|
|
this.getRing = function(quality, options) {
|
|
options = options ? options : {};
|
|
|
|
var sources = options.sources ? options.sources : self.SOURCES;
|
|
var source = selectSource(sources, quality, TYPES.RINGS, options);
|
|
var ring = self.getWeightedItem(source.data[TYPES.RINGS].data, quality);
|
|
ring.source = source.shortName;
|
|
ring.itemType = TYPES.RINGS;
|
|
return ring;
|
|
};
|
|
|
|
this.getRod = function(quality, options) {
|
|
options = options ? options : {};
|
|
|
|
var sources = options.sources ? options.sources : self.SOURCES;
|
|
var source = selectSource(sources, quality, TYPES.RODS, options);
|
|
var rod = self.getWeightedItem(source.data[TYPES.RODS].data, quality);
|
|
rod.source = source.shortName;
|
|
rod.itemType = TYPES.RODS;
|
|
return rod;
|
|
};
|
|
|
|
this.getStaff = function(quality, options) {
|
|
options = options ? options : {};
|
|
if(quality == QUALITIES.MINOR) {
|
|
console.error('Error! There are no minor staves!');
|
|
return;
|
|
}
|
|
|
|
var sources = options.sources ? options.sources : self.SOURCES;
|
|
var source = selectSource(sources, quality, TYPES.STAVES, options);
|
|
var staff = self.getWeightedItem(source.data[TYPES.STAVES].data, quality);
|
|
staff.source = source.shortName;
|
|
staff.itemType = TYPES.STAVES;
|
|
|
|
return staff;
|
|
};
|
|
|
|
this.getHumanoid = function(options) {
|
|
options = options ? options : {};
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
return chance.weighted(
|
|
_.map(self.Humanoids, function(h) { return h.name; }),
|
|
_.map(self.Humanoids, function(h) { return h.weight; }));
|
|
};
|
|
|
|
this.getOutsider = function(options) {
|
|
options = options ? options : {};
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
return chance.weighted(
|
|
_.map(self.Outsiders, function(o) { return o.name; }),
|
|
_.map(self.Outsiders, function(o) { return o.weight; }));
|
|
};
|
|
this.getBane = function(options) {
|
|
options = options ? options : {};
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
var bane = chance.weighted(
|
|
_.map(self.Banes, function(b) { return b.name; }),
|
|
_.map(self.Banes, function(b) { return b.weight; }));
|
|
bane = bane.replace(/GET_HUMANOID_TYPE/, self.getHumanoid(options));
|
|
bane = bane.replace(/GET_OUTSIDER_TYPE/, self.getOutsider(options));
|
|
return bane;
|
|
};
|
|
this.getEnergyType = function(options) {
|
|
options = options ? options : {};
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
|
|
return chance.pick(_.values(ENERGY_TYPES));
|
|
};
|
|
|
|
//type is 'armor', 'shield', 'melee', or 'ranged'
|
|
//options.excludeSpecials can contain a list of specials you do NOT want to get
|
|
//this is useful when creating a weapon that you do not want to have duplicate specials
|
|
//options.disableRecursiveSpecials
|
|
this.getSpecials = function(quality, type, options) {
|
|
options = options ? options : {};
|
|
options.excludeSpecials = options.excludeSpecials ? options.excludeSpecials : [];
|
|
options.itemType = options.itemType ? options.itemType :
|
|
(type == 'melee' || type == 'ranged') ? TYPES.WEAPONS :
|
|
(type == 'armor' || type == 'shield') ? TYPES.ARMOR_AND_SHIELDS :
|
|
undefined;
|
|
options.specialType = type + 'Specials';
|
|
options.disableRecursiveSpecials = options.disableRecursiveSpecials || false;
|
|
if(!options.itemType) {
|
|
console.error('Invalid special type', type);
|
|
return;
|
|
}
|
|
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
var sources = options.sources ? options.sources : self.SOURCES;
|
|
|
|
var specials = [];
|
|
var special = {};
|
|
do {
|
|
var source = selectSource(sources, quality, 'Specials', options);
|
|
//data source will look like source.data.Weapons.meleeSpecials
|
|
|
|
special = self.getWeightedItem(source.data[options.itemType].data[options.specialType], quality);
|
|
special.source = source.shortName;
|
|
|
|
//EXCEPTION for Bane special
|
|
if(/GET_BANE_TYPE/.test(special.name)) {
|
|
special.name = special.name.replace(/GET_BANE_TYPE/, self.getBane(options));
|
|
} else if(/GET_ENERGY_RESISTANCE_TYPE/.test(special.name)) {
|
|
special.name = special.name.replace(/GET_ENERGY_RESISTANCE_TYPE/, self.getEnergyType(options));
|
|
}
|
|
|
|
} while(_.chain(options.excludeSpecials).map(function(s) { return s.name; }).includes(special.name).value()); //if we get something from our exclusions, try again
|
|
specials.push(special);
|
|
|
|
var roll = chance.d100();
|
|
//chance for a second special
|
|
if((quality == QUALITIES.MINOR && roll === 100) ||
|
|
(quality == QUALITIES.MEDIUM && roll > 95) ||
|
|
(quality == QUALITIES.MAJOR && roll > 90)) {
|
|
|
|
options.excludeSpecials = options.excludeSpecials.concat(specials); //exclude the special(s) just rolled
|
|
var bonusSpecials = self.getSpecials(quality, type, options);
|
|
if(options.disableRecursiveSpecials) {
|
|
specials.push(bonusSpecials[0]); //if we're not allowed to return recursive specials, just take the first one
|
|
}
|
|
else {
|
|
specials = specials.concat(bonusSpecials);
|
|
}
|
|
}
|
|
|
|
return specials; //return special as array in case we need to concat it with another special
|
|
};
|
|
|
|
this.getUniqueWeapon = function(quality, options) {
|
|
options = options ? options : {};
|
|
options.itemType = TYPES.WEAPONS;
|
|
options.uniqueType = 'uniqueWeapons';
|
|
|
|
var sources = options.sources ? options.sources : self.SOURCES;
|
|
|
|
var source = selectSource(sources, quality, 'Uniques', options);
|
|
var weapon = self.getWeightedItem(source.data[TYPES.WEAPONS].data.uniqueWeapons, quality);
|
|
weapon.source = source.shortName;
|
|
weapon.itemType = TYPES.WEAPONS;
|
|
|
|
return weapon;
|
|
};
|
|
|
|
//options.type can be 'melee' or 'ranged'
|
|
//note that this option is NOT alwawys honored when a unique item is rolled
|
|
//I'm not sure if unique weapons should be allowed to have modifiers.
|
|
//I'm allowing it because it is more awesome. This can cause problems when the unique rolled is, for example, an arrow
|
|
//config.disableRecursiveSpecials If you roll 'get a special and roll again' twice, it will only
|
|
// generate one specials
|
|
this.getWeapon = function(quality, options) {
|
|
options = options ? options : {};
|
|
options.excludeSpecials = options.excludeSpecials || [];
|
|
options.disableRecursiveSpecials = options.disableRecursiveSpecials || false;
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
var weapon = {
|
|
name: '',
|
|
description: '',
|
|
type: '',
|
|
bonus: 0,
|
|
baseCost: 0,
|
|
cost: 0,
|
|
specials: [],
|
|
intelligence: {},
|
|
glowing: false
|
|
};
|
|
weapon.type = (options.type == 'melee' || options.type == 'ranged') ? options.type
|
|
: chance.weighted(['melee', 'ranged'], [7,3]);
|
|
|
|
var roll = chance.d100();
|
|
var action = '';
|
|
|
|
if(quality == QUALITIES.MINOR) {
|
|
if(roll <= 70) {
|
|
//generic +1 weapon
|
|
weapon.bonus = 1;
|
|
}
|
|
else if(roll <= 85) {
|
|
//generic +2 weapon
|
|
weapon.bonus = 2;
|
|
}
|
|
else if(roll <= 90) {
|
|
action = 'getUnique';
|
|
}
|
|
else {
|
|
//10% special ability and roll again
|
|
action = 'getSpecial';
|
|
}
|
|
}
|
|
else if(quality == QUALITIES.MEDIUM) {
|
|
if(roll <= 10) {
|
|
weapon.bonus = 1;
|
|
}
|
|
else if(roll <= 29) {
|
|
weapon.bonus = 2;
|
|
}
|
|
else if(roll <= 58) {
|
|
weapon.bonus = 3;
|
|
}
|
|
else if(roll <= 62) {
|
|
weapon.bonus = 4;
|
|
}
|
|
else if(roll <= 68) {
|
|
action = 'getUnique';
|
|
}
|
|
else {
|
|
//32% special ability and roll again
|
|
action = 'getSpecial';
|
|
}
|
|
}
|
|
else if(quality == QUALITIES.MAJOR) {
|
|
if(roll <= 20) {
|
|
weapon.bonus = 3;
|
|
}
|
|
else if(roll <= 38) {
|
|
weapon.bonus = 4;
|
|
}
|
|
else if(roll <= 49) {
|
|
weapon.bonus = 5;
|
|
}
|
|
else if(roll <= 63) {
|
|
action = 'getUnique';
|
|
}
|
|
else {
|
|
//37% special ability and roll again
|
|
action = 'getSpecial';
|
|
}
|
|
}
|
|
else {
|
|
console.error('invalid quality', quality);
|
|
return;
|
|
}
|
|
if(action == 'getUnique') {
|
|
var unique = self.getUniqueWeapon(quality, options);
|
|
weapon.name = unique.name;
|
|
weapon.baseCost = unique.cost;
|
|
weapon.source = unique.source;
|
|
weapon.url = unique.url;
|
|
}
|
|
else if(action == 'getSpecial') {
|
|
//if we either do not already have any specials OR we allow recursive specials
|
|
if(weapon.specials.length === 0 || options.disableRecursiveSpecials === false) {
|
|
weapon = self.getWeapon(quality, options);
|
|
var specialsBonus = _.chain(weapon.specials).map(function(s) { return s.bonus; }).sum().value();
|
|
var specials = [];
|
|
var newSpecialsBonus = 0;
|
|
options.excludeSpecials = options.excludeSpecials.concat(weapon.specials);
|
|
if(weapon.bonus + specialsBonus < 10) {
|
|
var tries = 5;
|
|
//this has the opportunity to loop forever either if no match can be found
|
|
do {
|
|
specials = self.getSpecials(quality, weapon.type, options);
|
|
newSpecialsBonus = _.chain(specials).map(function(s) { return s.bonus; }).sum().value();
|
|
} while(weapon.bonus + specialsBonus + newSpecialsBonus > 10 && --tries > 0);
|
|
if(tries > 0) { //we found a match without running out of tries!
|
|
weapon.specials = weapon.specials.concat(specials);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//ALL
|
|
//1% chance to be intelligent (NOT usable items - eg Arrows)
|
|
//30% chance to be glowing
|
|
//ammunition should NEVER be intelligent. I don't have a perfect filter, but here's a start
|
|
//I may instead add an attribute for 'consumable'
|
|
if(!/\b(arrow|bolt|bullet)\b/i.test(weapon.name)) {
|
|
if(options.allIntelligent ||
|
|
(!options.disableIntelligent && chance.d100() <= (options.intelligenceChance || 1))) {
|
|
var cost = weapon.baseCost + 2000 * Math.pow(weapon.bonus, 2);
|
|
weapon.intelligence = self.getIntelligence(cost, options); //'WOOHOO I AM INTELLIGENT!';
|
|
weapon.cost += weapon.intelligence.cost;
|
|
}
|
|
}
|
|
if(!options.disableGlowing && chance.d100() <= (options.glowingChance || 30)) {
|
|
weapon.glowing = true;
|
|
}
|
|
|
|
var totalBonus = weapon.bonus + _.sum(_.map(weapon.specials, function(s) { return s.bonus; }));
|
|
var specialsCost = _.sum(_.map(weapon.specials, function(s) { return s.cost || 0; })); //some specials added in APG have flat costs
|
|
weapon.cost = weapon.baseCost + 2000 * Math.pow(totalBonus, 2) + specialsCost + (weapon.intelligence.cost || 0);
|
|
weapon.print = function() {
|
|
var ret = 'Name: ';
|
|
ret += this.glowing ? 'Glowing ' : '';
|
|
ret += this.bonus > 0 ? ('+' + this.bonus + ' ') : '';
|
|
ret += this.specials.length > 0 ? _.map(this.specials, function(s) { return s.name;}).join(', ') + ' ' : '';
|
|
ret += this.name ? this.name : (this.type + ' weapon');
|
|
ret += this.description ? (' ' + this.description) : '';
|
|
ret += ';';
|
|
ret += this.intelligence.print ? 'Intelligence: [' + this.intelligence.print() + '];' : '';
|
|
//ret += 'Cost: ' + this.cost + 'gp';
|
|
return ret;
|
|
};
|
|
|
|
weapon.itemType = TYPES.WEAPONS;
|
|
return weapon;
|
|
};
|
|
|
|
this.getUniqueArmorOrShield = function(quality, options) {
|
|
options = options ? options : {};
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
|
|
options.itemType = TYPES.ARMOR_AND_SHIELDS;
|
|
options.uniqueType = (options.type == 'armor' ? 'uniqueArmors' :
|
|
options.type == 'shield' ? 'uniqueShields' :
|
|
chance.pick(['uniqueArmors', 'uniqueShields'])); //50-50 chance if type is not selected
|
|
options.disableRecursiveSpecials = options.disableRecursiveSpecials || false;
|
|
|
|
var sources = options.sources ? options.sources : self.SOURCES;
|
|
|
|
var source = selectSource(sources, quality, 'Uniques', options);
|
|
var item = self.getWeightedItem(source.data[TYPES.ARMOR_AND_SHIELDS].data[options.uniqueType], quality);
|
|
item.source = source.shortName;
|
|
item.itemType = TYPES.ARMOR_AND_SHIELDS;
|
|
|
|
return item;
|
|
};
|
|
|
|
//options.type can be 'armor' or 'shield'
|
|
this.getArmorOrShield = function(quality, options) {
|
|
options = options ? options : {};
|
|
options.excludeSpecials = options.excludeSpecials || [];
|
|
options.maxSpecials = options.maxSpecials || 5;
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
|
|
var item = {
|
|
name: '',
|
|
description: '',
|
|
type: '',
|
|
bonus: 0,
|
|
baseCost: 0,
|
|
cost: 0,
|
|
specials: [],
|
|
};
|
|
|
|
var roll = chance.d100();
|
|
var action = '';
|
|
|
|
if(quality == QUALITIES.MINOR) {
|
|
if(roll <= 60) {
|
|
item.type = 'shield';
|
|
item.bonus = 1;
|
|
}
|
|
else if(roll <= 80) {
|
|
item.type = 'armor';
|
|
item.bonus = 1;
|
|
}
|
|
else if(roll <= 85) {
|
|
item.type = 'shield';
|
|
item.bonus = 2;
|
|
}
|
|
else if(roll <= 87) {
|
|
item.type = 'armor';
|
|
item.bonus = 2;
|
|
}
|
|
else if(roll <= 89) {
|
|
item.type = 'armor';
|
|
action = 'getUnique';
|
|
}
|
|
else if(roll <= 91) {
|
|
item.type = 'shield';
|
|
action = 'getUnique';
|
|
}
|
|
else if(roll <= 100) {
|
|
//9% special ability and roll again
|
|
action = 'getSpecial';
|
|
}
|
|
}
|
|
else if(quality == QUALITIES.MEDIUM) {
|
|
if(roll <= 5) {
|
|
item.type = 'shield';
|
|
item.bonus = 1;
|
|
}
|
|
else if(roll <= 10) {
|
|
item.type = 'armor';
|
|
item.bonus = 1;
|
|
}
|
|
else if(roll <= 20) {
|
|
item.type = 'shield';
|
|
item.bonus = 2;
|
|
}
|
|
else if(roll <= 30) {
|
|
item.type = 'armor';
|
|
item.bonus = 2;
|
|
}
|
|
else if(roll <= 40) {
|
|
item.type = 'shield';
|
|
item.bonus = 3;
|
|
}
|
|
else if(roll <= 50) {
|
|
item.type = 'armor';
|
|
item.bonus = 3;
|
|
}
|
|
else if(roll <= 55) {
|
|
item.type = 'shield';
|
|
item.bonus = 4;
|
|
}
|
|
else if(roll <= 57) {
|
|
item.type = 'armor';
|
|
item.bonus = 2;
|
|
}
|
|
else if(roll <= 60) {
|
|
item.type = 'armor';
|
|
action = 'getUnique';
|
|
}
|
|
else if(roll <= 63) {
|
|
item.type = 'shield';
|
|
action = 'getUnique';
|
|
}
|
|
else if(roll <= 100) {
|
|
//37% special ability and roll again
|
|
action = 'getSpecial';
|
|
}
|
|
}
|
|
else if(quality == QUALITIES.MAJOR) {
|
|
if(roll <= 8) {
|
|
item.type = 'shield';
|
|
item.bonus = 3;
|
|
}
|
|
else if(roll <= 16) {
|
|
item.type = 'armor';
|
|
item.bonus = 3;
|
|
}
|
|
else if(roll <= 27) {
|
|
item.type = 'shield';
|
|
item.bonus = 4;
|
|
}
|
|
else if(roll <= 38) {
|
|
item.type = 'armor';
|
|
item.bonus = 4;
|
|
}
|
|
else if(roll <= 49) {
|
|
item.type = 'shield';
|
|
item.bonus = 5;
|
|
}
|
|
else if(roll <= 57) {
|
|
item.type = 'armor';
|
|
item.bonus = 5;
|
|
}
|
|
else if(roll <= 60) {
|
|
item.type = 'armor';
|
|
action = 'getUnique';
|
|
}
|
|
else if(roll <= 63) {
|
|
item.type = 'shield';
|
|
action = 'getUnique';
|
|
}
|
|
else if(roll <= 100) {
|
|
//37% special ability and roll again
|
|
action = 'getSpecial';
|
|
}
|
|
}
|
|
if(action == 'getUnique') {
|
|
options.type = item.type;
|
|
var unique = self.getUniqueArmorOrShield(quality, options);
|
|
item.name = unique.name;
|
|
item.baseCost = unique.cost;
|
|
item.url = unique.url;
|
|
}
|
|
else if(action == 'getSpecial') {
|
|
if(item.specials.length === 0 || options.disableRecursiveSpecials === false) {
|
|
item = self.getArmorOrShield(quality, options);
|
|
var specialsBonus = _.chain(item.specials).map(function(s) { return s.bonus; }).sum().value();
|
|
|
|
var specials = [];
|
|
var newSpecialsBonus = 0;
|
|
options.excludeSpecials = options.excludeSpecials.concat(item.specials);
|
|
|
|
if(item.bonus + specialsBonus < 10) {
|
|
var tries = 5;
|
|
//this has the opportunity to loop forever either if no match can be found
|
|
do {
|
|
specials = self.getSpecials(quality, item.type, options);
|
|
newSpecialsBonus = _.chain(specials).map(function(s) { return s.bonus; }).sum().value();
|
|
|
|
} while(item.bonus + specialsBonus + newSpecialsBonus > 10 && --tries > 0);
|
|
if(tries > 0) { //we found a match without running out of tries!
|
|
item.specials = item.specials.concat(specials);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
var totalBonus = item.bonus + _.sum(_.map(item.specials, function(s) { return s.bonus; }));
|
|
var specialsCost = _.sum(_.map(item.specials, function(s) { return s.cost; }));
|
|
item.cost = item.baseCost + 1000 * Math.pow(totalBonus, 2) + specialsCost;
|
|
|
|
item.print = function() {
|
|
var ret = 'Name: ';
|
|
ret += this.bonus > 0 ? ('+' + this.bonus + ' ') : '';
|
|
ret += this.specials.length > 0 ? _.map(this.specials, function(s) { return s.name;}).join(', ') + ' ' : '';
|
|
ret += this.name ? this.name : this.type;
|
|
ret += this.description ? (' ' + this.description) : '';
|
|
//ret += ';';
|
|
//ret += 'Cost: ' + this.cost + 'gp';
|
|
return ret;
|
|
};
|
|
|
|
item.itemType = TYPES.ARMOR_AND_SHIELDS;
|
|
return item;
|
|
};
|
|
|
|
|
|
//can take clWeights and geWeights to weight the alignment probabilities
|
|
//clWeights are for [chaotic, neutral, lawful]
|
|
//geWeights are for [good, neutral, evil]
|
|
this.getAlignment = function(options) {
|
|
options = options ? options : {};
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
var clWeights = options.clWeights ? options.clWeights : [1,1,1];
|
|
var geWeights = options.geWeights ? options.geWeights : [1,1,1];
|
|
|
|
var alignment = chance.weighted(['Chaotic', 'Neutral', 'Lawful'], clWeights) +
|
|
' ' +
|
|
chance.weighted(['Good', 'Neutral', 'Evil'], geWeights);
|
|
if(alignment == 'Neutral Neutral') {
|
|
alignment = 'True Neutral';
|
|
}
|
|
return alignment;
|
|
};
|
|
|
|
//options.cl can be Chaotic, Neutral, or Lawful
|
|
//options.ge can be Good, Neutral, or Evil
|
|
this.getDeity = function(options) {
|
|
options = options ? options : {};
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
var cl = options.cl || chance.pick(['Chaotic', 'Neutral', 'Lawful']);
|
|
var ge = options.ge || chance.pick(['Good', 'Neutral', 'Evil']);
|
|
var deity = {};
|
|
deity.name = chance.pick(self.Deities.data[cl][ge]);
|
|
deity.alignment = (cl == 'Neutral' && ge == 'Neutral') ? 'True Neutral' : (cl + ' ' + ge);
|
|
|
|
return deity;
|
|
};
|
|
|
|
//baseCost is the base cost of the item the intelligence is being made for.
|
|
//This directly affects the intelligence's ego score!
|
|
//baseCost also directly affects the probability of the number of special abilities an intelligence will have
|
|
//options.numPowers
|
|
//options.purposeChance
|
|
//options.numDedicatedPowers
|
|
this.getIntelligence = function(baseCost, options) {
|
|
baseCost = baseCost || 0;
|
|
options = options || {};
|
|
var chance = (self.chance ? self.chance : options.seed ? new Chance(options.seed) : new Chance());
|
|
var i = {}; //intelligence object. we'll be referencing this a LOT
|
|
|
|
|
|
//***BASE VALUES***
|
|
i.cost = 500; //base starting cost
|
|
i.ego = 0;
|
|
//Base cost ego modifier
|
|
switch(true) {
|
|
case (baseCost <= 1000): i.ego += 0; break;
|
|
case (baseCost <= 5000): i.ego += 1; break;
|
|
case (baseCost <= 10000): i.ego += 2; break;
|
|
case (baseCost <= 20000): i.ego += 3; break;
|
|
case (baseCost <= 50000): i.ego += 4; break;
|
|
case (baseCost <= 100000): i.ego += 6; break;
|
|
case (baseCost <= 200000): i.ego += 8; break;
|
|
case (baseCost > 200000): i.ego += 12; break;
|
|
}
|
|
|
|
|
|
//***ITEM ABILITY STATS***
|
|
var statScores = [
|
|
{'score': 10, 'costMod': 0, 'egoMod': 0},
|
|
{'score': 11, 'costMod': 200, 'egoMod': 0},
|
|
{'score': 12, 'costMod': 500, 'egoMod': 1},
|
|
{'score': 13, 'costMod': 700, 'egoMod': 1},
|
|
{'score': 14, 'costMod': 1000, 'egoMod': 2},
|
|
{'score': 15, 'costMod': 1400, 'egoMod': 2},
|
|
{'score': 16, 'costMod': 2000, 'egoMod': 3},
|
|
{'score': 17, 'costMod': 2800, 'egoMod': 3},
|
|
{'score': 18, 'costMod': 4000, 'egoMod': 4},
|
|
{'score': 19, 'costMod': 5200, 'egoMod': 4},
|
|
{'score': 20, 'costMod': 8000, 'egoMod': 5},
|
|
];
|
|
var statWeights = [10, 10, 20, 10, 20, 5, 5, 5, 5, 5, 5];
|
|
|
|
i.stats = {};
|
|
['intelligence', 'wisdom', 'charisma'].forEach(function(stat) {
|
|
var score = chance.weighted(statScores, statWeights);
|
|
i.stats[stat] = score.score;
|
|
i.cost += score.costMod;
|
|
i.ego += score.egoMod;
|
|
});
|
|
|
|
i.alignment = self.getAlignment(options);
|
|
|
|
|
|
//***ITEM COMMUNICATION AND SENSES***
|
|
//get communication ability for the item.
|
|
var communicationTypes = [
|
|
{'description': 'Empathy', 'costMod': 0, 'egoMod': 0},
|
|
{'description': 'Speech', 'costMod': 500, 'egoMod': 0},
|
|
{'description': 'Telepathy', 'costMod': 1000, 'egoMod': 1},
|
|
];
|
|
var communicationWeights = [3,4,3]; //There was no roll table so I made this up
|
|
var communication = chance.weighted(communicationTypes, communicationWeights);
|
|
|
|
//get senses for the item. there was no roll table so I made this up
|
|
var basicSenses = [
|
|
{'description': 'Senses (30 ft.)', 'costMod': 0, 'egoMod': 0},
|
|
{'description': 'Senses (60 ft.)', 'costMod': 500, 'egoMod': 0},
|
|
{'description': 'Senses (120 ft.)', 'costMod': 1000, 'egoMod': 0},
|
|
];
|
|
var basicSensesWeights = [5,3,2]; //There was no roll table so I made this up
|
|
var basicSense = chance.weighted(basicSenses, basicSensesWeights);
|
|
|
|
//get special senses for the item.
|
|
var specialSenses = [
|
|
{'description': '', 'costMod': 0, 'egoMod': 0},
|
|
{'description': 'Darkvision', 'costMod': 500, 'egoMod': 0},
|
|
{'description': 'Blindsense', 'costMod': 5000, 'egoMod': 1},
|
|
{'description': 'Read languages', 'costMod': 1000, 'egoMod': 1},
|
|
{'description': 'Read magic', 'costMod': 2000, 'egoMod': 1},
|
|
];
|
|
//There was no roll table so I made this up and limited it to only 1 special per item
|
|
var specialSensesWeights = [50, 20, 5, 15, 10];
|
|
var specialSense = chance.weighted(specialSenses, specialSensesWeights);
|
|
|
|
i.communication = communication.description;
|
|
i.senses = basicSense.description + (specialSense.description ? ', ' + specialSense.description : '');
|
|
|
|
i.cost += communication.costMod + basicSense.costMod + specialSense.costMod;
|
|
i.ego += communication.egoMod + basicSense.egoMod + specialSense.egoMod;
|
|
|
|
|
|
//***ITEM POWERS***
|
|
//Get item powers!
|
|
var powerCountWeights = [1,1,1,1,1];
|
|
//The probability for more powers is directly related to the base cost of the item
|
|
//I made up these roll weights. references the same basecost as the basic ego modifier
|
|
switch(true) {
|
|
case (baseCost <= 1000): powerCountWeights = [6,4,0,0,0]; break;
|
|
case (baseCost <= 5000): powerCountWeights = [3,4,3,0,0]; break;
|
|
case (baseCost <= 10000): powerCountWeights = [2,3,4,1,0]; break;
|
|
case (baseCost <= 20000): powerCountWeights = [1,3,4,2,0]; break;
|
|
case (baseCost <= 50000): powerCountWeights = [0,2,4,3,1]; break;
|
|
case (baseCost <= 100000): powerCountWeights = [0,1,3,4,2]; break;
|
|
case (baseCost <= 200000): powerCountWeights = [0,0,2,5,3]; break;
|
|
case (baseCost > 200000): powerCountWeights = [0,0,0,6,4]; break;
|
|
}
|
|
|
|
var powerWeights = _.map(self.ItemPowers, function(p) { return p.weight; });
|
|
|
|
i.powers = [];
|
|
var reSkill = /GET_ITEM_SKILL/;
|
|
var reSpell = /GET_SPELL_LEVEL_(.)/; //for spell levels 0-9
|
|
|
|
//Another option would be to do this with _.sample but it does not support weights
|
|
_.times((options.numPowers || chance.weighted(_.range(1,6), powerCountWeights)), function() {
|
|
var power = {};
|
|
do {
|
|
power = _.clone(chance.weighted(_.clone(self.ItemPowers), powerWeights));
|
|
if(reSkill.test(power.description)) {
|
|
power.description = power.description.replace(reSkill, chance.pick(self.ItemSkills).name);
|
|
}
|
|
else if (reSpell.test(power.description)) {
|
|
var spellLevel = reSpell.exec(power.description)[1];
|
|
power.spell = self.getSpell(spellLevel, options); //add the spell onto the power for later reference
|
|
power.description = power.description.replace(reSpell, power.spell.name);
|
|
}
|
|
} while(_.findWhere(i.powers, {'description': power.description}) && !power.repeatable); //power exists in item powers and is not repeatable
|
|
if(_.findWhere(i.powers, {'description': power.description})) { //if we selected an existing but repeatable power
|
|
//add the new power values to the old power
|
|
var oldPower = _.findWhere(i.powers, {'description': power.description});
|
|
oldPower.costMod += power.costMod;
|
|
oldPower.egoMod += power.egoMod;
|
|
oldPower.value += power.value;
|
|
}
|
|
else {
|
|
i.powers.push(power);
|
|
}
|
|
});
|
|
|
|
//Set for the next section
|
|
var bonusLanguages = 0;
|
|
|
|
//Finalize powers
|
|
i.powers.forEach(function(p) {
|
|
p.description = p.description.replace(/#/, p.value);
|
|
i.ego += p.egoMod;
|
|
i.cost += p.costMod;
|
|
|
|
if(/Linguistics/i.test(p.description)) { //check if we got any ranks in linguistics
|
|
bonusLanguages += p.value;
|
|
}
|
|
|
|
//clean up the power
|
|
delete p.value;
|
|
delete p.egoMod;
|
|
delete p.costMod;
|
|
delete p.weight;
|
|
delete p.repeatable;
|
|
//final power should only have description and spell
|
|
});
|
|
|
|
|
|
//***ITEM BONUS LANGUAGES***
|
|
i.languages = ['Common']; //ALL intelligent items know Common
|
|
//extra languages are based on intelligence and ranks in Linguistics
|
|
bonusLanguages += Math.floor((i.stats.intelligence-10)/2);
|
|
//pick is safe even if we have more bonusLanguages than exist
|
|
if(bonusLanguages > 0) {
|
|
i.languages = i.languages.concat(chance.pick(self.Languages.data, bonusLanguages));
|
|
}
|
|
|
|
//***ITEM PURPOSE***
|
|
//there was NO roll chart for determining if an item has a purpose so I made this up
|
|
//I figure 30% of the time sounds good
|
|
i.purpose = '';
|
|
i.dedicatedPowers = [];
|
|
|
|
if(chance.d100() <= (options.purposeChance || 30)) {
|
|
var reBane = /GET_BANE/;
|
|
var reHumOrOut = /GET_HUMANOID_OR_OUTSIDER_TYPE/;
|
|
var reDeity = /GET_DEITY/;
|
|
//get purpose
|
|
var purpose = chance.weighted(_.clone(self.ItemPurposes),
|
|
_.map(self.ItemPurposes, function(p) { return p.weight; }));
|
|
i.purpose = purpose.description;
|
|
i.ego += purpose.egoMod;
|
|
|
|
if(reBane.test(i.purpose)) {
|
|
i.purpose = i.purpose.replace(reBane, self.getBane(options));
|
|
}
|
|
else if(reHumOrOut.test(i.purpose)) {
|
|
i.purpose = i.purpose.replace(reHumOrOut,
|
|
chance.pick(['Humanoids (' + self.getHumanoid(options) + ')',
|
|
'Outsiders (' + self.getOutsider(options) + ')']));
|
|
}
|
|
else if(reDeity.test(i.purpose)) {
|
|
var deity = self.getDeity(options);
|
|
i.purpose = i.purpose.replace(reDeity, deity.name + ' (' + deity.alignment + ')');
|
|
}
|
|
|
|
//console.log(i.purpose.description);
|
|
i.dedicatedPowers = [];
|
|
//I decided that purposed items get between 1-3 dedicated powers, heavily leaning on 1
|
|
var numDedicatedPowers = options.numDedicatedPowers || chance.weighted([1,2,3], [7,2,1]);
|
|
_.times(numDedicatedPowers, function() {
|
|
var power = {};
|
|
do {
|
|
power = _.clone(chance.weighted(self.ItemDedicatedPowers,
|
|
_.map(self.ItemDedicatedPowers, function(p) { return p.weight; })));
|
|
|
|
if (reSpell.test(power.description)) { //as in the normal powers
|
|
var spellLevel = reSpell.exec(power.description)[1];
|
|
power.spell = self.getSpell(spellLevel, options); //add the spell onto the power for later reference
|
|
power.description = power.description.replace(reSpell, power.spell.name);
|
|
}
|
|
//don't allow for duplicate dedicated powers
|
|
} while(_.findWhere(i.dedicatedPowers, {'description': power.description}));
|
|
//finalize dedicated power
|
|
i.cost += power.costMod;
|
|
i.ego += power.egoMod;
|
|
delete power.costMod;
|
|
delete power.egoMod;
|
|
delete power.weight;
|
|
i.dedicatedPowers.push(power);
|
|
});
|
|
}
|
|
|
|
//***PRINT FUNCTION***
|
|
i.print = function() {
|
|
var ret = '';
|
|
ret += 'Alignment: ' + this.alignment + ';';
|
|
ret += 'Stats: Ego ' + this.ego + ', Int ' + this.stats.intelligence + ', ' +
|
|
'Wis ' + this.stats.wisdom + ', ' +
|
|
'Cha ' + this.stats.charisma + ';';
|
|
ret += 'Communication: ' + this.communication + ' (' + this.languages.join(', ' ) + ');';
|
|
ret += 'Senses: ' + this.senses + ';';
|
|
ret += 'Powers: ' + _.map(this.powers, function(p) { return p.description; }).join(', ') + ';';
|
|
if(this.purpose) {
|
|
ret += 'Purpose: ' + this.purpose + ';';
|
|
ret += 'Dedicated Powers: ' + _.map(this.dedicatedPowers, function(p) {return p.description; }).join(', ') + ';';
|
|
}
|
|
//ret += 'Cost: ' + i.cost + 'gp';
|
|
|
|
return ret;
|
|
|
|
};
|
|
|
|
return i; //object intelligence
|
|
};
|
|
|
|
}; |