Compare commits

...

47 Commits

Author SHA1 Message Date
Stefan Zermatten
574f8373e7 Fixed crash when indexing a non-array node, added more array node errors 2021-03-01 14:47:46 +02:00
Stefan Zermatten
a7ecdecec1 Prevented contextual variables #type from being written to creature variable list 2021-03-01 14:22:03 +02:00
Stefan Zermatten
0aa59a4bfc Fixed creature not recomputing correctly when weight carried changes 2021-03-01 14:15:21 +02:00
Stefan Zermatten
8f0ff3245e Fixed containers still carrying their own weight if their contents are weightless and they aren't carried 2021-03-01 14:15:01 +02:00
Stefan Zermatten
9a2d10b7ed Fixed new library button hiding and not coming back 2021-03-01 14:08:12 +02:00
Stefan Zermatten
a8aa1923a8 Fixed spells having a stray deativatedBySelf flag 2021-03-01 14:01:34 +02:00
Stefan Zermatten
57fa162c89 Fixed stray errors from unepexted types 2021-03-01 13:37:19 +02:00
Stefan Zermatten
4d548c901c Ensured property exists before attempting to damage it 2021-03-01 13:32:46 +02:00
Stefan Zermatten
a97be2f93a Made constants work in calculations performed after recomputation 2021-03-01 13:27:48 +02:00
Stefan Zermatten
1276f872a0 Removed unused function 2021-03-01 12:11:22 +02:00
Stefan Zermatten
7daab97297 Made toggles function properly when nested under inactive properties and each other 2021-03-01 11:55:43 +02:00
Stefan Zermatten
2e3704d096 Prevented resources from writing unchanged data to the database 2021-03-01 11:42:50 +02:00
Stefan Zermatten
7283a27727 Constants should now respect toggles 2021-03-01 11:42:23 +02:00
Stefan Zermatten
3517636b8b Reworked toggles, again, to try and catch more edge cases. Made toggles set the inactive status of their property children in the compute step instead of the inactive denormalisation step 2021-03-01 11:41:59 +02:00
Stefan Zermatten
e617ef9b75 Merge branch 'version-2' into version-2-dev 2021-03-01 10:18:55 +02:00
Stefan Zermatten
cd45ae1442 Fixed buffs not recomputing correctly because of inactive properties not being activated 2021-03-01 10:07:24 +02:00
Stefan Zermatten
bcedd548c7 Fixed: If usesUsed was undefined, usesLeft of an action was NaN 2021-03-01 10:06:31 +02:00
Stefan Zermatten
dc53e38efe Libraries only fetch their data whene expanded 2021-02-27 10:49:10 +02:00
Stefan Zermatten
e381b3b24d Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2021-02-26 09:48:22 +02:00
Stefan Zermatten
111d971bc2 Added attacks and actions to stats tab quick insert 2021-02-26 09:48:18 +02:00
Stefan Zermatten
bf4ce4f9f7 Hotfix: Adding properties to the tree, type selection fixed 2021-02-25 19:43:17 +02:00
Stefan Zermatten
2a983b0a94 User accounts can now be deleted with some UI to prevent accidental deletion 2021-02-25 14:28:51 +02:00
Stefan Zermatten
a5460bba0b Added floating action button to add properties directly to the sheet 2021-02-25 12:37:32 +02:00
Stefan Zermatten
df361236f5 Hotfix: Containers total weight now showing correctly on inventory tab 2021-02-24 15:18:32 +02:00
Stefan Zermatten
e1d670fe9f Fixed: buffs 2021-02-24 15:05:53 +02:00
Stefan Zermatten
1e9f0515e5 Contents that are weightless are now summed and stored on the container 2021-02-24 14:22:52 +02:00
Stefan Zermatten
0404020335 Added weights and content weight to containers UI 2021-02-24 14:07:20 +02:00
Stefan Zermatten
c248d8f4a0 Weight carried, Net worth, and Attunement implemented and exposed in UI 2021-02-24 13:41:30 +02:00
Stefan Zermatten
8d95da8b7a Fixed a bug where certain base values would be strings instead of numbers in effect aggregators 2021-02-24 11:58:04 +02:00
Stefan Zermatten
e11ab39864 Added tableLookup function 2021-02-24 11:57:40 +02:00
Stefan Zermatten
331fcef9ad Fixed: Error message when focus grabbing element is missing on form 2021-02-24 10:06:25 +02:00
Stefan Zermatten
7e3bff9677 Show creature milestone level and xp if creature has both 2021-02-24 10:05:11 +02:00
Stefan Zermatten
1b650b26b6 Fixed: using creature stats like XP in calculations 2021-02-24 10:01:02 +02:00
Stefan Zermatten
5925605962 Fixed property edit buttons no longer get pushed by long property name 2021-02-24 09:52:51 +02:00
Stefan Zermatten
dee1265b69 Fixed: Inline calculations in libarries now display as expected 2021-02-24 09:46:52 +02:00
Stefan Zermatten
3d3ec3bcf2 Increaed number of slot fillers loaded by the slot fill dialog to 20 2021-02-24 09:23:55 +02:00
Stefan Zermatten
dce2c92516 Added attack roll bonus and dc to spell list. Use them in spells with #spellList.dcResult and #spellList.attackRollBonusResult 2021-02-23 15:21:20 +02:00
Stefan Zermatten
0fe2780983 Added property viewer for Toggle properties 2021-02-23 15:07:07 +02:00
Stefan Zermatten
e126cdd3cb Added property viewer for slot filler 2021-02-23 14:59:53 +02:00
Stefan Zermatten
d69ada0db4 Slot quantity is now a computed value, added property viewer for slots 2021-02-23 14:53:47 +02:00
Stefan Zermatten
858915b25b Added viewer for Saving Throw properties 2021-02-23 14:38:20 +02:00
Stefan Zermatten
d10a7eca14 Added viewer for Roll properties 2021-02-23 14:29:48 +02:00
Stefan Zermatten
671d17018c Added a viewer for Constant properties 2021-02-23 14:23:00 +02:00
Stefan Zermatten
f2883d320f Improved Attribute damage viewer 2021-02-23 13:59:26 +02:00
Stefan Zermatten
aad0c7249e Removed stray log to console 2021-02-23 12:47:34 +02:00
Stefan Zermatten
612fcca68c Only split properties accross targets if there are targets 2021-02-22 14:30:50 +02:00
Stefan Zermatten
12939c46de made saves walk children when not targeted at self 2021-02-22 14:28:38 +02:00
98 changed files with 1637 additions and 403 deletions

View File

