Began abstracting default abilities, damage multipliers and skills into rulesets.

This commit is contained in:
Stefan Zermatten
2018-08-24 13:30:31 +02:00
parent 5c40a84660
commit b4506fd939
5 changed files with 157 additions and 485 deletions

View File

@@ -17,6 +17,10 @@ Schemas.Attribute = new SimpleSchema({
variableName: {
type: String,
},
// Attributes need to store their order to keep the sheet consistent
order: {
type: Number,
},
type: {
type: String,
allowedValues: [
@@ -26,7 +30,7 @@ Schemas.Attribute = new SimpleSchema({
"healthBar", // Hitpoints, Temporary Hitpoints
"resource", // Rages, sorcery points
"spellSlot", // Level 1, 2, 3... spell slots
"attribute", // Aren't displayed, Jump height, Carry capacity
"utility", // Aren't displayed, Jump height, Carry capacity
],
},
value: {

View File

@@ -1,3 +1,8 @@
// TODO recalculate carried weight method
// TODO Add attribute, skill, damageMultiplier and bundle library models
// Bundle libraries should support races, classes, and rulesets
// TODO Add monster library
import { ValidatedMethod } from 'meteor/mdg:validated-method';
const recomputeCharacter = new ValidatedMethod({
@@ -8,7 +13,7 @@ const recomputeCharacter = new ValidatedMethod({
charId: { type: String }
}).validator(),
run({ charId }) {
run({charId}) {
if (!canEditCharacter(charId, this.userId)) {
// Throw errors with a specific error code
throw new Meteor.Error('Characters.methods.recomputeCharacter.denied',
@@ -21,6 +26,31 @@ const recomputeCharacter = new ValidatedMethod({
});
const recomputeCharacterXP = new ValidatedMethod({
name: "Characters.methods.recomputeCharacterXP",
validate: new SimpleSchema({
charId: { type: String }
}).validator(),
run({charId}) {
if (!canEditCharacter(charId, this.userId)) {
// Throw errors with a specific error code
throw new Meteor.Error("Characters.methods.recomputeCharacterXP.denied",
"You do not have permission to recompute this character's XP");
}
var xp = 0;
Experiences.find(
{charId: charId},
{fields: {value: 1}}
).forEach(function(e){
xp += e.value;
});
//TODO write the XP to the database, don't just return it
return xp;
},
})
/*
* This function is the heart of DiceCloud. It recomputes a character's stats,
* distilling down effects and proficiencies into the final stats that make up
@@ -342,11 +372,11 @@ const combineSkill = function(stat, char){
if (prof.value > stat.proficiency) stat.proficiency = prof.value;
}
let profBonus;
if (char.atts.proificiencyBonus){
if (!char.atts.proficiencyBonus.computed){
computeStat(char.atts.proficiencyBonus, char);
if (char.skills.proificiencyBonus){
if (!char.skills.proficiencyBonus.computed){
computeStat(char.skills.proficiencyBonus, char);
}
profBonus = char.atts.proficiencyBonus.result;
profBonus = char.skills.proficiencyBonus.result;
} else {
profBonus = Math.floor(char.level / 4 + 1.75);
}

View File

@@ -16,151 +16,9 @@ Schemas.Character = new SimpleSchema({
flaws: {type: String, defaultValue: "", trim: false, optional: true},
backstory: {type: String, defaultValue: "", trim: false, optional: true},
//attributes
//ability scores
strength: {type: Schemas.Attribute},
dexterity: {type: Schemas.Attribute},
constitution: {type: Schemas.Attribute},
intelligence: {type: Schemas.Attribute},
wisdom: {type: Schemas.Attribute},
charisma: {type: Schemas.Attribute},
//stats
hitPoints: {type: Schemas.Attribute},
tempHP: {type: Schemas.Attribute},
experience: {type: Schemas.Attribute},
proficiencyBonus: {type: Schemas.Attribute},
speed: {type: Schemas.Attribute},
weight: {type: Schemas.Attribute},
age: {type: Schemas.Attribute},
ageRate: {type: Schemas.Attribute},
armor: {type: Schemas.Attribute},
carryMultiplier: {type: Schemas.Attribute},
//resources
level1SpellSlots: {type: Schemas.Attribute},
level2SpellSlots: {type: Schemas.Attribute},
level3SpellSlots: {type: Schemas.Attribute},
level4SpellSlots: {type: Schemas.Attribute},
level5SpellSlots: {type: Schemas.Attribute},
level6SpellSlots: {type: Schemas.Attribute},
level7SpellSlots: {type: Schemas.Attribute},
level8SpellSlots: {type: Schemas.Attribute},
level9SpellSlots: {type: Schemas.Attribute},
ki: {type: Schemas.Attribute},
sorceryPoints: {type: Schemas.Attribute},
rages: {type: Schemas.Attribute},
superiorityDice: {type: Schemas.Attribute},
expertiseDice: {type: Schemas.Attribute},
//specific features
rageDamage: {type: Schemas.Attribute},
//hit dice
d6HitDice: {type: Schemas.Attribute},
d8HitDice: {type: Schemas.Attribute},
d10HitDice: {type: Schemas.Attribute},
d12HitDice: {type: Schemas.Attribute},
//vulnerabilities
acidMultiplier: {type: Schemas.Attribute},
bludgeoningMultiplier: {type: Schemas.Attribute},
coldMultiplier: {type: Schemas.Attribute},
fireMultiplier: {type: Schemas.Attribute},
forceMultiplier: {type: Schemas.Attribute},
lightningMultiplier: {type: Schemas.Attribute},
necroticMultiplier: {type: Schemas.Attribute},
piercingMultiplier: {type: Schemas.Attribute},
poisonMultiplier: {type: Schemas.Attribute},
psychicMultiplier: {type: Schemas.Attribute},
radiantMultiplier: {type: Schemas.Attribute},
slashingMultiplier: {type: Schemas.Attribute},
thunderMultiplier: {type: Schemas.Attribute},
//skills
//saves
strengthSave: {type: Schemas.Skill},
"strengthSave.ability": {type: String, defaultValue: "strength"},
dexteritySave: {type: Schemas.Skill},
"dexteritySave.ability": {type: String, defaultValue: "dexterity"},
constitutionSave:{type: Schemas.Skill},
"constitutionSave.ability": {type: String, defaultValue: "constitution"},
intelligenceSave:{type: Schemas.Skill},
"intelligenceSave.ability": {type: String, defaultValue: "intelligence"},
wisdomSave: {type: Schemas.Skill},
"wisdomSave.ability": {type: String, defaultValue: "wisdom"},
charismaSave: {type: Schemas.Skill},
"charismaSave.ability": {type: String, defaultValue: "charisma"},
//skill skills
acrobatics: {type: Schemas.Skill},
"acrobatics.ability": {type: String, defaultValue: "dexterity"},
animalHandling: {type: Schemas.Skill},
"animalHandling.ability": {type: String, defaultValue: "wisdom"},
arcana: {type: Schemas.Skill},
"arcana.ability": {type: String, defaultValue: "intelligence"},
athletics: {type: Schemas.Skill},
"athletics.ability": {type: String, defaultValue: "strength"},
deception: {type: Schemas.Skill},
"deception.ability": {type: String, defaultValue: "charisma"},
history: {type: Schemas.Skill},
"history.ability": {type: String, defaultValue: "intelligence"},
insight: {type: Schemas.Skill},
"insight.ability": {type: String, defaultValue: "wisdom"},
intimidation: {type: Schemas.Skill},
"intimidation.ability": {type: String, defaultValue: "charisma"},
investigation: {type: Schemas.Skill},
"investigation.ability": {type: String, defaultValue: "intelligence"},
medicine: {type: Schemas.Skill},
"medicine.ability": {type: String, defaultValue: "wisdom"},
nature: {type: Schemas.Skill},
"nature.ability": {type: String, defaultValue: "intelligence"},
perception: {type: Schemas.Skill},
"perception.ability": {type: String, defaultValue: "wisdom"},
performance: {type: Schemas.Skill},
"performance.ability": {type: String, defaultValue: "charisma"},
persuasion: {type: Schemas.Skill},
"persuasion.ability": {type: String, defaultValue: "charisma"},
religion: {type: Schemas.Skill},
"religion.ability": {type: String, defaultValue: "intelligence"},
sleightOfHand: {type: Schemas.Skill},
"sleightOfHand.ability": {type: String, defaultValue: "dexterity"},
stealth: {type: Schemas.Skill},
"stealth.ability": {type: String, defaultValue: "dexterity"},
survival: {type: Schemas.Skill},
"survival.ability": {type: String, defaultValue: "wisdom"},
//Mechanical Skills
initiative: {type: Schemas.Skill},
"initiative.ability": {type: String, defaultValue: "dexterity"},
dexterityArmor: {type: Schemas.Skill},
"dexterityArmor.ability": {type: String, defaultValue: "dexterity"},
//mechanics
deathSave: {type: Schemas.DeathSave},
deathSave: {type: Schemas.DeathSave},
xp: {type: Number, defaultValue: 0},
//permissions
party: {type: String, regEx: SimpleSchema.RegEx.Id, optional: true},
@@ -196,238 +54,8 @@ Schemas.Character = new SimpleSchema({
Characters.attachSchema(Schemas.Character);
var attributeBase = preventLoop(function(charId, statName){
check(statName, String);
//if it's a damage multiplier, we treat it specially
if (_.contains(DAMAGE_MULTIPLIERS, statName)){
var invulnerabilityCount = Effects.find({
charId: charId,
stat: statName,
enabled: true,
operation: "mul",
value: 0,
}).count();
if (invulnerabilityCount) return 0;
var resistCount = Effects.find({
charId: charId,
stat: statName,
enabled: true,
operation: "mul",
value: 0.5,
}).count();
var vulnCount = Effects.find({
charId: charId,
stat: statName,
enabled: true,
operation: "mul",
value: 2,
}).count();
if (!resistCount && !vulnCount){
return 1;
} else if (resistCount && !vulnCount){
return 0.5;
} else if (!resistCount && vulnCount){
return 2;
} else {
return 1;
}
}
var value;
var base = 0;
var add = 0;
var mul = 1;
var min = Number.NEGATIVE_INFINITY;
var max = Number.POSITIVE_INFINITY;
Effects.find({
charId: charId,
stat: statName,
enabled: true,
operation: {$in: ["base", "add", "mul", "min", "max"]},
}).forEach(function(effect) {
value = evaluateEffect(charId, effect);
if (effect.operation === "base"){
if (value > base) base = value;
} else if (effect.operation === "add"){
add += value;
} else if (effect.operation === "mul"){
mul *= value;
} else if (effect.operation === "min"){
if (value > min) min = value;
} else if (effect.operation === "max"){
if (value < max) max = value;
}
});
var result = (base + add) * mul;
if (result < min) result = min;
if (result > max) result = max;
// Don't round carry multiplier
if (statName === "carryMultiplier"){
return result;
}
return Math.floor(result);
});
if (Meteor.isClient) {
Template.registerHelper("characterCalculate", function(func, charId, input) {
try {
return Characters.calculate[func](charId, input);
} catch (e){
if (!Characters.calculate[func]){
throw new Error(func + "is not a function name");
} else {
throw e;
}
}
});
}
//create a local memoize with a argument concatenating hash function
var memoize = function(f) {
if (Meteor.isServer) return f;
return Tracker.memoize(f, function() {
return _.reduce(arguments, function(memo, arg) {
return memo + arg;
}, "");
});
};
//memoize funcitons that have finds and slow loops
Characters.calculate = {
getField: function(charId, fieldName) {
var fieldSelector = {};
fieldSelector[fieldName] = 1;
var char = Characters.findOne(charId, {fields: fieldSelector});
if (!char) return;
var field = char[fieldName];
if (field === undefined){
throw new Meteor.Error(
"getField failed",
"getField could not find field " +
fieldName +
" in character " +
char._id
);
}
return field;
},
fieldValue: function(charId, fieldName) {
if (!Schemas.Character.schema(fieldName)){
throw new Meteor.Error(
"Field not found",
"Character's schema does not contain a field called: " + fieldName
);
}
//duck typing to get the right value function
//.ability implies skill
if (Schemas.Character.schema(fieldName + ".ability")){
return Characters.calculate.skillMod(charId, fieldName);
}
//adjustment implies attribute
if (Schemas.Character.schema(fieldName + ".adjustment")){
return Characters.calculate.attributeValue(charId, fieldName);
}
//fall back to just returning the field itself
return Characters.calculate.getField(charId, fieldName);
},
attributeValue: memoize(function(charId, attributeName){
var attribute = Characters.calculate.getField(charId, attributeName);
if (!attribute) return;
//base value
var value = Characters.calculate.attributeBase(charId, attributeName);
//plus adjustment
value += attribute.adjustment;
return value;
}),
attributeBase: memoize(function(charId, attributeName){
return attributeBase(charId, attributeName);
}),
skillMod: memoize(preventLoop(function(charId, skillName){
var skill = Characters.calculate.getField(charId, skillName);
if (!skill) return;
//get the final value of the ability score
var ability = Characters.calculate.attributeValue(charId, skill.ability);
//base modifier
var mod = +getMod(ability);
//multiply proficiency bonus by largest value in proficiency array
var prof = Characters.calculate.proficiency(charId, skillName);
//add multiplied proficiency bonus to modifier
mod += prof * Characters.calculate.attributeValue(charId, "proficiencyBonus");
//apply all effects
var value;
var add = 0;
var mul = 1;
var min = Number.NEGATIVE_INFINITY;
var max = Number.POSITIVE_INFINITY;
Effects.find({
charId: charId,
stat: skillName,
enabled: true,
operation: {$in: ["base", "add", "mul", "min", "max"]},
}).forEach(function(effect) {
value = evaluateEffect(charId, effect);
if (effect.operation === "add"){
add += value;
} else if (effect.operation === "mul"){
mul *= value;
} else if (effect.operation === "min"){
if (value > min) min = value;
} else if (effect.operation === "max"){
if (value < max) max = value;
}
});
var result = (mod + add) * mul;
if (result < min) result = min;
if (result > max) result = max;
return Math.floor(result);
})),
proficiency: memoize(function(charId, skillName){
//return largest value in proficiency array
var prof = Proficiencies.findOne(
{charId: charId, name: skillName, enabled: true},
{sort: {value: -1}}
);
return prof && prof.value || 0;
}),
passiveSkill: memoize(function(charId, skillName){
var mod = +Characters.calculate.skillMod(charId, skillName);
var value = 10 + mod;
Effects.find(
{charId: charId, stat: skillName, enabled: true, operation: "passiveAdd"}
).forEach(function(effect){
value += evaluateEffect(charId, effect);
});
var advantage = Characters.calculate.advantage(charId, skillName);
value += 5 * advantage;
return Math.floor(value);
}),
advantage: memoize(function(charId, skillName){
var advantage = Effects.find(
{charId: charId, stat: skillName, enabled: true, operation: "advantage"}
).count();
var disadvantage = Effects.find(
{charId: charId, stat: skillName, enabled: true, operation: "disadvantage"}
).count();
if (advantage && !disadvantage) return 1;
if (disadvantage && !advantage) return -1;
return 0;
}),
abilityMod: function(charId, attribute){
return getMod(
Characters.calculate.attributeValue(charId, attribute)
);
},
passiveAbility: function(charId, attribute){
var mod = +getMod(Characters.calculate.attributeValue(charId, attribute));
return 10 + mod;
},
xpLevel: function(charId){
var xp = Characters.calculate.experience(charId);
for (var i = 0; i < 19; i++){
@@ -438,116 +66,28 @@ Characters.calculate = {
if (xp > 355000) return 20;
return 0;
},
level: memoize(function(charId){
var level = 0;
Classes.find({charId: charId}).forEach(function(cls){
level += cls.level;
});
return level;
}),
experience: memoize(function(charId){
var xp = 0;
Experiences.find(
{charId: charId},
{fields: {value: 1}}
).forEach(function(e){
xp += e.value;
});
return xp;
}),
};
var deprecated = function() {
//var err = new Error("this function has been deprecated");
var name = "";
if (Template.instance()){
name = Template.instance().view.name;
}
var logString = "this function has been deprecated \n";
if (name){
logString += "View: " + name + "\n\n";
}
//logString += err.stack + "\n\n---------------------\n\n";
console.log(logString);
};
//functions and calculated values.
//These functions can only rely on this._id since no other
//field is likely to be attached to all returned characters
Characters.helpers({
//returns the value stored in the field requested
//will set up dependencies on just that field
getField : function(fieldName){
deprecated();
return Characters.calculate.getField(this._id, fieldName);
},
//returns the value of a field
fieldValue : function(fieldName){
deprecated();
return Characters.calculate.fieldValue(this._id, fieldName);
},
attributeValue: function(attributeName){
deprecated();
return Characters.calculate.attributeValue(this._id, attributeName);
},
attributeBase: function(attributeName){
deprecated();
return Characters.calculate.attributeBase(this._id, attributeName);
},
skillMod: function(skillName){
deprecated();
return Characters.calculate.skillMod(this._id, skillName);
},
proficiency: function(skillName){
deprecated();
return Characters.calculate.proficiency(this._id, skillName);
},
passiveSkill: function(skillName){
deprecated();
return Characters.calculate.passiveSkill(this._id, skillName);
},
advantage: function(skillName){
deprecated();
return Characters.calculate.advantage(this._id, skillName);
},
abilityMod: function(attribute){
deprecated();
return Characters.calculate.abilityMod(this._id, attribute);
},
passiveAbility: function(attribute){
deprecated();
return Characters.calculate.passiveAbility(this._id, attribute);
},
xpLevel: function(){
deprecated();
return Characters.calculate.xpLevel(this._id);
},
level: function(){
deprecated();
return Characters.calculate.level(this._id);
},
experience: function(){
deprecated();
return Characters.calculate.experience(this._id);
},
});
//clean up all data related to that character before removing it
if (Meteor.isServer){
Characters.after.remove(function(userId, character) {
Actions .remove({charId: character._id});
Attacks .remove({charId: character._id});
Buffs .remove({charId: character._id});
Classes .remove({charId: character._id});
CustomBuffs .remove({charId: character._id});
Effects .remove({charId: character._id});
Experiences .remove({charId: character._id});
Features .remove({charId: character._id});
Notes .remove({charId: character._id});
Proficiencies .remove({charId: character._id});
SpellLists .remove({charId: character._id});
Items .remove({charId: character._id});
Containers .remove({charId: character._id});
let charId = character._id;
Actions .remove({charId});
Attacks .remove({charId});
Attributes .remove({charId});
Buffs .remove({charId});
Classes .remove({charId});
CustomBuffs .remove({charId});
DamageMultipliers.remove({charId});
Effects .remove({charId});
Experiences .remove({charId});
Features .remove({charId});
Notes .remove({charId});
Proficiencies .remove({charId});
Skills .remove({charId});
SpellLists .remove({charId});
Items .remove({charId});
Containers .remove({charId});
});
Characters.after.update(function(userId, doc, fieldNames, modifier, options) {
if (_.contains(fieldNames, "name")){

View File

@@ -27,9 +27,11 @@ Schemas.Skill = new SimpleSchema({
allowedValues: [
"skill",
"save",
"stat",
"tool",
"weapon",
"language",
"utility", //not displayed anywhere
],
},
value: {