Began refactoring character computations

This commit is contained in:
Stefan Zermatten
2020-03-16 17:28:53 +02:00
parent 5ed8e08993
commit 1a0c2bca78
6 changed files with 420 additions and 469 deletions

View File

@@ -27,6 +27,10 @@ let CreaturePropertySchema = new SimpleSchema({
'tags.$': {
type: String,
},
disabled: {
type: Boolean,
optional: true,
},
});
for (let key in propertySchemasIndex){

View File

@@ -7,7 +7,7 @@ import { assertEditPermission } from '/imports/api/creature/creaturePermissions.
import Creatures from "/imports/api/creature/Creatures.js";
import CreatureProperties from "/imports/api/creature/CreatureProperties.js";
import * as math from 'mathjs';
import parser from '/imports/parser/parser.js';
import { includes } from 'lodash';
export const recomputeCreature = new ValidatedMethod({
@@ -27,380 +27,278 @@ export const recomputeCreature = new ValidatedMethod({
});
/**
* This function is the heart of DiceCloud. It recomputes a creature's stats,
* distilling down effects and proficiencies into the final stats that make up
* a creature.
*
* Essentially this is a depth first tree traversal algorithm that computes
* stats' dependencies before computing stats themselves, while detecting
* dependency loops.
*
* At the moment it makes no effort to limit recomputation to just what was
* changed.
*
* Attempting to implement dependency management to limit recomputation to just
* change affected stats should only happen as a last resort, when this function
* can no longer be performed more efficiently, and server resources can not be
* expanded to meet demand.
*
* A brief overview:
* - Fetch the stats of the creature and add them to
* an object for quick lookup
* - Fetch the effects and proficiencies which apply to each stat and store them with the stat
* - Fetch the class levels and store them as well
* - Mark each stat and effect as uncomputed
* - Iterate over each stat in order and compute it
* - If the stat is already computed, skip it
* - If the stat is busy being computed, we are in a dependency loop, make it NaN and mark computed
* - Mark the stat as busy computing
* - Iterate over each effect which applies to the attribute
* - If the effect is not computed compute it
* - If the effect relies on another attribute, get its computed value
* - Recurse if that attribute is uncomputed
* - apply the effect to the attribute
* - Conglomerate all the effects to compute the final stat values
* - Mark the stat as computed
* - Write the computed results back to the database
*
* @param {String} charId the Id of the creature to compute
* @returns {Object} An in-memory description of the character as
* computed and written to the database
*/
export function recomputeCreatureById(charId){
let char = buildCreature(charId);
/**
* This function is the heart of DiceCloud. It recomputes a creature's stats,
* distilling down effects and proficiencies into the final stats that make up
* a creature.
*
* Essentially this is a depth first tree traversal algorithm that computes
* stats' dependencies before computing stats themselves, while detecting
* dependency loops.
*
* At the moment it makes no effort to limit recomputation to just what was
* changed.
*
* Attempting to implement dependency management to limit recomputation to just
* change affected stats should only happen as a last resort, when this function
* can no longer be performed more efficiently, and server resources can not be
* expanded to meet demand.
*
* A brief overview:
* - Fetch the stats of the creature and add them to
* an object for quick lookup
* - Fetch the effects and proficiencies which apply to each stat and store them with the stat
* - Fetch the class levels and store them as well
* - Mark each stat and effect as uncomputed
* - Iterate over each stat in order and compute it
* - If the stat is already computed, skip it
* - If the stat is busy being computed, we are in a dependency loop, make it NaN and mark computed
* - Mark the stat as busy computing
* - Iterate over each effect which applies to the attribute
* - If the effect is not computed compute it
* - If the effect relies on another attribute, get its computed value
* - Recurse if that attribute is uncomputed
* - apply the effect to the attribute
* - Conglomerate all the effects to compute the final stat values
* - Mark the stat as computed
* - Write the computed results back to the database
*/
export function recomputeCreatureById(creatureId){
let char = buildCreature(creatureId);
char = computeCreature(char);
writeCreature(char);
return char;
}
/**
* Write the in-memory creature to the database docs
* This could be optimized to only write changed fields to the database
*
* @param {Object} char in-memory char object
* @returns {undefined}
*/
function writeCreature(char) {
writeAttributes(char);
writeSkills(char);
writeDamageMultipliers(char);
writeEffects(char);
writeCreatureDoc(char);
}
function writeCreatureDoc(char) {
// Store all the variables, using the same priority as computation evaluation
let variables = {};
for (let key in char.variables){
variables[key] = char.variables[key].result;
}
// Write the creature
Creatures.update(char.id, {$set: {level: char.level, variables}});
}
/*
* Write all the attributes from the in-memory char object to the Attirbute docs
*/
function writeAttributes(char) {
let bulkWriteOps = _.map(char.atts, (att, variableName) => {
let op = {
updateMany: {
filter: {'ancestors.id': char.id, variableName},
update: {'$set': {
value: att.result,
}},
}
};
if (typeof att.mod === 'number'){
op.updateMany.update.$set.mod = att.mod;
} else {
op.updateMany.update.$unset = {mod: 1};
}
return op;
});
if (!bulkWriteOps.length) return;
if (Meteor.isServer){
let result = CreatureProperties.rawCollection().bulkWrite(bulkWriteOps, {ordered : false}, function(e, r){
if (e) console.error(e);
});
} else {
_.each(bulkWriteOps, op => {
CreatureProperties.update(op.updateMany.filter, op.updateMany.update, {
multi: true,
selector: {type: 'attribute'}
});
});
}
}
function writeEffects(char){
let bulkWriteOps = _.map(char.computedEffects, effect => ({
updateOne: {
filter: {_id: effect._id},
update: {$set: {
result: effect.result,
}},
},
}));
if (!bulkWriteOps.length) return;
if (Meteor.isServer){
let result = CreatureProperties.rawCollection().bulkWrite(bulkWriteOps, {ordered : false}, function(e, r){
if (e) console.error(e);
});
} else {
_.each(bulkWriteOps, op => {
CreatureProperties.update(op.updateOne.filter, op.updateOne.update, {selector: {type: 'effect'}});
});
}
}
/**
* Write all the Creature Properties from the in-memory char object to the
* properties docs
*
* @param {type} char description
* @returns {type} description
*/
function writeSkills(char) {
let bulkWriteOps = _.map(char.skills, (skill, variableName) => {
let op = {
updateMany: {
filter: {'ancestors.id': char.id, variableName},
update: {$set: {
value: skill.result,
abilityMod: skill.abilityMod,
advantage: skill.advantage,
passiveBonus: skill.passiveAdd,
proficiency: skill.proficiency,
conditionalBenefits: skill.conditional,
fail: skill.fail,
}},
}
};
return op;
});
if (!bulkWriteOps.length) return;
if (Meteor.isServer){
let result = CreatureProperties.rawCollection().bulkWrite( bulkWriteOps, {ordered : false}, function(e, r){
if (e) console.error(e);
});
} else {
_.each(bulkWriteOps, op => {
CreatureProperties.update(op.updateMany.filter, op.updateMany.update, {
multi: true,
selector: {type: 'skill'},
});
});
}
}
/**
* Write all the damange multipliers from the in-memory char object to the docs
*
* @param {type} char description
* @returns {type} description
*/
function writeDamageMultipliers(char) {
let bulkWriteOps = _.map(char.dms, (dm, variableName) => {
let op = {
updateMany: {
filter: {'ancestors.id': char.id, variableName},
update: {$set: {
value: dm.result,
}},
}
};
return op;
});
if (!bulkWriteOps.length) return;
if (Meteor.isServer){
let result = CreatureProperties.rawCollection().bulkWrite( bulkWriteOps, {ordered : false}, function(e, r){
if (e) console.error(e);
});
} else {
_.each(bulkWriteOps, op => {
CreatureProperties.update(op.updateMany.filter, op.updateMany.update, {
multi: true,
selector: {type: 'damageMultiplier'},
});
});
}
}
/**
* Get the creature's data from the database and build an in-memory model that
* can be computed.
*
* @param {type} charId description
* @returns {type} description
*/
function buildCreature(charId){
// Load creature details into memory from database
function buildCreature(creatureId){
let char = {
id: charId,
atts: {},
skills: {},
dms: {},
classes: {},
variables: {},
otherEffects: [],
id: creatureId,
stats: {},
skillsByAbility: {},
unassignedEffects: [],
computedEffects: [],
level: 0,
};
// Fetch the properties of the creature and add them to the char object for
// quicker lookup
CreatureProperties.find({'ancestors.id': charId}).forEach(prop => {
// Get all relevant properties and store them on the in-memory creature
getCreatureProps(creatureId).filter(prop => {
const key = prop.variableName;
// Attributes
if (prop.type === 'attribute'){
char.atts[key] = {
computed: false,
busyComputing: false,
type: "attribute",
attributeType: prop.attributeType,
base: prop.baseValue || 0,
decimal: prop.decimal,
result: 0,
mod: 0, // The resulting modifier if this is an ability
add: 0,
mul: 1,
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY,
advantage: 0,
disadvantage: 0,
passiveAdd: 0,
fail: 0,
effects: [],
};
char.variables[key] = char.atts[key];
}
//Skill
else if (prop.type === 'skill'){
if (!char.skills[key]){
char.skills[key] = {
computed: false,
busyComputing: false,
type: "skill",
ability: prop.ability,
base: prop.baseValue,
result: 0, // For skills the result is the skillMod
proficiency: prop.baseProficiency || 0,
add: 0,
mul: 1,
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY,
advantage: 0,
disadvantage: 0,
passiveAdd: 0,
fail: 0,
conditional: 0,
effects: [],
proficiencies: [],
};
if (!char.variables[key]){
char.variables[key] = char.skills[key];
switch(prop.type){
case 'attribute':
case 'skill':
let skill = statObject(prop);
// Store the skill as a stat on the in-memory creature
if (!char.stats[key]) char.stats[key] = skill;
// Store a reference to the skill under the relevant ability as well
if (skill.ability){
if (!char.skillsByAbility[skill.ability]){
char.skillsByAbility[skill.ability] = [];
}
char.skillsByAbility[skill.ability].push(skill);
}
}
return false;
case 'damageMultiplier':
if (!char.stats[key]) char.stats[key] = damageMultiplierObject();
return false;
default:
return true;
}
// Damage multipliers
else if (prop.type === 'damageMultiplier'){
if (!char.dms[key]){
char.dms[key] = {
computed: false,
busyComputing: false,
type: "damageMultiplier",
result: 0,
immunityCount: 0,
ressistanceCount: 0,
vulnerabilityCount: 0,
effects: [],
};
if (!char.variables[key]){
char.variables[key] = char.dms[key];
}).forEach(prop => {
// In a second pass through the props, assign effects and proficiencies to
// their matched attributes and skills
switch(prop.type){
case 'effect':
let effect = effectObject(prop);
// Assign the effect to all the stats it directly targets
let targets = getEffectTargets(char, effect);
targets.forEach(target =>
target.effects.push(effect)
);
if (!targets.length){
char.unassignedEffects.push(effect);
}
break;
case 'proficiency':
let proficiency = proficiencyObject(prop);
getProficiencyTargets(char, proficiency).forEach(target =>
target.proficiencies.push(proficiency)
);
break;
}
}
// Classes
//TODO
// Effects
else if (prop.type === 'effect'){
for (let stat of prop.stats){
let storedEffect = {
_id: prop._id,
computed: false,
result: 0,
operation: prop.operation,
calculation: prop.calculation,
};
if (char.atts[stat]) {
char.atts[stat].effects.push(storedEffect);
} else if (char.skills[stat]) {
char.skills[stat].effects.push(storedEffect);
} else if (char.dms[stat]) {
char.dms[stat].effects.push(storedEffect);
} else {
char.otherEffects.push(storedEffect);
}
}
}
// Proficiencies
else if (prop.type === 'proficiency'){
if (char.skills[prop.skill]) {
char.skills[prop.skill].proficiencies.push(proficiency);
}
}
// Add direct properties from creature to variable list
const fields = { xp: 1, weightCarried: 1};
const creature = Creatures.findOne(charId, {fields});
for (let key in fields){
if (!char.variables[key]){
char.variables[key] = {
result: creature[key] || 0,
type: 'creatureProperty',
computed: true,
};
}
}
});
// Add direct properties from creature to variable list
const fields = { xp: 1, weightCarried: 1, level: 1};
const creature = Creatures.findOne(creatureId, {fields});
for (let key in fields){
if (!char.stats[key]){
char.stats[key] = {
computed: true,
type: 'creatureProperty',
result: creature[key] || 0,
};
}
}
return char;
}
/**
* Compute the creature's stats in-place, returns the same char object
* @param {type} char description
* @returns {type} description
*/
export function computeCreature(char){
// Iterate over each stat in order and compute it
let statName;
for (statName in char.atts){
let stat = char.atts[statName];
function getCreatureProps(creatureId){
// First get ids of disabled properties and unequiped items
let disabledAncestorIds = CreatureProperties.find({
'ancestors.id': creatureId,
$or: [
{disabled: true},
{equipped: false},
],
}, {
fields: {_id: 1},
}).map(prop => prop._id);
// Then get the ids of creatures that are children of this creature
// to isolate their decendent properties from this calculation
Creatures.find({
'ancestors.id': creatureId,
}, {
fields: {_id: 1},
}).forEach(prop => {
disabledAncestorIds.push(prop._id);
});
// Get all the properties that aren't from the excluded decendents
return CreatureProperties.find({
'ancestors.id': {
$eq: creatureId,
$nin: disabledAncestorIds,
},
type: {$in: [
'attribute',
'skill',
'damageMultiplier',
'effect',
'proficiency',
]},
}).fetch();
}
function getProficiencyTargets(char, proficiency){
let extraTargets = [];
if (!proficiency.stats) return [];
let targets = proficiency.stats.map(targetStat => {
let target = char[targetStat];
// Proficiencies targeting ability scores apply to 'skill' and check' skills
// based on that ability as well
if (
target &&
target.type === 'attribute' &&
target.attributeType === 'ability' &&
char.skillsByAbility[targetStat]
) {
extraTargets.push(
...char.skillsByAbility[targetStat].filter(skill =>
includes(['skill', 'check'], skill.skillType)
)
);
}
return target;
}).filter(target => !!target);
return targets.concat(extraTargets);
}
function getEffectTargets(char, effect){
const skillOperations = [
'advantage',
'disadvantage',
'passiveAdd',
'fail',
'conditional',
'rollBonus',
];
let extraTargets = [];
if (!effect.stats) return [];
let targets = effect.stats.map(targetStat => {
let target = char[targetStat];
// Certain effects targeting ability scores apply to skills
// based on that ability as well
if (
includes(skillOperations, effect.operation) &&
target &&
target.type === 'attribute' &&
target.attributeType === 'ability' &&
char.skillsByAbility[targetStat]
) {
extraTargets.push(...char.skillsByAbility[targetStat]);
}
return target;
});
return targets.concat(extraTargets);
}
function statObject(prop){
return {
computed: false,
busyComputing: false,
type: prop.type,
attributeType: prop.attributeType,
skillType: prop.skillType,
ability: prop.ability,
base: prop.baseValue || 0,
proficiency: prop.baseProficiency || 0,
decimal: prop.decimal,
result: 0,
mod: 0, // The resulting modifier if this is an ability
add: 0,
mul: 1,
min: Number.NEGATIVE_INFINITY,
max: Number.POSITIVE_INFINITY,
advantage: 0,
disadvantage: 0,
passiveAdd: 0,
fail: 0,
conditional: 0,
rollBonuses: 0,
effects: [],
proficiencies: [],
};
}
function damageMultiplierObject(){
return {
computed: false,
busyComputing: false,
type: "damageMultiplier",
result: 0,
immunityCount: 0,
ressistanceCount: 0,
vulnerabilityCount: 0,
effects: [],
};
};
function effectObject(prop){
return {
_id: prop._id,
computed: false,
result: 0,
operation: prop.operation,
calculation: prop.calculation,
};
}
function proficiencyObject(prop){
return {
value: prop.value,
};
}
function computeCreature(char){
for (let statName in char.stats){
let stat = char.stats[statName];
computeStat (stat, char);
}
for (statName in char.skills){
let stat = char.skills[statName];
computeStat (stat, char);
}
for (statName in char.dms){
let stat = char.dms[statName];
computeStat (stat, char);
}
for (let effect of char.otherEffects){
for (let effect of char.unassignedEffects){
computeEffect(effect, char);
}
return char;
}
/**
* Compute a single stat on a creature
*
* @param {type} stat description
* @param {type} char description
* @returns {type} description
*/
function computeStat(stat, char){
// If the stat is already computed, skip it
@@ -431,22 +329,22 @@ function computeStat(stat, char){
stat.busyComputing = false;
}
/**
* Compute a the result of a single effect
*/
/**
* Compute a the result of a single effect
*/
function computeEffect(effect, char){
if (effect.computed) return;
if (_.isFinite(effect.calculation)){
effect.result = +effect.calculation;
} else if(effect.operation === "conditional"){
effect.result = effect.calculation;
} else if(_.contains(["advantage", "disadvantage", "fail"], effect.operation)){
effect.result = 1;
} else {
effect.result = evaluateCalculation(effect.calculation, char);
}
effect.computed = true;
char.computedEffects.push(effect);
if (effect.computed) return;
if (_.isFinite(effect.calculation)){
effect.result = +effect.calculation;
} else if(effect.operation === "conditional" || effect.operation === "rollBonuses"){
effect.result = effect.calculation;
} else if(_.contains(["advantage", "disadvantage", "fail"], effect.operation)){
effect.result = 1;
} else {
effect.result = evaluateCalculation(effect.calculation, char);
}
effect.computed = true;
char.computedEffects.push(effect);
}
/**
@@ -497,6 +395,10 @@ function applyEffect(effect, stat){
// Sum number of conditionals
stat.conditional++;
break;
case "rollBonus":
// Sum number of roll bonuses
stat.rollBonus++;
break;
}
}
@@ -530,29 +432,34 @@ function combineAttribute(stat, char){
* Combine skills results into final values
*/
function combineSkill(stat, char){
// Skills are based on some ability Modifier
let abilityMod = 0;
let ability = char.stats[stat.ability]
if (stat.ability && ability){
if (!ability.computed){
computeStat(ability, char);
}
stat.abilityMod = ability.mod;
}
// Combine all the child proficiencies
for (let i in stat.proficiencies){
let prof = stat.proficiencies[i];
if (prof.value > stat.proficiency) stat.proficiency = prof.value;
}
// Get the character's proficiency bonus to apply
let profBonus;
if (char.skills.proficiencyBonus){
if (!char.skills.proficiencyBonus.computed){
computeStat(char.skills.proficiencyBonus, char);
if (char.stats.proficiencyBonus){
if (!char.stats.proficiencyBonus.computed){
computeStat(char.stats.proficiencyBonus, char);
}
profBonus = char.skills.proficiencyBonus.result;
profBonus = char.stats.proficiencyBonus.result;
} else {
profBonus = Math.floor(char.level / 4 + 1.75);
}
// Multiply the proficiency bonus by the actual proficiency
profBonus *= stat.proficiency;
// Skills are based on some ability Modifier
stat.abilityMod = 0;
if (stat.ability && char.atts[stat.ability]){
if (!char.atts[stat.ability].computed){
computeStat(char.atts[stat.ability], char);
}
stat.abilityMod = char.atts[stat.ability].mod;
}
stat.result = (stat.abilityMod + profBonus + stat.add) * stat.mul;
// Combine everything to get the final result
stat.result = (abilityMod + profBonus + stat.add) * stat.mul;
if (stat.result < stat.min) stat.result = stat.min;
if (stat.result > stat.max) stat.result = stat.max;
stat.result = Math.floor(stat.result);
@@ -562,7 +469,7 @@ function combineSkill(stat, char){
/**
* Combine damageMultiplier's results into final values
*/
function combineDamageMultiplier(stat, char){
function combineDamageMultiplier(stat){
if (stat.immunityCount) return 0;
if (stat.ressistanceCount && !stat.vulnerabilityCount){
stat.result = 0.5;
@@ -577,7 +484,7 @@ function combineDamageMultiplier(stat, char){
* Get the value of a key, compute it if necessary
*/
function getComputedValueOfKey(sub, char){
const stat = char.variables[sub];
const stat = char.stats[sub];
if (!stat) return null;
if (!stat.computed){
computeStat(stat, char);
@@ -617,75 +524,100 @@ function evaluateCalculation(string, char){
}
}
/**
* recompute a character's XP from a given id
function writeCreature(char) {
//TODO these functions don't filter the stats before trying to write
writeAttributes(char);
writeSkills(char);
writeDamageMultipliers(char);
writeEffects(char);
}
/*
* Write all the attributes from the in-memory char object to the Attirbute docs
*/
export const recomputeCreatureXP = new ValidatedMethod({
name: "Creatures.methods.recomputeCreatureXP",
validate: new SimpleSchema({
charId: { type: String }
}).validator(),
run({charId}) {
assertEditPermission(charId, this.userId);
var xp = 0;
Experiences.find(
{charId: charId},
{fields: {value: 1}}
).forEach(function(e){
xp += e.value;
});
Creatures.update(charId, {$set: {xp}});
return xp;
},
});
/**
* Recompute a character's weight carried from a given id
*/
export const recomputeCreatureWeightCarried = new ValidatedMethod({
name: "Creature.methods.recomputeCreatureWeightCarried",
validate: new SimpleSchema({
charId: { type: String }
}).validator(),
run({charId}){
assertEditPermission(charId, this.userId);
var weightCarried = 0;
// store a dictionary of carried containers
var carriedContainers = {};
Containers.find(
{
charId,
isCarried: true,
},
{ fields: {
isCarried: 1,
weight: 1,
}}
).forEach(container => {
carriedContainers[container._id] = true;
weightCarried += container.weight;
});
Items.find(
{
charId,
},
{ fields: {
weight: 1,
parent: 1,
}}
).forEach(item => {
// if the item is carried/equiped or in a carried container, add its weight
if (parent.id === charId || carriedContainers[parent.id]){
weightCarried += item.weight;
function writeAttributes(char) {
let bulkWriteOps = _.map(char.atts, (att, variableName) => {
let op = {
updateMany: {
filter: {'ancestors.id': char.id, variableName},
update: {'$set': {
value: att.result,
rollBonuses: skill.rollBonus,
}},
}
});
};
if (typeof att.mod === 'number'){
op.updateMany.update.$set.mod = att.mod;
} else {
op.updateMany.update.$unset = {mod: 1};
}
return op;
});
bulkWriteProperties({bulkWriteOps, selectorType: 'attribute'});
}
Creatures.update(charId, {$set: {weightCarried}});
return weightCarried;
function writeSkills(char) {
let bulkWriteOps = _.map(char.skills, (skill, variableName) => {
let op = {
updateMany: {
filter: {'ancestors.id': char.id, variableName},
update: {$set: {
value: skill.result,
abilityMod: skill.abilityMod,
advantage: skill.advantage,
passiveBonus: skill.passiveAdd,
proficiency: skill.proficiency,
conditionalBenefits: skill.conditional,
rollBonuses: skill.rollBonus,
fail: skill.fail,
}},
}
};
return op;
});
bulkWriteProperties({bulkWriteOps, selectorType: 'skill'});
}
function writeDamageMultipliers(char) {
let bulkWriteOps = _.map(char.dms, (dm, variableName) => {
let op = {
updateMany: {
filter: {'ancestors.id': char.id, variableName},
update: {$set: {
value: dm.result,
}},
}
};
return op;
});
bulkWriteProperties({bulkWriteOps, selectorType: 'damageMultiplier'});
}
function writeEffects(char){
let bulkWriteOps = _.map(char.computedEffects, effect => ({
updateOne: {
filter: {_id: effect._id},
update: {$set: {
result: effect.result,
}},
},
}));
if (!bulkWriteOps.length) return;
bulkWriteProperties({bulkWriteOps, selectorType: 'effect'});
}
function bulkWriteProperties({bulkWriteOps, selectorType}){
if (!bulkWriteOps.length) return;
if (Meteor.isServer){
CreatureProperties.rawCollection().bulkWrite(bulkWriteOps, {ordered : false}, function(e){
if (e) console.error(e);
});
} else {
_.each(bulkWriteOps, op => {
CreatureProperties.update(op.updateMany.filter, op.updateMany.update, {
multi: true,
selector: {type: selectorType}
});
});
}
});
}

View File

@@ -23,6 +23,7 @@ let EffectSchema = new SimpleSchema({
'passiveAdd',
'fail',
'conditional',
'rollBonus',
],
},
calculation: {

View File

@@ -55,7 +55,7 @@ ItemSchema = new SimpleSchema({
// Unequipped items shouldn't affect creature stats
equipped: {
type: Boolean,
optional: true,
defaultValue: false,
},
});

View File

@@ -1,8 +1,16 @@
import SimpleSchema from 'simpl-schema';
let ProficiencySchema = new SimpleSchema({
// The variableName of the skill to apply this to
skill: {
name: {
type: String,
optional: true,
},
// The variableNames of the skills, tags, or attributes to apply proficiency to
stats: {
type: Array,
defaultValue: [],
},
'stats.$': {
type: String,
optional: true,
},

View File

@@ -1,23 +1,29 @@
<template lang="html">
<div class="layout row wrap justify-start proficiency-form">
<div>
<text-field
label="Skill"
class="mr-2"
append-icon="arrow_drop_down"
item-text="name"
item-value="skill"
style="flex-basis: 300px;"
:menu-props="{transition: 'slide-y-transition', lazy: true}"
:value="model.skill"
:items="undefined"
@change="(value, ack) => $emit('change', {path: ['skill'], value, ack})"
/>
<proficiency-select
label="Proficiency"
style="flex-basis: 300px;"
:value="model.value"
@change="(value, ack) => $emit('change', {path: ['value'], value, ack})"
label="Name"
:value="model.name"
@change="(value, ack) => $emit('change', {path: ['name'], value, ack})"
:error-messages="errors.name"
:debounce-time="debounceTime"
/>
<div class="layout row wrap justify-start proficiency-form">
<text-field
label="Skill"
class="mr-2"
:value="model.stats[0]"
:items="stats"
:error-messages="errors.stats"
:debounce-time="debounceTime"
@change="(value, ack) => $emit('change', {path: ['stats'], value: [value], ack})"
/>
<proficiency-select
label="Proficiency"
style="flex-basis: 300px;"
:value="model.value"
@change="(value, ack) => $emit('change', {path: ['value'], value, ack})"
/>
</div>
</div>
</template>