@@ -92,8 +92,32 @@ let CreatureSchema = new SimpleSchema({
type: SimpleSchema.Integer,
defaultValue: 0,
},
// Sum of all weights of items and containers that are carried
'denormalizedStats.weightCarried': {
// Inventory
'denormalizedStats.weightTotal': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.weightEquipment': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.weightCarried': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.valueTotal': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.valueEquipment': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.valueCarried': {
type: Number,
defaultValue: 0,
},
'denormalizedStats.itemsAttuned': {
type: Number,
defaultValue: 0,
},

View File

@@ -13,42 +13,43 @@ export default function applyAdjustment({
...creature.variables,
...actionContext,
};
try {
var {result, errors} = evaluateString(prop.amount, scope, 'reduce');
if (typeof result !== 'number') {
log.content.push({
name: 'Attribute damage',
error: errors.join(', ') || 'Something went wrong',
});
}
} catch (e){
var {result, context} = evaluateString({
string: prop.amount,
scope,
fn: 'reduce'
});
context.errors.forEach(e => {
log.content.push({
name: 'Attribute damage',
error: e.toString(),
name: 'Attribute damage error',
error: e.message || e.toString(),
});
}
});
if (damageTargets) {
damageTargets.forEach(target => {
if (prop.target === 'each'){
result = evaluateString(prop.amount, scope, 'reduce');
({result} = evaluateString({
string: prop.amount,
scope,
fn: 'reduce'
}));
}
damagePropertiesByName.call({
creatureId: target._id,
variableName: prop.stat,
operation: prop.operation || 'increment',
value: result
value: result.value,
});
log.content.push({
name: 'Attribute damage',
resultPrefix: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''}`,
result: `${-result}`,
result: `${result.isNumber ? -result.value : result.toString()}`,
});
});
} else {
log.content.push({
name: 'Attribute damage',
resultPrefix: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''}`,
result: `${-result}`,
result: `${result.isNumber ? -result.value : result.toString()}`,
});
}
}

View File

@@ -15,49 +15,79 @@ export default function applyDamage({
...creature.variables,
...actionContext,
};
// Add the target's variables to the scope
if (targets.length === 1){
scope.target = targets[0].variables;
}
// Determine if the hit is critical
let criticalHit = !!(
actionContext.criticalHit &&
actionContext.criticalHit.value &&
prop.damageType !== 'healing' // Can't critically heal
);
// Double the damage rolls if the hit is critical
let context = new CompilationContext({
doubleRolls: criticalHit,
});
try {
var {result, errors} = evaluateString(prop.amount, scope, 'reduce', context);
if (typeof result !== 'number') {
log.content.push({
error: errors.join(', '),
});
}
} catch (e){
// Compute the roll the first time, logging any errors
var {result} = evaluateString({
string: prop.amount,
scope,
fn: 'reduce',
context
});
// If the result is an error bail out now
if (result.constructor.name === 'ErrorNode'){
log.content.push({
error: e.toString(),
name: 'Damage error',
error: result.toString(),
});
return;
}
// Memoise the damage suffix for the log
let suffix = (criticalHit ? ' critical ' : '') +
prop.damageType +
(prop.damageType !== 'healing' ? ' damage': '');
if (damageTargets && damageTargets.length) {
// Iterate through all the targets
damageTargets.forEach(target => {
let name = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// Reroll the damage if needed
if (prop.target === 'each'){
result = evaluateString(prop.amount, scope, 'reduce');
({result, context} = evaluateString({
string: prop.amount,
scope,
fn: 'reduce'
}));
}
// If the result is an error or not a number bail out now
if (result.constructor.name === 'ErrorNode' || !result.isNumber){
log.content.push({
name: 'Damage error',
error: result.toString(),
});
return;
}
// Deal the damage to the target
let damageDealt = dealDamage.call({
creatureId: target._id,
damageType: prop.damageType,
amount: result,
amount: result.value,
});
// Log the damage done
if (target._id === creature._id){
// Target is same as self, log damage as such
log.content.push({
name,
result: damageDealt,
details: suffix + 'to self',
details: suffix + ' to self',
});
} else {
log.content.push({
@@ -66,6 +96,7 @@ export default function applyDamage({
result: damageDealt,
details: suffix + `${target.name && ' to '}${target.name}`,
});
// Log the damage received on that creature's log as well
insertCreatureLog.call({
log: {
content: [{
@@ -80,9 +111,10 @@ export default function applyDamage({
}
});
} else {
// There are no targets, just log the result
log.content.push({
name: prop.damageType === 'healing' ? 'Healing' : 'Damage',
result,
result: result.toString(),
details: suffix,
});
}

View File

@@ -10,7 +10,7 @@ import applySave from '/imports/api/creature/actions/applySave.js';
function applyProperty(options){
let prop = options.prop;
if (prop.type === 'buff'){
// ignore only applied buffs
// ignore only applied buffs, don't apply them again
if (prop.applied === true){
return false;
}
@@ -40,7 +40,7 @@ function applyProperty(options){
break;
case 'buff':
applyBuff(options);
break;
return false;
case 'toggle':
return applyToggle(options);
case 'roll':
@@ -52,23 +52,24 @@ function applyProperty(options){
return true;
}
function applyPropertyAndWalkChildren({prop, child, targets, ...options}){
let shouldKeepWalking = applyProperty({ prop, targets, ...options });
function applyPropertyAndWalkChildren({prop, children, targets, ...options}){
let shouldKeepWalking = applyProperty({ prop, children, targets, ...options });
if (shouldKeepWalking){
applyProperties({ forest: child.children, targets, ...options,});
applyProperties({ forest: children, targets, ...options,});
}
}
export default function applyProperties({ forest, targets, ...options}){
forest.forEach(child => {
let prop = child.node;
if (shouldSplit(prop)){
forest.forEach(node => {
let prop = node.node;
let children = node.children;
if (shouldSplit(prop) && targets.length){
targets.forEach(target => {
let targets = [target]
applyPropertyAndWalkChildren({ targets, prop, child, ...options});
applyPropertyAndWalkChildren({ targets, prop, children, ...options});
});
} else {
applyPropertyAndWalkChildren({prop, child, targets, ...options});
applyPropertyAndWalkChildren({prop, children, targets, ...options});
}
});
}

View File

@@ -10,23 +10,17 @@ export default function applyRoll({
...creature.variables,
...actionContext,
};
try {
var {result, errors} = evaluateString(prop.roll, scope, 'reduce');
actionContext[prop.variableName] = result;
log.content.push({
name: prop.name,
resultPrefix: prop.variableName + ' = ' + prop.roll + ' = ',
result,
});
if (errors.length) {
log.content.push({
error: errors.join(', '),
});
}
} catch (e){
log.content.push({
error: e.toString(),
});
var {result} = evaluateString({
string: prop.roll,
scope,
fn: 'reduce'
});
if (result.isNumber){
actionContext[prop.variableName] = result.value;
}
log.content.push({
name: prop.name,
resultPrefix: prop.variableName + ' = ' + prop.roll + ' = ',
result: result.toString(),
});
}

View File

@@ -14,19 +14,17 @@ export default function applySave({
};
try {
// Calculate the DC
var {result, errors} = evaluateString(prop.dc, scope, 'reduce');
let dc = result;
var {result} = evaluateString({
string: prop.dc,
scope,
fn: 'reduce'
});
let dc = result.value;
log.content.push({
name: prop.name,
resultPrefix: ' DC ',
result,
result: result.toString(),
});
if (errors.length) {
log.content.push({
error: errors.join(', '),
});
return false;
}
if (prop.target === 'self'){
let save = CreaturesProperties.findOne({
'ancestors.id': creature._id,
@@ -69,6 +67,7 @@ export default function applySave({
return !saveSuccess;
} else {
// TODO
return true;
}
} catch (e){
log.content.push({

View File

@@ -13,23 +13,22 @@ export default function applyToggle({
if (Number.isFinite(+prop.condition)){
return !!+prop.condition;
}
try {
var {result, errors} = evaluateString(prop.condition, scope, 'reduce');
if (typeof result !== 'number' && typeof result !== 'boolean') {
log.content.push({
error: errors.join(', '),
});
return false;
}
var {result} = evaluateString({
string: prop.condition,
scope,
fn: 'reduce'
});
if (result.constructor.name === 'ErrorNode') {
log.content.push({
name: prop.name,
resultPrefix: prop.condition + ' = ',
result,
});
return !!result;
} catch (e){
log.content.push({
error: e.toString(),
name: 'Toggle error',
error: result.toString(),
});
return false;
}
log.content.push({
name: prop.name || 'Toggle',
resultPrefix: prop.condition + ' = ',
result: result.toString(),
});
return !!result.value;
}

View File

@@ -65,7 +65,7 @@ const castSpellWithSlot = new ValidatedMethod({
action: spell,
context: {slotLevel},
creature,
target,
targets: [target],
method: this,
});
// Note this only recomputes the top-level creature, not the nearest one

View File

@@ -9,6 +9,8 @@ import { assertEditPermission } from '/imports/api/creature/creaturePermissions.
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import { nodesToTree } from '/imports/api/parenting/parenting.js';
import applyProperties from '/imports/api/creature/actions/applyProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
const doAction = new ValidatedMethod({
name: 'creatureProperties.doAction',
@@ -43,9 +45,16 @@ const doAction = new ValidatedMethod({
});
doActionWork({action, creature, targets, method: this});
// The acting creature might have used ammo
recomputeInventory(creature._id);
// The action might add properties which need to be activated
recomputeInactiveProperties(creature._id);
// recompute creatures
recomputeCreatureByDoc(creature);
targets.forEach(target => {
recomputeInactiveProperties(target._id);
recomputeCreatureByDoc(target);
});
},

View File

@@ -61,7 +61,7 @@ export default function spendResources({prop, log}){
});
log.content.push({
name: 'Uses left',
result: prop.usesResult - prop.usesUsed - 1,
result: prop.usesResult - (prop.usesUsed || 0) - 1,
});
}

View File

@@ -4,8 +4,8 @@ export default function embedInlineCalculations(string, calculations){
if (!string) return '';
if (!calculations) return string;
let index = 0;
return string.replace(INLINE_CALCULATION_REGEX, () => {
return string.replace(INLINE_CALCULATION_REGEX, substring => {
let comp = calculations && calculations[index++];
return comp && comp.result ? comp.result : string;
return (comp && 'result' in comp) ? comp.result : substring;
});
}

View File

@@ -1,30 +1,67 @@
import { parse, CompilationContext } from '/imports/parser/parser.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
export default function evaluateString(string, scope, fn = 'compile', context){
let errors = [];
//TODO replace constants with their parsed node
export default function evaluateString({string, scope, fn = 'compile', context}){
if (!context){
context = new CompilationContext({});
}
if (!string){
errors.push('No string provided');
return {result: string, errors};
context.storeError('No string provided');
return {result: {value: string}, context};
}
if (!scope) errors.push('No scope provided');
if (!scope) context.storeError('No scope provided');
// Parse the string using mathjs
let node;
try {
node = parse(string);
} catch (e) {
errors.push(e);
return {result: string, errors};
}
if (!context){
context = new CompilationContext({});
context.storeError(e);
return {result: {value: string}, context};
}
node = replaceConstants({calc: node, context, scope});
let result = node[fn](scope, context);
if (result instanceof ConstantNode){
return {result: result.value, errors: context.errors}
} else {
return {result: result.toString(), errors: context.errors};
}
return {result, context};
}
// Replace constants in the calc with the right ParseNodes
function replaceConstants({calc, context, scope}){
let constFailed = [];
calc = calc.replaceNodes(node => {
if (!(node instanceof SymbolNode)) return;
let constant = scope[node.name];
// replace constants that aren't overridden by stats or disabled by a toggle
if (constant && constant.type === 'constant'){
// Fail if the constant has errors
if (constant.errors && constant.errors.length){
constFailed.push(node.name);
return;
}
let parsedConstantNode;
try {
parsedConstantNode = parse(constant.calculation);
} catch(e){
constFailed.push(node.name);
return;
}
if (!parsedConstantNode) constFailed.push(node.name);
return parsedConstantNode;
}
});
constFailed.forEach(name => {
context.storeError({
type: 'error',
message: `${name} is a constant property with parsing errors`
});
});
let failed = !!constFailed.length;
if (failed){
calc = new ErrorNode({error: 'Failed to replace constants'});
}
return calc;
}

View File

@@ -1,13 +0,0 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
// Strings can have computations in bracers like so: {computation}
export default function evalutateStringWithEmbeddedCalculations(string, scope){
console.warn('evalutateStringWithEmbeddedCalculations should be replaced with ' +
'fetching the result from the compuations on the property doc');
if (!string) return string;
// Compute everything inside bracers
return string.replace(/\{([^{}]*)\}/g, function(match, p1){
let {result} = evaluateString(p1, scope);
return result;
});
}

View File

@@ -256,7 +256,6 @@ const propDetailsByType = {
default(){
return {
toggleAncestors: [],
disabledByToggle: false,
};
},
toggle(){
@@ -264,7 +263,6 @@ const propDetailsByType = {
computed: false,
busyComputing: false,
toggleAncestors: [],
disabledByToggle: false,
};
},
attribute(){
@@ -273,7 +271,6 @@ const propDetailsByType = {
busyComputing: false,
effects: [],
toggleAncestors: [],
disabledByToggle: false,
idsOfSameName: [],
};
},
@@ -284,7 +281,6 @@ const propDetailsByType = {
effects: [],
proficiencies: [],
toggleAncestors: [],
disabledByToggle: false,
idsOfSameName: [],
};
},
@@ -293,26 +289,22 @@ const propDetailsByType = {
computed: false,
busyComputing: false,
toggleAncestors: [],
disabledByToggle: false,
};
},
classLevel(){
return {
computed: true,
toggleAncestors: [],
disabledByToggle: false,
};
},
proficiency(){
return {
toggleAncestors: [],
disabledByToggle: false,
};
},
denormalizedStat(){
return {
toggleAncestors: [],
disabledByToggle: false,
};
}
}

View File

@@ -14,7 +14,7 @@ export default class EffectAggregator{
prop: stat,
memo
});
this.statBaseValue = result.value;
this.statBaseValue = +result.value;
stat.dependencies = union(
stat.dependencies,
dependencies,

View File

@@ -2,6 +2,11 @@ import computeToggle from '/imports/api/creature/computation/engine/computeToggl
import { union } from 'lodash';
export default function applyToggles(prop, memo){
// If it used to be inactive delete those fields
if (prop.inactive) prop.inactive = undefined;
if (prop.deactivatedByAncestor) prop.deactivatedByAncestor = undefined;
if (prop.deactivatedByToggle) prop.deactivatedByToggle = undefined;
// Iterate through the toggle ancestors from oldest to nearest
prop.computationDetails.toggleAncestors.forEach(toggleId => {
let toggle = memo.togglesById[toggleId];
computeToggle(toggle, memo);
@@ -10,8 +15,11 @@ export default function applyToggles(prop, memo){
[toggle._id],
toggle.dependencies,
);
// Deactivate if the toggle is false
if (!toggle.toggleResult){
prop.computationDetails.disabledByToggle = true;
prop.inactive = true;
prop.deactivatedByAncestor = true;
prop.deactivatedByToggle = true;
}
});
}

View File

@@ -96,7 +96,7 @@ function combineSkill(stat, aggregator, memo){
let prof = stat.computationDetails.proficiencies[i];
applyToggles(prof, memo);
if (
!prof.computationDetails.disabledByToggle &&
!prof.deactivatedByToggle &&
prof.value > stat.proficiency
){
stat.proficiency = prof.value;

View File

@@ -0,0 +1,6 @@
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
export default function computeConstant(constant, memo){
// Apply any toggles
applyToggles(constant, memo);
}

View File

@@ -14,16 +14,22 @@ export default function computeEndStepProperty(prop, memo){
break;
case 'attack':
computeAction(prop, memo);
computeAttack(prop, memo);
computePropertyField(prop, memo, 'rollBonus');
break;
case 'savingThrow':
computeSavingThrow(prop, memo);
computePropertyField(prop, memo, 'dc');
break;
case 'spellList':
computeSpellList(prop, memo);
computePropertyField(prop, memo, 'maxPrepared');
computePropertyField(prop, memo, 'attackRollBonus');
computePropertyField(prop, memo, 'dc');
break;
case 'propertySlot':
computeSlot(prop, memo);
computePropertyField(prop, memo, 'quantityExpected');
computePropertyField(prop, memo, 'slotCondition');
break;
case 'roll':
computePropertyField(prop, memo, 'roll', 'compile');
break;
}
}
@@ -69,26 +75,33 @@ function computeAction(prop, memo){
});
// Items consumed
prop.resources.itemsConsumed.forEach((itemConsumed, i) => {
let item = itemConsumed.itemId && memo.equipmentById[itemConsumed.itemId];
prop.resources.itemsConsumed[i].itemId = item && item._id;
let available = item && item.quantity || 0;
let item = itemConsumed.itemId ?
memo.equipmentById[itemConsumed.itemId] :
undefined;
let available = item ? item.quantity : 0;
prop.resources.itemsConsumed[i].available = available;
let name = item && item.name;
if (item && item.quantity !== 1 && item.plural){
name = item.plural;
}
prop.resources.itemsConsumed[i].itemName = name;
prop.resources.itemsConsumed[i].itemIcon = item && item.icon;
prop.resources.itemsConsumed[i].itemColor = item && item.color;
if (!item || available < itemConsumed.quantity){
prop.insufficientResources = true;
}
if (item){
prop.resources.itemsConsumed[i].itemId = item._id;
let name = item.name;
if (item.quantity !== 1 && item.plural){
name = item.plural;
}
if (name) prop.resources.itemsConsumed[i].itemName = name;
if (item.icon) prop.resources.itemsConsumed[i].itemIcon = item.icon;
if (item.color) prop.resources.itemsConsumed[i].itemColor = item.color;
prop.dependencies = union(
prop.dependencies,
[item._id],
item.dependencies
);
} else {
delete prop.resources.itemsConsumed[i].itemId;
delete prop.resources.itemsConsumed[i].itemName;
delete prop.resources.itemsConsumed[i].itemIcon;
delete prop.resources.itemsConsumed[i].itemColor;
}
});
}
@@ -111,19 +124,3 @@ function computePropertyField(prop, memo, fieldName, fn){
delete prop[`${fieldName}Errors`];
}
}
function computeAttack(prop, memo){
computePropertyField(prop, memo, 'rollBonus');
}
function computeSavingThrow(prop, memo){
computePropertyField(prop, memo, 'dc');
}
function computeSpellList(prop, memo){
computePropertyField(prop, memo, 'maxPrepared');
}
function computeSlot(prop, memo){
computePropertyField(prop, memo, 'slotCondition');
}

View File

@@ -5,8 +5,13 @@ import computeEffect from '/imports/api/creature/computation/engine/computeEffec
import computeToggle from '/imports/api/creature/computation/engine/computeToggle.js';
import computeEndStepProperty from '/imports/api/creature/computation/engine/computeEndStepProperty.js';
import computeInlineCalculations from '/imports/api/creature/computation/engine/computeInlineCalculations.js';
import computeConstant from '/imports/api/creature/computation/engine/computeConstant.js';
export default function computeMemo(memo){
// Compute all constants that could be used
forOwn(memo.constantsByVariableName, constant => {
computeConstant (constant, memo);
});
// Compute level
computeLevels(memo);
// Compute all stats, even if they are overriden

View File

@@ -22,7 +22,7 @@ export default function computeStat(stat, memo){
// Apply any toggles
applyToggles(stat, memo);
if (!stat.computationDetails.disabledByToggle){
if (!stat.deactivatedByToggle){
// Compute and aggregate all the effects
let aggregator = new EffectAggregator(stat, memo)
each(stat.computationDetails.effects, (effect) => {
@@ -37,7 +37,7 @@ export default function computeStat(stat, memo){
stat.dependencies,
effect.dependencies
)
if (!effect.computationDetails.disabledByToggle){
if (!effect.deactivatedByToggle){
aggregator.addEffect(effect);
}
});

View File

@@ -1,4 +1,5 @@
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
import { union } from 'lodash';
export default function computeToggle(toggle, memo){
@@ -16,6 +17,9 @@ export default function computeToggle(toggle, memo){
// Before doing any work, mark this toggle as busy
toggle.computationDetails.busyComputing = true;
// Apply any parent toggles
applyToggles(toggle, memo);
// Do work
delete toggle.errors;
if (toggle.enabled){
@@ -41,6 +45,11 @@ export default function computeToggle(toggle, memo){
toggle.errors = context.errors;
}
}
if (!toggle.toggleResult){
toggle.inactive = true;
toggle.deactivatedBySelf = true;
toggle.deactivatedByToggle = true;
}
toggle.computationDetails.computed = true;
toggle.computationDetails.busyComputing = false;
}

View File

@@ -21,6 +21,9 @@ export default function evaluateCalculation({
context,
dependencies,
};
if (typeof string !== 'string'){
string = string.toString();
}
// Parse the string
let calc;
try {
@@ -68,8 +71,8 @@ function replaceConstants({calc, memo, prop, dependencies, context}){
} else if (node.name === '#constant'){
constant = findAncestorByType({type: 'constant', prop, memo});
}
// replace constants that aren't overridden by stats
if (constant && !stat){
// replace constants that aren't overridden by stats or disabled by a toggle
if (constant && !constant.deactivatedByToggle && !stat){
dependencies = union(dependencies, [
constant._id,
...constant.dependencies
@@ -119,10 +122,14 @@ function computeSymbols({calc, memo, prop, dependencies}){
computeStat(stat, memo);
}
if (stat){
dependencies = union(dependencies, [
stat._id || node.name,
...stat.dependencies
]);
if (stat.dependencies){
dependencies = union(dependencies, [
stat._id || node.name,
...stat.dependencies
]);
} else {
dependencies = union(dependencies, [stat._id || node.name]);
}
}
}
});

View File

@@ -1,15 +1,6 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
export default function getComputationProperties(creatureId){
// find ids of all toggles that have conditions, even if they are inactive
let toggleIds = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'toggle',
removed: {$ne: true},
condition: { $exists: true },
}, {
fields: {_id: 1},
}).map(t => t._id);
// Find all the relevant properties
return CreatureProperties.find({
'ancestors.id': creatureId,
@@ -17,17 +8,15 @@ export default function getComputationProperties(creatureId){
$or: [
// All active properties
{inactive: {$ne: true}},
// All active and inactive toggles with conditions
// Same as {$in: toggleIds}, but should be slightly faster
{type: 'toggle', condition: { $exists: true }},
// All decendents of the above toggles
{'ancestors.id': {$in: toggleIds}},
// Unless they were deactivated because of a toggle
{deactivatedByToggle: true},
]
}, {
// Filter out fields never used by calculations
fields: {
icon: 0,
},
// Obey tree order
sort: {
order: 1,
}

View File

@@ -19,7 +19,14 @@ export default function writeAlteredProperties(memo){
ids.forEach(id => {
let op = undefined;
let original = memo.originalPropsById[id];
let keys = ['dependencies', ...schema.objectKeys()];
let keys = [
'dependencies',
'inactive',
'deactivatedBySelf',
'deactivatedByAncestor',
'deactivatedByToggle',
...schema.objectKeys(),
];
op = addChangedKeysToOp(op, keys, original, changed);
if (op){
bulkWriteOperations.push(op);

View File

@@ -4,36 +4,46 @@ import VERSION from '/imports/constants/VERSION.js';
export default function writeCreatureVariables(memo, creatureId, fullRecompute = true) {
const fields = [
'name',
'attributeType',
'baseValue',
'spellSlotLevelValue',
'damage',
'decimal',
'reset',
'resetMultiplier',
'value',
'currentValue',
'modifier',
'ability',
'skillType',
'baseProficiency',
'abilityMod',
'advantage',
'passiveBonus',
'proficiency',
'attributeType',
'baseProficiency',
'baseValue',
'calculation',
'conditionalBenefits',
'rollBonuses',
'currentValue',
'damage',
'decimal',
'fail',
'level',
'modifier',
'name',
'passiveBonus',
'proficiency',
'reset',
'resetMultiplier',
'rollBonuses',
'skillType',
'spellSlotLevelValue',
'type',
'value',
];
if (fullRecompute){
memo.creatureVariables = {};
forOwn(memo.statsByVariableName, (stat, variableName) => {
// Don't save context variables
if (variableName[0] === '#') return;
let condensedStat = pick(stat, fields);
memo.creatureVariables[variableName] = condensedStat;
});
forOwn(memo.constantsByVariableName, (stat, variableName) => {
let condensedStat = pick(stat, fields);
if (!memo.creatureVariables[variableName]){
memo.creatureVariables[variableName] = condensedStat;
}
});
Creatures.update(creatureId, {$set: {
variables: memo.creatureVariables,
computeVersion: VERSION,

View File

@@ -89,7 +89,6 @@ export function recomputeCreatureByDoc(creature){
writeCreatureVariables(computationMemo, creatureId);
recomputeDamageMultipliersById(creatureId);
recomputeSlotFullness(creatureId);
recomputeInactiveProperties(creatureId);
return computationMemo;
}

View File

@@ -50,6 +50,13 @@ let CreaturePropertySchema = new SimpleSchema({
optional: true,
index: 1,
},
// Denormalised flag if this property was made inactive because of a toggle
// calculation. Either an ancestor toggle calculation or its own.
deactivatedByToggle: {
type: Boolean,
optional: true,
index: 1,
},
// Denormalised list of all properties or creatures this property depends on
dependencies: {
type: Array,

View File

@@ -4,7 +4,8 @@ import SimpleSchema from 'simpl-schema';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { recomputePropertyDependencies } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
const adjustQuantity = new ValidatedMethod({
name: 'creatureProperties.adjustQuantity',
@@ -30,8 +31,10 @@ const adjustQuantity = new ValidatedMethod({
// Do work
adjustQuantityWork({property, operation, value});
// Changing quantity does not change dependencies, recompute deps
recomputePropertyDependencies(property);
// Changing quantity does not change dependencies, but recomputing the
// inventory changes many deps at once, so recompute fully
recomputeCreatureByDoc(rootCreature);
recomputeInventory(rootCreature._id);
},
});

View File

@@ -24,6 +24,9 @@ const damageProperty = new ValidatedMethod({
run({_id, operation, value}) {
// Check permissions
let property = CreatureProperties.findOne(_id);
if (!property) throw new Meteor.Error(
'Damage property failed', 'Property doesn\'t exist'
);
let rootCreature = getRootCreatureAncestor(property);
assertEditPermission(rootCreature, this.userId);
// Check if property can take damage
@@ -34,9 +37,10 @@ const damageProperty = new ValidatedMethod({
`Property of type "${property.type}" can't be damaged`
);
}
damagePropertyWork({property, operation, value});
let result = damagePropertyWork({property, operation, value});
// Dependencies can't be changed through damage, only recompute deps
recomputePropertyDependencies(property);
return result;
},
});

View File

@@ -4,6 +4,9 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import INVENTORY_TAGS from '/imports/constants/INVENTORY_TAGS.js';
export function getParentRefByTag(creatureId, tag){
@@ -49,7 +52,7 @@ const equipItem = new ValidatedMethod({
});
let tag = equipped ? INVENTORY_TAGS.equipment : INVENTORY_TAGS.carried;
let parentRef = getParentRefByTag(creature._id, tag);
// organizeDoc handles recompuation
organizeDoc.call({
docRef: {
id: _id,
@@ -57,7 +60,12 @@ const equipItem = new ValidatedMethod({
},
parentRef,
order: Number.MAX_SAFE_INTEGER,
skipRecompute: true,
});
recomputeInactiveProperties(creature._id);
recomputeInventory(creature._id);
recomputeCreatureByDoc(creature);
},
});

View File

@@ -6,6 +6,7 @@ import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js
import { reorderDocs } from '/imports/api/parenting/order.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
const insertProperty = new ValidatedMethod({
name: 'creatureProperties.insert',
@@ -35,6 +36,11 @@ export function insertPropertyWork({property, creature}){
});
// Inserting the active status of the property needs to be denormalised
recomputeInactiveProperties(creature._id);
// Recompute the inventory if it has changed
if (property.type === 'item' || property.type === 'container'){
recomputeInventory(creature._id);
}
// Inserting a creature property invalidates dependencies: full recompute
recomputeCreatureByDoc(creature);
return _id;

View File

@@ -15,6 +15,7 @@ import {
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
const insertPropertyFromLibraryNode = new ValidatedMethod({
name: 'creatureProperties.insertPropertyFromLibraryNode',
@@ -97,6 +98,8 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
// The library properties need to denormalise which of them are inactive
recomputeInactiveProperties(rootId);
// Some of the library properties may be items or containers
recomputeInventory(rootCreature._id);
// Inserting a creature property invalidates dependencies: full recompute
recomputeCreatureByDoc(rootCreature);
// Return the docId of the last property, the inserted root property

View File

@@ -7,6 +7,7 @@ import { restore } from '/imports/api/parenting/softRemove.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
const restoreProperty = new ValidatedMethod({
name: 'creatureProperties.restore',
@@ -27,6 +28,8 @@ const restoreProperty = new ValidatedMethod({
// Do work
restore({_id, collection: CreatureProperties});
// Items and containers might be restored
recomputeInventory(rootCreature._id);
// Parents active status may have changed while it was deleted
recomputeInactiveProperties(rootCreature._id);
// Changes dependency tree by restoring children

View File

@@ -6,6 +6,7 @@ import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js
import { softRemove } from '/imports/api/parenting/softRemove.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
const softRemoveProperty = new ValidatedMethod({
name: 'creatureProperties.softRemove',
@@ -26,6 +27,8 @@ const softRemoveProperty = new ValidatedMethod({
// Do work
softRemove({_id, collection: CreatureProperties});
// Potentially changes items and containers
recomputeInventory(rootCreature._id);
// Changes dependency tree by removing children
recomputeCreatureByDoc(rootCreature);
}

View File

@@ -3,8 +3,9 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
const updateCreatureProperty = new ValidatedMethod({
name: 'creatureProperties.update',
@@ -52,8 +53,14 @@ const updateCreatureProperty = new ValidatedMethod({
].includes(path[0])){
recomputeInactiveProperties(rootCreature._id);
}
if (property.type === 'item' || property.type === 'container'){
// Potentially changes items and containers
recomputeInventory(rootCreature._id);
}
// Updating a property is likely to change dependencies, do a full recompute
recomputeCreatureByDoc(rootCreature);
// denormalised stats might change, so fetch the creature again
recomputeCreatureById(rootCreature._id);
},
});

View File

@@ -7,7 +7,6 @@ export default function recomputeInactiveProperties(ancestorId){
{disabled: true}, // Everything can be disabled
{type: 'buff', applied: false}, // Buffs can be applied
{type: 'item', equipped: {$ne: true}},
{type: 'toggle', toggleResult: false},
{type: 'spell', prepared: {$ne: true}, alwaysPrepared: {$ne: true}},
],
};
@@ -56,14 +55,18 @@ export default function recomputeInactiveProperties(ancestorId){
CreatureProperties.update({
'ancestors.id': {$eq: ancestorId, $nin: disabledIds},
'_id': {$nin: disabledIds},
// if it was a toggle responsible, we leave it alone
deactivatedByToggle: {$ne: true},
$or: [
{inactive: true},
{deactivatedByAncestor: true},
{deactivatedBySelf: true}
],
}, {
$unset: {
inactive: 1,
deactivatedByAncestor: 1,
deactivatedBySelf: 1,
},
}, {
multi: true,

View File

@@ -1,5 +1,6 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import nodesToTree from '/imports/api/parenting/parenting.js';
import Creatures from '/imports/api/creature/Creatures.js';
import { nodesToTree } from '/imports/api/parenting/parenting.js';
export default function recomputeInventory(creatureId){
let inventoryForest = nodesToTree({
@@ -10,27 +11,27 @@ export default function recomputeInventory(creatureId){
},
deactivatedByAncestor: {$ne: true},
});
return getChildrenInventoryData(inventoryForest);
}
function getChildrenInventoryData(forest){
let data = {
weightTotal: 0,
weightEquipment: 0,
weightCarried: 0,
valueTotal: 0,
valueEquipment: 0,
valueCarried: 0,
}
forest.forEach(tree => {
let treeData = getInventoryData(tree);
for (let key in data){
data[key] += treeData[key];
}
let containersToWrite = [];
let data = getChildrenInventoryData(inventoryForest, containersToWrite);
containersToWrite.forEach(container => {
CreatureProperties.update(container._id, {$set: {
contentsWeight: container.contentsWeight,
contentsValue: container.contentsValue,
}}, {selector: {type: 'container'}});
});
Creatures.update(creatureId, {$set: {
'denormalizedStats.weightTotal': data.weightTotal,
'denormalizedStats.weightEquipment': data.weightEquipment,
'denormalizedStats.weightCarried': data.weightCarried,
'denormalizedStats.valueTotal': data.valueTotal,
'denormalizedStats.valueEquipment': data.valueEquipment,
'denormalizedStats.valueCarried': data.valueCarried,
'denormalizedStats.itemsAttuned': data.itemsAttuned,
}});
return data;
}
function getInventoryData(tree){
function getChildrenInventoryData(forest, containersToWrite){
let data = {
weightTotal: 0,
weightEquipment: 0,
@@ -40,24 +41,41 @@ function getInventoryData(tree){
valueCarried: 0,
itemsAttuned: 0,
}
let childData = getChildrenInventoryData(tree.children);
forest.forEach(tree => {
let treeData = getInventoryData(tree, containersToWrite);
for (let key in data){
data[key] += treeData[key] || 0;
}
});
return data;
}
function getInventoryData(tree, containersToWrite){
let data = {
weightTotal: 0,
weightEquipment: 0,
weightCarried: 0,
valueTotal: 0,
valueEquipment: 0,
valueCarried: 0,
itemsAttuned: 0,
}
let childData = getChildrenInventoryData(tree.children, containersToWrite);
let node = tree.node;
if (node.type === 'container'){
data.weightTotal += node.weight;
data.valueTotal += node.value;
if (node.carried){
data.weightCarried += node.weight;
data.valueCarried += node.valueCarried;
}
storeContentsData(node, childData);
data.weightTotal += node.weight || 0;
data.valueTotal += node.value || 0;
data.weightCarried += node.weight || 0;
data.valueCarried += node.value || 0;
storeContentsData(node, childData, containersToWrite);
} else if (node.type === 'item'){
data.weightTotal += node.weight * node.quantity;
data.valueTotal += node.value * node.quantity;
data.weightCarried += node.weight * node.quantity;
data.valueCarried += node.valueCarried * node.quantity;
data.weightTotal += (node.weight * node.quantity) || 0;
data.valueTotal += (node.value * node.quantity) || 0;
data.weightCarried += (node.weight * node.quantity) || 0;
data.valueCarried += (node.value * node.quantity) || 0;
if (node.equipped){
data.weightEquipment += node.weight * node.quantity;
data.valueEquipment += node.valueCarried * node.quantity;
data.weightEquipment += (node.weight * node.quantity) || 0;
data.valueEquipment += (node.value * node.quantity) || 0;
}
if (node.attuned){
data.itemsAttuned += 1;
@@ -66,16 +84,18 @@ function getInventoryData(tree){
for (let key in data){
data[key] += childData[key];
}
if (node.contentsWeightless){
data.weightCarried = node.weight;
}
if (node.carried === false){
data.weightCarried = 0;
data.valueCarried = 0;
}
return data
}
function storeContentsData(node, childData){
let newContentsWeight;
if (node.contentsWeightless){
newContentsWeight = 0;
} else {
newContentsWeight = childData.weightCarried
}
function storeContentsData(node, childData, containersToWrite){
let newContentsWeight = childData.weightCarried
if (node.contentsWeight !== newContentsWeight){
node.contentsWeight = newContentsWeight;
node.contentsWeightChanged = true;
@@ -85,4 +105,7 @@ function storeContentsData(node, childData){
node.contentsValue = newContentsValue;
node.contentsValueChanged = true;
}
if (node.contentsWeightChanged || node.contentsValueChanged){
containersToWrite.push(node);
}
}

View File

@@ -23,10 +23,14 @@ export default function recomputeSlotFullness(ancestorId){
}
});
let spaceLeft;
if (slot.quantityExpected === 0){
let expected = slot.quantityExpectedResult;
if (typeof expected !== 'number'){
expected = 1;
}
if (expected === 0){
spaceLeft = null;
} else {
spaceLeft = slot.quantityExpected - totalFilled;
spaceLeft = expected - totalFilled;
}
if (slot.totalFilled !== totalFilled || slot.spaceLeft !== spaceLeft){
CreatureProperties.update(slot._id, {

View File

@@ -28,10 +28,14 @@ const removeCreature = new ValidatedMethod({
},
run({charId}) {
assertOwnership(charId, this.userId)
Creatures.remove(charId);
this.unblock();
removeRelatedDocuments(charId);
this.unblock();
removeCreatureWork(charId)
},
});
export function removeCreatureWork(creatureId){
Creatures.remove(creatureId);
removeRelatedDocuments(creatureId);
}
export default removeCreature;

View File

@@ -118,10 +118,14 @@ const removeLibrary = new ValidatedMethod({
run({_id}){
let library = Libraries.findOne(_id);
assertOwnership(library, this.userId);
Libraries.remove(_id);
this.unblock();
LibraryNodes.remove({'ancestors.id': _id});
this.unblock();
removeLibaryWork(_id)
}
})
});
export function removeLibaryWork(libraryId){
Libraries.remove(libraryId);
LibraryNodes.remove({'ancestors.id': libraryId});
}
export { LibrarySchema, insertLibrary, setLibraryDefault, updateLibraryName, removeLibrary };

View File

@@ -10,7 +10,7 @@ import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
import getCollectionByName from '/imports/api/parenting/getCollectionByName.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
const organizeDoc = new ValidatedMethod({
name: 'organize.organizeDoc',
validate: new SimpleSchema({
@@ -20,13 +20,17 @@ const organizeDoc = new ValidatedMethod({
type: Number,
// Should end in 0.5 to place it reliably between two existing documents
},
skipRecompute: {
type: Boolean,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({docRef, parentRef, order}) {
run({docRef, parentRef, order, skipRecompute}) {
let doc = fetchDocByRef(docRef);
let collection = getCollectionByName(docRef.collection);
// The user must be able to edit both the doc and its parent to move it
@@ -52,15 +56,20 @@ const organizeDoc = new ValidatedMethod({
// Figure out which creatures need to be recalculated after this move
let docCreatures = getCreatureAncestors(doc);
let parentCreatures = getCreatureAncestors(parent);
let creaturesToRecompute = union(docCreatures, parentCreatures);
// Recompute the creatures
creaturesToRecompute.forEach(id => {
// The active status of some properties might change due to a change in
// ancestry
recomputeInactiveProperties(id);
// Some Dependencies depend on ancestry, so a full recompute is needed
recomputeCreatureById(id);
});
if (!skipRecompute){
let creaturesToRecompute = union(docCreatures, parentCreatures);
// Recompute the creatures
creaturesToRecompute.forEach(id => {
// The active status of some properties might change due to a change in
// ancestry
recomputeInactiveProperties(id);
if (doc.type === 'container' || doc.type === 'item'){
recomputeInventory(id);
}
// Some Dependencies depend on ancestry, so a full recompute is needed
recomputeCreatureById(id);
});
}
},
});

View File

@@ -22,9 +22,9 @@ let SlotSchema = new SimpleSchema({
type: String,
},
quantityExpected: {
type: SimpleSchema.Integer,
defaultValue: 1,
min: 0,
type: String,
optional: true,
defaultValue: '1',
},
ignored: {
type: Boolean,

View File

@@ -23,6 +23,16 @@ let SpellListSchema = new SimpleSchema({
type: String,
optional: true,
},
// Calculation of The attack roll bonus used by spell attacks in this list
attackRollBonus: {
type: String,
optional: true,
},
// Calculation of the save dc used by spells in this list
dc: {
type: String,
optional: true,
},
});
const ComputedOnlySpellListSchema = new SimpleSchema({
@@ -33,6 +43,7 @@ const ComputedOnlySpellListSchema = new SimpleSchema({
},
'descriptionCalculations.$': InlineComputationSchema,
// maxPrepared
maxPreparedResult: {
type: Number,
optional: true,
@@ -44,6 +55,32 @@ const ComputedOnlySpellListSchema = new SimpleSchema({
'maxPreparedErrors.$':{
type: ErrorSchema,
},
// attackRollBonus
attackRollBonusResult: {
type: Number,
optional: true,
},
attackRollBonusErrors: {
type: Array,
optional: true,
},
'attackRollBonusErrors.$':{
type: ErrorSchema,
},
// dc
dcResult: {
type: Number,
optional: true,
},
dcErrors: {
type: Array,
optional: true,
},
'dcErrors.$':{
type: ErrorSchema,
},
});
const ComputedSpellListSchema = new SimpleSchema()

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import '/imports/api/users/deleteMyAccount.js';
const userSchema = new SimpleSchema({
username: {

View File

@@ -0,0 +1,61 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Libraries, {removeLibaryWork} from '/imports/api/library/Libraries.js';
import Creatures from '/imports/api/creature/Creatures.js';
import {removeCreatureWork} from '/imports/api/creature/removeCreature.js';
Meteor.users.deleteMyAccount = new ValidatedMethod({
name: 'users.deleteMyAccount',
validate: null,
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 5000,
},
run(){
let userId = Meteor.userId();
if (!userId) throw new Meteor.Error('No user',
'You must be logged into to delete your account');
// Delete all creatures
let creatures = Creatures.find({owner: userId}, {fields: {_id: 1}}).fetch();
creatures.forEach(creature => removeCreatureWork(creature._id));
// Remove permissions from all creatures
Creatures.update({
$or: [
{writers: userId},
{readers: userId},
],
}, {
$pull: {
writers: userId,
readers: userId
},
}, {
multi: true,
});
// Delete all libraries
let libraries = Libraries.find({owner: userId}, {fields: {_id: 1}}).fetch();
libraries.forEach(library => removeLibaryWork(library._id));
// Remove permissions from all creatures
Libraries.update({
$or: [
{writers: userId},
{readers: userId},
],
}, {
$pull: {
writers: userId,
readers: userId
},
}, {
multi: true,
});
// delete the account
Meteor.users.remove(userId);
}
});

View File

@@ -91,6 +91,10 @@ const SVG_ICONS = Object.freeze({
name: 'weight',
shape: 'M256 46c-45.074 0-82 36.926-82 82 0 25.812 12.123 48.936 30.938 64H128L32 480h448l-96-288h-76.938C325.877 176.936 338 153.812 338 128c0-45.074-36.926-82-82-82zm0 36c25.618 0 46 20.382 46 46s-20.382 46-46 46-46-20.382-46-46 20.382-46 46-46z',
},
'weightless': {
name: 'weightless',
shape: 'M470.72 20L368.186 49.813l41.563-28.094c-26.254 5.922-59.36 17.502-100.97 36.186l-67.874 70.78L264.97 79.25c-23.247 12.958-47.95 29.99-71.814 49.844l-15.78 64.312L174 145.844c-23.55 21.548-45.624 45.6-63.875 70.812-19.25 26.59-34.28 54.506-41.813 82.438L40.19 280.28c6.138 19.613 11.892 39.232 22.906 58.845.032 1.468.1 2.944.187 4.406L29.657 333.19c11.227 18.284 23.577 35.893 43 49.125.45 1.003.953 1.973 1.438 2.968-11.838 33.33-20.568 67.004-26.53 101.69l18.405 3.155c4.952-28.808 11.836-56.842 20.905-84.563.04.053.084.105.125.157 44.277-156.11 142.813-266.846 287.03-324l6.876 17.374c-129.048 51.143-219.303 145.15-265.78 279.062 18.106.102 35.796-2.088 52.218-6.22l4.875-60.967 13.093 55.5c10.84-3.922 20.88-8.762 29.812-14.376l-20.688-43.47 32.782 34.813c7.944-6.468 14.613-13.678 19.624-21.53 30.308-47.507 62.195-94.728 124.75-134.188l-45.72-16.25 70.157 2.124c2.044-1.085 4.087-2.18 6.19-3.25 9.087-4.63 17.916-10.182 26.31-16.375L378.814 150l74.718-17.625c5.788-5.81 11.174-11.836 16.033-17.97 17.384-21.94 29.034-44.784 26.28-65.56-1.376-10.39-7.556-20.154-17.624-25.626-2.333-1.27-4.832-2.337-7.5-3.22zM106.25 406c-.89 3.06-1.778 6.122-2.625 9.22l2.625-9.22z'
}
});
export default SVG_ICONS;

View File

@@ -1,3 +1,5 @@
import ArrayNode from '/imports/parser/parseTree/ArrayNode.js';
export default {
'abs': {
comment: 'Returns the absolute value of a number',
@@ -5,7 +7,7 @@ export default {
{input: 'abs(9)', result: '9'},
{input: 'abs(-3)', result: '3'},
],
argumentType: 'number',
arguments: ['number'],
resultType: 'number',
fn: Math.abs,
},
@@ -15,21 +17,21 @@ export default {
{input: 'sqrt(16)', result: '4'},
{input: 'sqrt(10)', result: '3.1622776601683795'},
],
argumentType: 'number',
arguments: ['number'],
resultType: 'number',
fn: Math.sqrt,
},
'max': {
comment: 'Returns the largest of the given numbers',
examples: [{input: 'min(12, 6, 3, 168)', result: '168'}],
argumentType: 'number',
examples: [{input: 'max(12, 6, 3, 168)', result: '168'}],
arguments: anyNumberOf('number'),
resultType: 'number',
fn: Math.max,
},
'min': {
comment: 'Returns the smallest of the given numbers',
examples: [{input: 'min(12, 6, 3, 168)', result: '3'}],
argumentType: 'number',
arguments: anyNumberOf('number'),
resultType: 'number',
fn: Math.min,
},
@@ -40,7 +42,7 @@ export default {
{input: 'round(5.5)', result: '6'},
{input: 'round(5.05)', result: '5'},
],
argumentType: 'number',
arguments: ['number'],
resultType: 'number',
fn: Math.round,
},
@@ -52,7 +54,7 @@ export default {
{input: 'floor(5)', result: '5'},
{input: 'floor(-5.5)', result: '-6'},
],
argumentType: 'number',
arguments: ['number'],
resultType: 'number',
fn: Math.floor,
},
@@ -64,7 +66,7 @@ export default {
{input: 'ceil(5)', result: '5'},
{input: 'ceil(-5.5)', result: '-5'},
],
argumentType: 'number',
arguments: ['number'],
resultType: 'number',
fn: Math.ceil,
},
@@ -76,7 +78,7 @@ export default {
{input: 'trunc(5)', result: '5'},
{input: 'trunc(-5.5)', result: '-5'},
],
argumentType: 'number',
arguments:[ 'number'],
resultType: 'number',
fn: Math.trunc,
},
@@ -87,8 +89,32 @@ export default {
{input: 'sign(3)', result: '1'},
{input: 'sign(0)', result: '0'},
],
argumentType: 'number',
arguments: ['number'],
resultType: 'number',
fn: Math.sign,
},
'tableLookup': {
comment: 'Returns the index of the last value in the array that is less than the specified amount',
examples: [
{input: 'tableLookup([100, 300, 900], 457)', result: '2'},
{input: 'tableLookup([100, 300, 900], 23)', result: '0'},
{input: 'tableLookup([100, 300, 900, 1200], 900)', result: '3'},
{input: 'tableLookup([100, 300], 594)', result: '2'},
],
arguments: [ArrayNode, 'number'],
resultType: 'number',
fn: function tableLookup(arrayNode, number){
for(let i in arrayNode.values){
let node = arrayNode.values[i];
if (node.value > number) return i;
}
return arrayNode.values.length;
}
}
}
function anyNumberOf(type){
let argumentArray = [type];
argumentArray.anyLength = true;
return argumentArray;
}

View File

@@ -11,40 +11,64 @@ export default class CallNode extends ParseNode {
}
resolve(fn, scope, context){
let func = functions[this.functionName];
// Check that the function exists
if (!func) return new ErrorNode({
node: this,
error: `${this.functionName} is not a function`,
error: `${this.functionName} is not a supported function`,
context,
});
let args = castArgsToType({fn, scope, context, args: this.args, type: func.argumentType});
if (args.failed){
if (fn === 'reduce'){
// Resolve the arguments
let resolvedArgs = this.args.map(node => node[fn](scope, context));
// Check that the arguments match what is expected
let checkFailed = this.checkArugments({
fn,
context,
resolvedArgs,
argumentsExpected: func.arguments
});
if (checkFailed){
if (fn !== 'reduce'){
return new ErrorNode({
node: this,
error: 'Could not convert all arguments to the correct type',
context,
error: `Invalid arguments to ${this.functionName} function`,
});
} else {
return new CallNode({
functionName: this.functionName,
args: args,
args: resolvedArgs,
});
}
} else {
try {
let value = func.fn.apply(null, args);
return new ConstantNode({
value,
type: 'number',
previousNodes: [this],
});
} catch (error) {
return new ErrorNode({
node: this,
error,
context,
});
}
// Map contant nodes to constants before attempting to run the function
let mappedArgs = resolvedArgs.map(node => {
if (node instanceof ConstantNode){
return node.value;
} else {
return node;
}
});
try {
// Run the function
let value = func.fn.apply(null, mappedArgs);
let type = typeof value;
if (type === 'number' || type === 'string' || type === 'boolean'){
// Convert constant results into constant nodes
return new ConstantNode({ value, type });
} else {
return value;
}
} catch (error) {
return new ErrorNode({
node: this,
error: error.message || error,
context,
});
}
}
toString(){
@@ -57,20 +81,47 @@ export default class CallNode extends ParseNode {
replaceChildren(fn){
this.args = this.args.map(arg => arg.replaceNodes(fn));
}
}
checkArugments({fn, context, argumentsExpected, resolvedArgs}){
// Check that the number of arguments matches the number expected
if (
!argumentsExpected.anyLength &&
argumentsExpected.length !== resolvedArgs.length
){
context.storeError({
type: 'error',
message: 'Incorrect number of arguments ' +
`to ${this.functionName} function, ` +
`expected ${argumentsExpected.length} got ${resolvedArgs.length}`
});
return true;
}
function castArgsToType({fn, scope, context, args, type}){
let resolvedArgs = args.map(node => node[fn](scope, context))
let result = [];
if (type === 'number'){
resolvedArgs.forEach(node => {
if (node.isNumber){
result.push(node.value);
let failed = false;
// Check that each argument is of the correct type
resolvedArgs.forEach((node, index) => {
let type;
if (argumentsExpected.anyLength){
type = argumentsExpected[0];
} else {
resolvedArgs.failed = true;
type = argumentsExpected[index];
}
})
if (typeof type === 'string'){
// Type being a string means a constant node with matching type
if (node.type !== type) failed = true;
} else {
// Otherwise check that the node is an instance of the given type
if (!(node instanceof type)) failed = true;
}
if (failed && fn === 'reduce'){
let typeName = typeof type === 'string' ? type : type.constructor.name;
let nodeName = node.type || node.constructor.name
context.storeError({
type: 'error',
message: `Incorrect arguments to ${this.functionName} function` +
`expected ${typeName} got ${nodeName}`
});
}
});
return failed;
}
if (resolvedArgs.failed) return resolvedArgs;
return result;
}

View File

@@ -1,4 +1,6 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
import ArrayNode from '/imports/parser/parseTree/ArrayNode.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
export default class IndexNode extends ParseNode {
constructor({array, index}) {
@@ -8,16 +10,42 @@ export default class IndexNode extends ParseNode {
}
resolve(fn, scope, context){
let index = this.index[fn](scope, context);
if (index.isInteger){
let selection = this.array.values[index.value - 1];
let array = this.array[fn](scope, context);
if (index.isInteger && array instanceof ArrayNode){
if (index.value < 1 || index.value > array.values.length){
if (context){
context.storeError({
type: 'warning',
message: `Index of ${index.value} is out of range for an array` +
` of length ${array.values.length}`,
});
}
}
let selection = array.values[index.value - 1];
if (selection){
let result = selection[fn](scope, context);
return result;
}
} else if (fn === 'reduce'){
if (!(array instanceof ArrayNode)){
return new ErrorNode({
node: this,
error: 'Can not get the index of a non-array node: ' +
this.array.toString() + ' = ' + array.toString(),
context,
});
} else if (!index.isInteger){
return new ErrorNode({
node: this,
error: array.toString() + ' is not an integer index of the array',
context,
});
}
}
return new IndexNode({
index,
array: this.array[fn](scope, context),
array,
previousNodes: [this],
});
}

View File

@@ -8,6 +8,7 @@ export default class ParenthesisNode extends ParseNode {
resolve(fn, scope, context){
let content = this.content[fn](scope, context);
if (
fn === 'reduce' ||
content.constructor.name === 'ConstantNode' ||
content.constructor.name === 'ErrorNode'
){

View File

@@ -10,7 +10,6 @@ export default function sendWebhook({webhookURL, data = {}}){
const hook = new Discord.WebhookClient(id, token);
// Send a message using the webhook
console.log(JSON.stringify(data, null, 2));
hook.send(data);
}

View File

@@ -7,3 +7,4 @@ import '/imports/server/publications/users.js';
import '/imports/server/publications/icons.js';
import '/imports/server/publications/tabletops.js';
import '/imports/server/publications/slotFillers.js'
import '/imports/server/publications/ownedDocuments.js'

View File

@@ -32,26 +32,31 @@ Meteor.publish('libraries', function(){
});
let libraryIdSchema = new SimpleSchema({
libraryId: {
libraryIds: {
type: Array,
},
'libraryIds.$':{
type: String,
regEx: SimpleSchema.RegEx.Id,
},
});
Meteor.publish('library', function(libraryId){
libraryIdSchema.validate({libraryId});
Meteor.publish('libraryNodes', function(libraryIds){
libraryIdSchema.validate({libraryIds});
if (!libraryIds.length) return [];
this.autorun(function (){
let userId = this.userId;
let libraryCursor = Libraries.find({
_id: libraryId,
});
let library = libraryCursor.fetch()[0];
try { assertViewPermission(library, userId) }
catch(e){ return [] }
for (let i in libraryIds){
let libraryId = libraryIds[i];
let library = Libraries.findOne(libraryId);
try { assertViewPermission(library, userId) }
catch(e){
return this.error(e);
}
}
return [
libraryCursor,
LibraryNodes.find({
'ancestors.id': libraryId,
'ancestors.id': {$in: libraryIds},
}, {
sort: {order: 1},
}),

View File

@@ -0,0 +1,15 @@
import Creatures from '/imports/api/creature/Creatures.js';
import Libraries from '/imports/api/library/Libraries.js';
Meteor.publish('ownedDocuments', function(){
this.autorun(function (){
let userId = this.userId;
if (!userId) {
return [];
}
return [
Creatures.find({owner: userId}),
Libraries.find({owner: userId}),
]
});
});

View File

@@ -3,6 +3,7 @@ import Creatures from '/imports/api/creature/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import { assertViewPermission } from '/imports/api/creature/creaturePermissions.js';
import recomputeInvetory from '/imports/api/creature/denormalise/recomputeInventory.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/methods/recomputeCreature.js';
import VERSION from '/imports/constants/VERSION.js';
@@ -25,7 +26,10 @@ Meteor.publish('singleCharacter', function(creatureId){
try { assertViewPermission(creature, userId) }
catch(e){ return [] }
if (creature.computeVersion !== VERSION){
try { recomputeCreatureById(creatureId) }
try {
recomputeInvetory(creatureId);
recomputeCreatureById(creatureId)
}
catch(e){ console.error(e) }
}
return [

View File

@@ -50,7 +50,7 @@ Meteor.publish('slotFillers', function(slotId){
}
this.autorun(function(){
// Get the limit of the documents the user can fetch
var limit = self.data('limit') || 16;
var limit = self.data('limit') || 20;
check(limit, Number);
// Get the search term

View File

@@ -73,7 +73,7 @@
props: {
value: {
type: Number,
required: true,
default: 0,
},
open: Boolean,
flat: Boolean,

View File

@@ -3,6 +3,8 @@
fab
small
v-bind="$attrs"
:disabled="disabled"
:style="disabled ? 'background-color: #616161 !important;' : ''"
@click="$emit('click')"
>
<v-icon>{{ icon }}</v-icon>
@@ -18,7 +20,7 @@
* component creates a v-btn with a label.
*/
export default {
props: ['icon', 'label'],
props: ['icon', 'label', 'disabled'],
}
</script>

View File

@@ -19,6 +19,7 @@
<v-layout
v-if="editing && model"
key="edit-buttons"
style="flex-shrink: 0;"
>
<v-spacer />
<color-picker

View File

@@ -0,0 +1,104 @@
<template lang="html">
<v-speed-dial
v-if="speedDials && speedDials.length"
v-model="fab"
direction="bottom"
>
<template #activator>
<v-btn
v-model="fab"
color="primary"
fab
data-id="insert-creature-property-fab"
small
>
<v-icon>add</v-icon>
<v-icon>close</v-icon>
</v-btn>
</template>
<labeled-fab
v-for="type in speedDials"
:key="type"
color="primary"
:data-id="`insert-creature-property-type-${type}`"
:label="'New ' + properties[type].name"
:icon="properties[type].icon"
:disabled="!editPermission"
@click="insertPropertyOfType(type)"
/>
</v-speed-dial>
</template>
<script>
import LabeledFab from '/imports/ui/components/LabeledFab.vue';
import { setDocToLastOrder } from '/imports/api/parenting/order.js';
import insertProperty from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js';
const tabs = [
'stats',
'features',
'inventory',
'spells',
'character',
'tree',
];
export default {
components: {
LabeledFab,
},
props: {
editPermission: Boolean,
},
data(){return {
fab: false,
};},
computed: {
creatureId(){
return this.$route.params.id;
},
tabNumber(){
return this.$store.getters.tabById(this.creatureId);
},
speedDials(){
return this.speedDialsByTab[tabs[this.tabNumber]];
},
speedDialsByTab() { return {
'stats': ['attribute', 'skill', 'action', 'attack'],
'features': ['feature'],
'inventory': ['item', 'container'],
'spells': ['spellList', 'spell'],
'character': ['note'],
};},
properties(){
return PROPERTIES;
},
},
methods: {
insertPropertyOfType(type){
let that = this;
this.$store.commit('pushDialogStack', {
component: 'creature-property-creation-dialog',
elementId: 'insert-creature-property-type-' + type,
data: {
forcedType: type,
},
callback(creatureProperty){
if (!creatureProperty) return;
creatureProperty.parent = {collection: 'creatures', id: that.creatureId};
creatureProperty.ancestors = [ {collection: 'creatures', id: that.creatureId}];
setDocToLastOrder({collection: CreatureProperties, doc: creatureProperty});
let id = insertProperty.call({creatureProperty});
return id;
}
});
}
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -5,6 +5,7 @@
:color="toolbarColor"
:dark="isDark"
:light="!isDark"
extended
tabs
dense
>
@@ -73,11 +74,12 @@
>
<div
:key="$route.meta.title"
style="width: 100%"
class="layout row"
>
<v-tabs
v-if="creature"
slot="extension"
class="flex"
style="min-width: 0"
centered
grow
max="100px"
@@ -106,6 +108,11 @@
Tree
</v-tab>
</v-tabs>
<v-spacer />
<character-sheet-fab
class="character-sheet-fab"
:edit-permission="editPermission"
/>
</div>
</v-fade-transition>
</v-toolbar>
@@ -119,8 +126,15 @@ import { theme } from '/imports/ui/theme.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { updateUserSharePermissions } from '/imports/api/sharing/sharing.js';
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
import CharacterSheetFab from '/imports/ui/creature/character/CharacterSheetFab.vue';
export default {
inject: {
context: { default: {} }
},
components: {
CharacterSheetFab,
},
data(){return {
theme,
}},
@@ -231,4 +245,8 @@ export default {
.character-sheet-toolbar .v-tabs__bar {
background: none !important;
}
.character-sheet-fab {
bottom: -24px;
margin-right: -8px;
}
</style>

View File

@@ -41,7 +41,7 @@
>
Level {{ creature.variables.level.value }}
</v-card-title>
<v-list>
<v-list two-line>
<v-list-tile>
<v-list-tile-content>
<v-list-tile-title
@@ -52,7 +52,14 @@
>
{{ creature.variables.milestoneLevels.value }} Milestone levels
</v-list-tile-title>
<v-list-tile-title v-else>
<v-list-tile-title
v-if="
!(creature.variables.milestoneLevels &&
creature.variables.milestoneLevels.value) ||
(creature.variables.xp &&
creature.variables.xp.value)
"
>
{{
creature.variables.xp &&
creature.variables.xp.value ||

View File

@@ -1,6 +1,59 @@
<template lang="html">
<div class="inventory">
<column-layout wide-columns>
<div>
<v-card>
<v-list>
<v-list-tile>
<v-list-tile-avatar>
<v-icon>$vuetify.icons.injustice</v-icon>
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title>
Weight Carried
</v-list-tile-title>
</v-list-tile-content>
<v-list-tile-action>
<v-list-tile-title>
{{ creature.denormalizedStats.weightCarried || 0 }} lb
</v-list-tile-title>
</v-list-tile-action>
</v-list-tile>
<v-list-tile>
<v-list-tile-avatar>
<v-icon>$vuetify.icons.cash</v-icon>
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title>
Net worth
</v-list-tile-title>
</v-list-tile-content>
<v-list-tile-action>
<v-list-tile-title>
<coin-value
:value="creature.denormalizedStats.valueTotal || 0"
/>
</v-list-tile-title>
</v-list-tile-action>
</v-list-tile>
<v-list-tile v-if="creature.denormalizedStats.itemsAttuned">
<v-list-tile-avatar>
<v-icon>$vuetify.icons.spell</v-icon>
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title>
Items attuned
</v-list-tile-title>
</v-list-tile-content>
<v-list-tile-action>
<v-list-tile-title>
{{ creature.denormalizedStats.itemsAttuned }}
</v-list-tile-title>
</v-list-tile-action>
</v-list-tile>
</v-list>
</v-card>
</div>
<div>
<toolbar-card
:color="creature.color"
@@ -53,6 +106,7 @@ import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
import ItemList from '/imports/ui/properties/components/inventory/ItemList.vue';
import { getParentRefByTag } from '/imports/api/creature/creatureProperties/methods/equipItem.js';
import INVENTORY_TAGS from '/imports/constants/INVENTORY_TAGS.js';
import CoinValue from '/imports/ui/components/CoinValue.vue';
export default {
components: {
@@ -60,6 +114,7 @@ export default {
ContainerCard,
ToolbarCard,
ItemList,
CoinValue,
},
props: {
creatureId: {
@@ -82,7 +137,10 @@ export default {
});
},
creature(){
return Creatures.findOne(this.creatureId, {fields: {color: 1}});
return Creatures.findOne(this.creatureId, {fields: {
color: 1,
denormalizedStats: 1,
}});
},
containersWithoutAncestorContainers(){
return CreatureProperties.find({

View File

@@ -1,11 +1,14 @@
<template lang="html">
<selectable-property-dialog v-model="type">
<creature-property-insert-form
:type="type"
:property-name="getPropertyName(type)"
@back="type = undefined"
/>
</selectable-property-dialog>
<selectable-property-dialog
:value="forcedType || type"
@input="e => type = e"
>
<creature-property-insert-form
:type="forcedType || type"
:property-name="getPropertyName(forcedType || type)"
@back="back"
/>
</selectable-property-dialog>
</template>
<script>
@@ -14,15 +17,28 @@ import CreaturePropertyInsertForm from '/imports/ui/creature/creatureProperties/
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
export default {
data() { return {
type: undefined,
};},
components: {
SelectablePropertyDialog,
CreaturePropertyInsertForm,
},
props: {
forcedType: {
type: String,
default: undefined,
},
},
data() { return {
type: undefined,
};},
methods: {
getPropertyName,
back(){
if (this.forcedType){
this.$store.dispatch('popDialogStack');
} else {
this.type = undefined;
}
},
},
};
</script>

View File

@@ -70,14 +70,22 @@ export default {
};},
watch: {
type(newType){
if (!newType) return;
this.schema = propertySchemasIndex[newType];
this.validationContext = this.schema.newContext();
let model = this.schema.clean({});
model.type = newType;
this.model = model;
this.changeType(newType);
},
},
mounted(){
this.changeType(this.type);
},
methods:{
changeType(type){
if (!type) return;
this.schema = propertySchemasIndex[type];
this.validationContext = this.schema.newContext();
let model = this.schema.clean({});
model.type = type;
this.model = model;
}
},
}
</script>

View File

@@ -143,10 +143,10 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import { parse, CompilationContext } from '/imports/parser/parser.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js';
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue'
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
export default {
components: {
@@ -196,7 +196,7 @@ export default {
},
loadMore(){
if (this.currentLimit >= this.countAll) return;
this._subs['slotFillers'].setData('limit', this.currentLimit + 16);
this._subs['slotFillers'].setData('limit', this.currentLimit + 20);
},
insert(){
if (!this.selectedNode) return;
@@ -243,15 +243,12 @@ export default {
// the quantity to fill
nodes = nodes.filter(node => {
if (node.slotFillerCondition){
let context = new CompilationContext();
let conditionResult;
try {
conditionResult = parse(node.slotFillerCondition)
.reduce(this.creature.variables, context);
} catch (e){
console.warn(e);
}
if (conditionResult && !conditionResult.value) return false;
let {result} = evaluateString({
string: node.slotFillerCondition,
scope: this.creature.variables,
fn: 'reduce',
});
if (!result.value) return false;
}
if (
node.type === 'slotFiller' &&

View File

@@ -8,8 +8,8 @@
<h3 class="layout row align-center">
{{ slot.name }}
<v-spacer />
<span v-if="slot.quantityExpected > 1">
{{ slot.totalFilled }} / {{ slot.quantityExpected }}
<span v-if="slot.quantityExpectedResult > 1">
{{ slot.totalFilled }} / {{ slot.quantityExpectedResult }}
</span>
</h3>
<v-list v-if="slot.children.length">
@@ -38,7 +38,7 @@
</v-list-tile>
</v-list>
<v-btn
v-if="!slot.quantityExpected || slot.spaceLeft"
v-if="!slot.quantityExpectedResult || slot.spaceLeft"
icon
:data-id="`slot-add-button-${slot._id}`"
class="slot-add-button"

View File

@@ -4,6 +4,7 @@ import CreaturePropertyCreationDialog from '/imports/ui/creature/creaturePropert
import CreaturePropertyDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue'
import CreaturePropertyFromLibraryDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyFromLibraryDialog.vue'
import DeleteConfirmationDialog from '/imports/ui/dialogStack/DeleteConfirmationDialog.vue';
import DeleteUserAccountDialog from '/imports/ui/user/DeleteUserAccountDialog.vue';
import ExperienceInsertDialog from '/imports/ui/creature/experiences/ExperienceInsertDialog.vue';
import ExperienceListDialog from '/imports/ui/creature/experiences/ExperienceListDialog.vue';
import InviteDialog from '/imports/ui/user/InviteDialog.vue';
@@ -26,6 +27,7 @@ export default {
CreaturePropertyDialog,
CreaturePropertyFromLibraryDialog,
DeleteConfirmationDialog,
DeleteUserAccountDialog,
ExperienceInsertDialog,
ExperienceListDialog,
InviteDialog,

View File

@@ -13,7 +13,7 @@
name="toolbar"
/>
<v-toolbar
v-if="!$route.matched[0].components.toolbar"
v-if="!$route.matched[0] || !$route.matched[0].components.toolbar"
app
color="secondary"
dark

View File

@@ -112,9 +112,6 @@ export default {
getPropertyName,
},
meteor: {
$subscribe: {
'libraries': [],
},
libraries(){
return Libraries.find({}, {
sort: {name: 1}

View File

@@ -56,7 +56,7 @@
</v-expansion-panel-content>
</v-expansion-panel>
<v-btn
v-show="expandedLibrary === null"
v-show="noLibrariesExpanded"
v-if="editMode"
flat
color="primary"
@@ -94,6 +94,17 @@ export default {
meteor: {
$subscribe: {
'libraries': [],
'libraryNodes'(){
if (!this.expandedLibrary) return [[]];
let libraryIds = [];
this.expandedLibrary.forEach((expanded, index) => {
if (expanded){
let library = this.libraries[index];
if (library) libraryIds.push(library._id)
}
});
return [libraryIds];
}
},
libraries(){
return Libraries.find({}, {
@@ -105,6 +116,16 @@ export default {
return tier && tier.paidBenefits;
},
},
computed: {
noLibrariesExpanded(){
if (!this.expandedLibrary) return true;
let noneExpanded = true;
this.expandedLibrary.forEach(lib => {
if(lib) noneExpanded = false;
});
return noneExpanded;
},
},
methods: {
insertLibrary(){
if (this.paidBenefits){

View File

@@ -27,11 +27,6 @@
selectedNodeId: String,
},
meteor: {
$subscribe: {
'library'(){
return [this.libraryId]
},
},
library(){
return Libraries.findOne(this.libraryId);
},

View File

@@ -141,6 +141,19 @@
</template>
</v-list>
</template>
<v-layout
row
justify-end
class="mt-3"
>
<v-btn
color="error"
data-id="delete-account-btn"
@click="deleteAccount"
>
Delete Account
</v-btn>
</v-layout>
</v-card>
</div>
</template>
@@ -270,6 +283,12 @@
this.updatePatreonLoading = false;
if (error) this.updatePatreonError = error;
});
},
deleteAccount(){
this.$store.commit('pushDialogStack', {
component: 'delete-user-account-dialog',
elementId: 'delete-account-btn',
});
}
},
}

View File

@@ -146,7 +146,7 @@ export default {
return Math.max(this.model.usesResult, 0);
},
usesLeft(){
return Math.max(this.model.usesResult - this.model.usesUsed, 0);
return Math.max(this.model.usesResult - (this.model.usesUsed || 0), 0);
},
propertyName(){
return getPropertyName(this.model.type);

View File

@@ -9,6 +9,31 @@
{{ model.name }}
</v-toolbar-title>
<v-spacer />
<v-toolbar-title>
<v-icon
small
style="width: 16px;"
class="mr-1"
>
$vuetify.icons.weight
</v-icon>
{{ (model.contentsWeightless ? 0 : model.contentsWeight || 0) + (model.weight || 0) }}
</v-toolbar-title>
<v-toolbar-title
class="layout row align-center"
style="flex-grow: 0;"
>
<v-icon
small
style="width: 16px;"
class="mr-1"
>
$vuetify.icons.two_coins
</v-icon>
<coin-value
:value="(model.contentsValue || 0) + (model.value || 0)"
/>
</v-toolbar-title>
</template>
<v-card-text class="px-0">
<item-list
@@ -23,11 +48,13 @@
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
import ItemList from '/imports/ui/properties/components/inventory/ItemList.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CoinValue from '/imports/ui/components/CoinValue.vue';
export default {
components: {
ToolbarCard,
ItemList,
CoinValue,
},
props: {
model: {

View File

@@ -15,7 +15,13 @@
{{ title }}
</v-list-tile-title>
</v-list-tile-content>
<v-list-tile-action>
<v-list-tile-action
v-if="model.attuned"
style="min-width: 40px;"
>
<v-icon>$vuetify.icons.spell</v-icon>
</v-list-tile-action>
<v-list-tile-action style="min-width: 40px;">
<increment-button
v-if="context.creatureId && model.showIncrement"
icon

View File

@@ -1,7 +1,7 @@
<template lang="html">
<div class="resources-form">
<div
v-if="model.attributesConsumed.length"
v-if="model.attributesConsumed && model.attributesConsumed.length"
class="subheading"
>
Attributes

View File

@@ -28,8 +28,6 @@
/>
<text-field
label="Quantity"
type="number"
min="0"
hint="How many matching properties must be used to fill this slot, 0 is unlimited"
:value="model.quantityExpected"
:error-messages="errors.quantityExpected"

View File

@@ -25,7 +25,7 @@
@change="change('description', ...arguments)"
/>
<calculation-error-list :calculations="model.descriptionCalculations" />
<text-field
label="Maximum prepared spells"
:value="model.maxPrepared"
@@ -35,6 +35,24 @@
/>
<calculation-error-list :errors="model.maxPreparedErrors" />
<text-field
label="Spell save DC"
:value="model.dc"
hint="The spell save DC of spells in this list"
:error-messages="errors.dc"
@change="change('dc', ...arguments)"
/>
<calculation-error-list :errors="model.dcErrors" />
<text-field
label="Attack roll bonus"
:value="model.attackRollBonus"
hint="The attack roll bonus of spell attacks made by spells in this list"
:error-messages="errors.attackRollBonus"
@change="change('attackRollBonus', ...arguments)"
/>
<calculation-error-list :errors="model.attackRollBonusErrors" />
<smart-combobox
label="Tags"
multiple

View File

@@ -10,7 +10,7 @@ export default {
},
},
mounted(){
if (this.$refs.focusFirst){
if (this.$refs.focusFirst && this.$refs.focusFirst.focus){
setTimeout(() => this.$refs.focusFirst.focus(), 300);
}
},

View File

@@ -8,7 +8,7 @@
<v-toolbar-title slot="toolbar">
Add Library Content
</v-toolbar-title>
<property-selector @select="property => $emit('input', property)" />
<property-selector @select="type => $emit('input', type)" />
</dialog-base>
<div
v-show="value"

View File

@@ -10,7 +10,11 @@
<div
class="text-no-wrap text-truncate"
>
{{ model.amountResult }} {{ model.stat }} damage
<span v-if="model.amountResult < 0">+</span>
{{ absoluteAmount }} {{ model.stat }}
<span v-if="typeof absoluteAmount === 'string' || absoluteAmount >= 0">
damage
</span>
</div>
</div>
</template>
@@ -20,5 +24,14 @@ import treeNodeViewMixin from '/imports/ui/properties/treeNodeViews/treeNodeView
export default {
mixins: [treeNodeViewMixin],
computed: {
absoluteAmount(){
if (typeof this.model.amountResult === 'number'){
return Math.abs(this.model.amountResult);
} else {
return this.model.amountResult;
}
},
}
}
</script>

View File

@@ -168,7 +168,7 @@ export default {
return Math.max(this.model.usesResult, 0);
},
usesLeft(){
return Math.max(this.model.usesResult - this.model.usesUsed, 0);
return Math.max(this.model.usesResult - (this.model.usesUsed || 0), 0);
},
actionTypeIcon() {
return `$vuetify.icons.${this.model.actionType}`;

View File

@@ -1,14 +1,81 @@
<template lang="html">
<div class="adjustment-viewer layout row align-center">
{{ model.amountResult }} {{ model.stat }} damage
</div>
<v-list-tile class="effect-viewer">
<v-list-tile-avatar>
<v-tooltip bottom>
<template
v-if="effectIcon !== 'remove'"
#activator="{ on }"
>
<v-icon
class="mx-2"
style="cursor: default;"
v-on="on"
>
{{ effectIcon }}
</v-icon>
</template>
<span>{{ tooltip }}</span>
</v-tooltip>
</v-list-tile-avatar>
<v-list-tile-action class="headline">
{{ displayedValue }}
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>
<code>{{ model.stat }}</code>
<template v-if="effectIcon === 'remove'">
damage
</template>
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</template>
<script>
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js';
import getEffectIcon from '/imports/ui/utility/getEffectIcon.js';
export default {
mixins: [propertyViewerMixin],
computed: {
effectIcon(){
let effectOp = this.model.operation === 'increment' ? 'add' : 'base';
let value = this.value;
if (typeof value === 'string'){
value = 1;
}
return getEffectIcon(effectOp, -value);
},
value(){
return 'amountResult' in this.model ?
this.model.amountResult :
this.model.amount;
},
displayedValue(){
if (
typeof this.value === 'number' &&
this.model.operation !== 'set'
){
return Math.abs(this.value);
} else {
return this.value;
}
},
tooltip(){
if (this.model.operation === 'increment'){
if (
typeof this.value === 'string' ||
this.value >= 0
){
return 'Minus';
} else {
return 'Add'
}
} else {
return 'Set'
}
}
},
}
</script>

View File

@@ -0,0 +1,23 @@
<template lang="html">
<div class="buff-viewer">
<property-name :value="model.name" />
<property-variable-name :value="model.variableName" />
<property-field
name="Calculation"
:value="model.calculation"
/>
<calculation-error-list :errors="model.errors" />
</div>
</template>
<script>
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
import CalculationErrorList from '/imports/ui/properties/forms/shared/CalculationErrorList.vue';
export default {
components: {
CalculationErrorList,
},
mixins: [propertyViewerMixin],
}
</script>

View File

@@ -3,12 +3,13 @@
<property-tags :tags="model.tags" />
<div class="layout row wrap justify-space-around">
<div
v-if="model.value !== undefined"
class="mr-3 my-3"
>
<v-layout
v-if="model.value !== undefined"
row
align-center
class="mb-2"
>
<v-icon
class="mr-2"
@@ -21,15 +22,34 @@
:value="model.value"
/>
</v-layout>
</div>
<div
v-if="model.weight !== undefined"
class="my-3"
>
<v-layout
row
align-center
>
<v-icon
class="mr-2"
x-large
>
$vuetify.icons.cash
</v-icon>
<coin-value
class="title mr-2"
:value="model.contentsValue"
/>
<span class="title">
contents
</span>
</v-layout>
</div>
<div
class="my-3"
>
<v-layout
v-if="model.weight !== undefined"
row
align-center
justify-end
class="mb-2"
>
<span class="title mr-2">
{{ model.weight }} lb
@@ -41,6 +61,45 @@
$vuetify.icons.weight
</v-icon>
</v-layout>
<v-layout
row
align-center
justify-end
:class="{'mb-2': model.contentsWeightless}"
>
<span class="title mr-2">
{{ model.contentsWeight }} lb
</span>
<span
class="title"
>
contents
</span>
<v-icon
class="ml-2"
x-large
>
$vuetify.icons.injustice
</v-icon>
</v-layout>
<v-layout
v-if="model.contentsWeightless"
row
align-center
justify-end
>
<span
class="title"
>
Contents weightless
</span>
<v-icon
class="ml-2"
x-large
>
$vuetify.icons.weightless
</v-icon>
</v-layout>
</div>
</div>
<property-description

View File

@@ -89,6 +89,7 @@
row
align-center
justify-end
:class="{'mb-2': model.attuned}"
>
<span class="title mr-2">
{{ model.weight }} lb
@@ -106,6 +107,22 @@
$vuetify.icons.weight
</v-icon>
</v-layout>
<v-layout
v-if="model.attuned"
row
align-center
justify-end
>
<span class="title">
Attuned
</span>
<v-icon
class="ml-2"
x-large
>
$vuetify.icons.spell
</v-icon>
</v-layout>
</div>
</div>
<property-description

View File

@@ -0,0 +1,23 @@
<template lang="html">
<div class="buff-viewer">
<property-name :value="model.name" />
<property-variable-name :value="model.variableName" />
<property-field
name="Roll"
:value="'rollResult' in model ? model.rollResult : model.roll"
/>
<calculation-error-list :errors="model.rollErrors" />
</div>
</template>
<script>
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
import CalculationErrorList from '/imports/ui/properties/forms/shared/CalculationErrorList.vue';
export default {
components: {
CalculationErrorList,
},
mixins: [propertyViewerMixin],
}
</script>

View File

@@ -0,0 +1,21 @@
<template lang="html">
<div class="buff-viewer">
<property-name :value="model.name" />
<property-field
name="Save"
:value="model.stat"
/>
<property-field
name="DC"
:value="'dcResult' in model ? model.dcResult : model.dc"
/>
</div>
</template>
<script>
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
export default {
mixins: [propertyViewerMixin],
}
</script>

View File

@@ -0,0 +1,37 @@
<template lang="html">
<div class="slot-filler-viewer">
<property-name :value="model.name" />
<v-img
v-if="model.picture"
:src="model.picture"
:height="200"
contain
class="slot-card-image"
/>
<property-field
name="Type"
:value="model.slotFillerType"
/>
<property-field
name="Quantity"
:value="model.slotQuantityFilled"
/>
<property-field
name="Condition"
:value="model.slotFillerCondition"
/>
<property-description
:string="model.description"
:calculations="model.descriptionCalculations"
:inactive="model.inactive"
/>
</div>
</template>
<script>
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js';
export default {
mixins: [propertyViewerMixin],
}
</script>

View File

@@ -0,0 +1,41 @@
<template lang="html">
<div class="buff-viewer">
<property-name :value="model.name" />
<property-field
name="Type"
:value="model.slotType"
/>
<property-field
name="Quantity"
:value="'quantityExpectedResult' in model ? model.quantityExpectedResult : model.quantityExpected"
/>
<property-field
name="Condition"
:value="model.slotCondition"
/>
<property-field
v-if="'slotConditionResult' in model"
name="Condition result"
:value="model.slotConditionResult"
/>
<template v-if="model.tags.length">
<div class="caption">
Tags
</div>
<property-tags :tags="model.tags" />
</template>
<property-description
:string="model.description"
:calculations="model.descriptionCalculations"
:inactive="model.inactive"
/>
</div>
</template>
<script>
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
export default {
mixins: [propertyViewerMixin],
}
</script>

View File

@@ -4,7 +4,15 @@
<property-variable-name :value="model.variableName" />
<property-field
name="Maximum prepared spells"
:value="model.maxPreparedResult"
:value="'maxPreparedResult' in model ? model.maxPreparedResult : model.maxPrepared"
/>
<property-field
name="Spell Save DC"
:value="'dcResult' in model ? model.dcResult : model.dcResult"
/>
<property-field
name="Attack roll bonus"
:value="'attackRollBonusResult' in model ? model.attackRollBonusResult : model.attackRollBonus"
/>
<property-description
:string="model.description"

View File

@@ -0,0 +1,31 @@
<template lang="html">
<div class="toggle-viewer">
<property-name :value="model.name" />
<property-field
v-if="model.disabled || model.enabled"
name="Status"
:value="model.enabled ? 'Enabled' : 'Disabled'"
/>
<template
v-else-if="model.condition"
>
<property-field
name="Condition"
:value="model.condition"
/>
<property-field
v-if="'toggleResult' in model"
name="Result"
:value="model.toggleResult"
/>
</template>
</div>
</template>
<script>
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
export default {
mixins: [propertyViewerMixin],
}
</script>

View File

@@ -1,11 +1,11 @@
<template lang="html">
<div v-if="value !== undefined || $slots.default">
<div class="caption">
{{name}}
</div>
<div class="caption">
{{ name }}
</div>
<p class="ml-2 subheading">
<slot>
{{value}}
{{ value }}
</slot>
</p>
</div>
@@ -15,7 +15,7 @@
export default {
props: {
name: String,
value: [String, Number],
value: [String, Number, Boolean],
}
}
</script>

View File

@@ -5,6 +5,7 @@ import AttributeViewer from '/imports/ui/properties/viewers/AttributeViewer.vue'
import BuffViewer from '/imports/ui/properties/viewers/BuffViewer.vue';
import ContainerViewer from '/imports/ui/properties/viewers/ContainerViewer.vue';
import ClassLevelViewer from '/imports/ui/properties/viewers/ClassLevelViewer.vue';
import ConstantViewer from '/imports/ui/properties/viewers/ConstantViewer.vue';
import DamageViewer from '/imports/ui/properties/viewers/DamageViewer.vue';
import DamageMultiplierViewer from '/imports/ui/properties/viewers/DamageMultiplierViewer.vue';
import EffectViewer from '/imports/ui/properties/viewers/EffectViewer.vue';
@@ -13,10 +14,14 @@ import FolderViewer from '/imports/ui/properties/viewers/FolderViewer.vue';
import ItemViewer from '/imports/ui/properties/viewers/ItemViewer.vue';
import NoteViewer from '/imports/ui/properties/viewers/NoteViewer.vue';
import ProficiencyViewer from '/imports/ui/properties/viewers/ProficiencyViewer.vue';
//import RollViewer from '/imports/ui/properties/viewers/RollViewer.vue';
import RollViewer from '/imports/ui/properties/viewers/RollViewer.vue';
import SkillViewer from '/imports/ui/properties/viewers/SkillViewer.vue';
import SavingThrowViewer from '/imports/ui/properties/viewers/SavingThrowViewer.vue';
import SlotViewer from '/imports/ui/properties/viewers/SlotViewer.vue';
import SlotFillerViewer from '/imports/ui/properties/viewers/SlotFillerViewer.vue';
import SpellListViewer from '/imports/ui/properties/viewers/SpellListViewer.vue';
import SpellViewer from '/imports/ui/properties/viewers/SpellViewer.vue';
import ToggleViewer from '/imports/ui/properties/viewers/ToggleViewer.vue';
export default {
action: ActionViewer,
@@ -26,6 +31,7 @@ export default {
buff: BuffViewer,
container: ContainerViewer,
classLevel: ClassLevelViewer,
constant: ConstantViewer,
damage: DamageViewer,
damageMultiplier: DamageMultiplierViewer,
effect: EffectViewer,
@@ -34,8 +40,12 @@ export default {
item: ItemViewer,
note: NoteViewer,
proficiency: ProficiencyViewer,
// roll: RollViewer,
propertySlot: SlotViewer,
roll: RollViewer,
savingThrow: SavingThrowViewer,
slotFiller: SlotFillerViewer,
skill: SkillViewer,
spellList: SpellListViewer,
spell: SpellViewer,
toggle: ToggleViewer,
};

View File

@@ -0,0 +1,153 @@
<template lang="html">
<dialog-base>
<v-toolbar-title slot="toolbar">
Delete User Account
</v-toolbar-title>
<div>
<h2>Are you sure you want to delete your account?</h2>
<v-alert
:value="true"
icon="warning"
color="error"
outline
>
Deleted accounts can not be recovered
</v-alert>
<p>We will immediately delete your account and all of your data</p>
<p>Your username will become available to anyone on DiceCloud</p>
<template v-if="characters.length">
<h3 v-if="characters.length > 1">
These {{ characters.length }} characters will be deleted:
</h3>
<h3 v-else>
This character will be deleted:
</h3>
<v-list>
<creature-list-tile
v-for="character in characters"
:key="character._id"
:model="character"
/>
</v-list>
</template>
<template v-if="libraries.length">
<h3 v-if="libraries.length > 1">
These {{ libraries.length }} libraries will be deleted:
</h3>
<h3 v-else>
This library will be deleted:
</h3>
<v-list>
<creature-list-tile
v-for="library in libraries"
:key="library._id"
:model="library"
/>
</v-list>
</template>
<v-layout
column
align-start
>
<v-text-field
v-if="user.username"
v-model="usernameInput"
label="Type your username or email"
style="width: 350px;"
:error-messages="usernameInputValid ? undefined : ' '"
:append-icon="usernameInputValid ? 'done' : undefined"
/>
<v-text-field
v-model="verificationInput"
label="To verify type 'delete my account'"
style="width: 350px;"
:error-messages="verificationInputValid ? undefined : ' '"
:append-icon="verificationInputValid ? 'done' : undefined"
/>
<v-btn
class="mt-4"
color="error"
:disabled="!valid"
@click="deleteAccount"
>
Permanently delete account
</v-btn>
</v-layout>
</div>
<div
slot="actions"
class="layout row justify-end"
>
<v-btn
flat
@click="$store.dispatch('popDialogStack')"
>
Cancel
</v-btn>
</div>
</dialog-base>
</template>
<script>
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import Creatures from '/imports/api/creature/Creatures.js';
import Libraries from '/imports/api/library/Libraries.js';
import CreatureListTile from '/imports/ui/creature/CreatureListTile.vue';
export default {
components: {
DialogBase,
CreatureListTile,
},
data(){return {
usernameInput: '',
verificationInput: '',
};},
meteor: {
$subscribe: {
'ownedDocuments'(){
return [];
},
},
characters(){
return Creatures.find({owner: Meteor.userId()});
},
libraries(){
return Libraries.find({owner: Meteor.userId()});
},
user(){
return Meteor.user();
},
},
computed:{
usernameInputValid(){
let username = this.user.username;
if (!username) return true;
let input = this.usernameInput;
if (!input) return false;
if (input.toLowerCase() === username.toLowerCase()){
return true;
} else {
return false;
}
},
verificationInputValid(){
let input = this.verificationInput || '';
return input.toLowerCase() === 'delete my account'
},
valid(){
return this.usernameInputValid && this.verificationInputValid;
}
},
methods: {
deleteAccount(){
this.$router.push('/');
Meteor.users.deleteMyAccount.call();
this.$store.dispatch('popDialogStack');
},
},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -25,7 +25,8 @@ for (const name in SVG_ICONS) {
}
Vue.use(VueMeteorTracker);
Vue.config.meteor.freeze = true
Vue.config.meteor.freeze = true;
Vue.config.devtools = true;
Vue.use(Vuetify, {
theme,
iconfont: 'md',