Compare commits

...

41 Commits

Author SHA1 Message Date
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
Stefan Zermatten
3801b17fde Attacks can now critical hit. criticalHitTarget overrides the roll required 2021-02-22 14:07:12 +02:00
Stefan Zermatten
88133a2fa3 Saving throws now work in actions 2021-02-22 12:38:21 +02:00
Stefan Zermatten
d00eedac19 Rolls now work in actions 2021-02-22 11:55:08 +02:00
Stefan Zermatten
6571fb860a Toggles now work in actions to make choices based on action context 2021-02-22 11:36:30 +02:00
Stefan Zermatten
8148f4d701 Fixed: Library nodes are published in order to prevent disordered building of the tree 2021-02-21 17:05:09 +02:00
Stefan Zermatten
523c34b719 Fixed: Slots that use conditions now only hide on falsey value (false, 0, '') 2021-02-21 17:01:24 +02:00
Stefan Zermatten
e833fba870 Fixed: bug that stopped buffs being deleted 2021-02-21 16:35:35 +02:00
Stefan Zermatten
f3e191c12e Fixed: Inserting properties to the tree now animate correctly to the inserted property 2021-02-20 16:00:40 +02:00
Stefan Zermatten
33415275a3 Item tiles are now smaller in the inventory view 2021-02-20 15:53:58 +02:00
Stefan Zermatten
3b1151d987 Fixed notes without summaries are no longer oversized 2021-02-20 15:52:15 +02:00
Stefan Zermatten
4288f98f7c Fixed: stats with no ability selected have an ability modifier of 0 instead of NaN 2021-02-20 15:50:25 +02:00
Stefan Zermatten
1a2ef8a4a2 Fixed: markdown images no longer overflow their container width 2021-02-20 15:45:45 +02:00
Stefan Zermatten
10e9a5faa8 Notes now show both summary and description in viewer 2021-02-20 15:41:30 +02:00
Stefan Zermatten
53594c0004 Fixed: items in containers not following tree order 2021-02-20 15:38:51 +02:00
Stefan Zermatten
e068675b46 Fixed and improved: Discord webhooks are working again with a new format 2021-02-20 15:27:20 +02:00
Stefan Zermatten
067f5df36e Fixed: Emptying the search bar in slot filler dialog now correctly clears the search 2021-02-20 10:17:35 +02:00
Stefan Zermatten
6113d86059 Fixed typo in calling a function in Constants autovalue 2021-02-16 11:19:50 +02:00
Stefan Zermatten
e3862bcdd9 Fixed constants autovalue 2021-02-16 10:49:18 +02:00
75 changed files with 1338 additions and 293 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

@@ -2,23 +2,12 @@ import spendResources from '/imports/api/creature/actions/spendResources.js'
import embedInlineCalculations from '/imports/api/creature/computation/afterComputation/embedInlineCalculations.js';
export default function applyAction({prop, log}){
spendResources(prop);
// If this is not the top level action, we can add its name to the log
if (log.content.length){
log.content.push({name: prop.name});
}
let content = { name: prop.name };
if (prop.summary){
log.content.push({
description: embedInlineCalculations(
prop.summary, prop.summaryCalculations
),
});
}
if (prop.description){
log.content.push({
description: embedInlineCalculations(
prop.description, prop.descriptionCalculations
),
});
content.description = embedInlineCalculations(
prop.summary, prop.summaryCalculations
);
}
log.content.push(content);
spendResources({prop, log});
}

View File

@@ -16,10 +16,16 @@ export default function applyAdjustment({
try {
var {result, errors} = evaluateString(prop.amount, scope, 'reduce');
if (typeof result !== 'number') {
log.content.push({error: errors.join(', ') || 'Something went wrong'});
log.content.push({
name: 'Attribute damage',
error: errors.join(', ') || 'Something went wrong',
});
}
} catch (e){
log.content.push({error: e.toString()});
log.content.push({
name: 'Attribute damage',
error: e.toString(),
});
}
if (damageTargets) {
damageTargets.forEach(target => {
@@ -33,12 +39,14 @@ export default function applyAdjustment({
value: result
});
log.content.push({
name: 'Attribute damage',
resultPrefix: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''}`,
result: `${-result}`,
});
});
} else {
log.content.push({
name: 'Attribute damage',
resultPrefix: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''}`,
result: `${-result}`,
});

View File

@@ -3,12 +3,21 @@ import roll from '/imports/parser/roll.js';
export default function applyAttack({
prop,
log,
actionContext,
creature,
}){
let result = roll(1, 20)[0] + prop.rollBonusResult;
let value = roll(1, 20)[0];
actionContext.attackRoll = {value};
let criticalHitTarget = creature.variables.criticalHitTarget &&
creature.variables.criticalHitTarget.currentValue || 20;
let criticalHit = value >= criticalHitTarget;
if (criticalHit) actionContext.criticalHit = {value: true};
let result = value + prop.rollBonusResult;
actionContext.toHit = {value: result};
log.content.push({
// If this is not the first item in the log content, give it a name
name: log.content.length ? prop.name + ' attack' : undefined,
name: criticalHit ? 'Critical Hit!' : 'To Hit',
resultPrefix: `1d20 [${value}] + ${prop.rollBonusResult} = `,
result,
details: 'to hit',
});
}

View File

@@ -4,6 +4,7 @@ import {
} from '/imports/api/parenting/parenting.js';
import {setDocToLastOrder} from '/imports/api/parenting/order.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
export default function applyBuff({
prop,
@@ -57,5 +58,10 @@ function copyNodeListToTarget(propList, target, oldParent){
collection: CreatureProperties,
doc: propList[0],
});
CreatureProperties.batchInsert(propList);
CreatureProperties.batchInsert(propList, () => {
// This insert is racing the main recompute, recmpute again after it's
// certainly finished
recomputeCreatureByDoc(target);
});
}

View File

@@ -1,6 +1,7 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import dealDamage from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import { CompilationContext } from '/imports/parser/parser.js';
export default function applyDamage({
prop,
@@ -14,8 +15,19 @@ export default function applyDamage({
...creature.variables,
...actionContext,
};
if (targets.length === 1){
scope.target = targets[0].variables;
}
let criticalHit = !!(
actionContext.criticalHit &&
actionContext.criticalHit.value &&
prop.damageType !== 'healing' // Can't critically heal
);
let context = new CompilationContext({
doubleRolls: criticalHit,
});
try {
var {result, errors} = evaluateString(prop.amount, scope, 'reduce');
var {result, errors} = evaluateString(prop.amount, scope, 'reduce', context);
if (typeof result !== 'number') {
log.content.push({
error: errors.join(', '),
@@ -26,8 +38,13 @@ export default function applyDamage({
error: e.toString(),
});
}
let suffix = (criticalHit ? ' critical ' : '') +
prop.damageType +
(prop.damageType !== 'healing' ? ' damage': '');
if (damageTargets && damageTargets.length) {
damageTargets.forEach(target => {
let name = prop.damageType === 'healing' ? 'Healing' : 'Damage';
if (prop.target === 'each'){
result = evaluateString(prop.amount, scope, 'reduce');
}
@@ -38,25 +55,24 @@ export default function applyDamage({
});
if (target._id === creature._id){
log.content.push({
name,
result: damageDealt,
details: `${prop.damageType}`+
`${prop.damageType !== 'healing' ? ' damage': ''} to self`,
details: suffix + 'to self',
});
} else {
log.content.push({
name,
resultPrefix: 'Dealt ',
result: damageDealt,
details: `${prop.damageType}` +
`${prop.damageType !== 'healing'? ' damage': ''}` +
`${target.name && ' to '}${target.name}`,
details: suffix + `${target.name && ' to '}${target.name}`,
});
insertCreatureLog.call({
log: {
content: [{
name,
resultPrefix: 'Recieved ',
result: damageDealt,
details: `${prop.damageType}` +
`${prop.damageType !== 'healing'? ' damage': ''}`
details: suffix,
}],
creatureId: target._id,
}
@@ -65,8 +81,9 @@ export default function applyDamage({
});
} else {
log.content.push({
name: prop.damageType === 'healing' ? 'Healing' : 'Damage',
result,
details: `${prop.damageType}${prop.damageType !== 'healing'? ' damage': ''}`,
details: suffix,
});
}
}

View File

@@ -1,8 +1,11 @@
import applyAction from '/imports/api/creature/actions/applyAction.js';
import applyAdjustment from '/imports/api/creature/actions/applyAdjustment.js';
import applyAttack from '/imports/api/creature/actions/applyAttack.js';
import applyDamage from '/imports/api/creature/actions/applyDamage.js';
import applyBuff from '/imports/api/creature/actions/applyBuff.js';
import applyDamage from '/imports/api/creature/actions/applyDamage.js';
import applyRoll from '/imports/api/creature/actions/applyRoll.js';
import applyToggle from '/imports/api/creature/actions/applyToggle.js';
import applySave from '/imports/api/creature/actions/applySave.js';
function applyProperty(options){
let prop = options.prop;
@@ -11,8 +14,13 @@ function applyProperty(options){
if (prop.applied === true){
return false;
}
// Only ignore toggles if they wont be computed
} else if (prop.type === 'toggle') {
if (prop.disabled) return false;
if (prop.enabled) return true;
if (!prop.condition) return false;
// Ignore inactive props of other types
} else if (prop.inactive === true){
} else if (prop.deactivatedBySelf === true){
return false;
}
switch (prop.type){
@@ -33,40 +41,41 @@ function applyProperty(options){
case 'buff':
applyBuff(options);
break;
case 'toggle':
return applyToggle(options);
case 'roll':
// applyRoll(options);
applyRoll(options);
break;
case 'savingThrow':
// applySavingThrow(options);
break;
return applySave(options);
}
return true;
}
export default function applyProperties({
forest,
creature,
targets,
actionContext,
log,
}){
forest.forEach(child => {
let walkChildren = applyProperty({
prop: child.node,
children: child.children,
creature,
targets,
actionContext,
log,
});
if (walkChildren){
applyProperties({
forest: child.children,
creature,
targets,
actionContext,
log,
function applyPropertyAndWalkChildren({prop, children, targets, ...options}){
let shouldKeepWalking = applyProperty({ prop, children, targets, ...options });
if (shouldKeepWalking){
applyProperties({ forest: children, targets, ...options,});
}
}
export default function applyProperties({ forest, targets, ...options}){
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, children, ...options});
});
} else {
applyPropertyAndWalkChildren({prop, children, targets, ...options});
}
});
}
function shouldSplit(prop){
if (prop.target === 'each'){
return true;
}
}

View File

@@ -0,0 +1,32 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
export default function applyRoll({
prop,
creature,
actionContext,
log,
}){
let scope = {
...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(),
});
}
}

View File

@@ -0,0 +1,79 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import CreaturesProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import roll from '/imports/parser/roll.js';
export default function applySave({
prop,
creature,
actionContext,
log,
}){
let scope = {
...creature.variables,
...actionContext,
};
try {
// Calculate the DC
var {result, errors} = evaluateString(prop.dc, scope, 'reduce');
let dc = result;
log.content.push({
name: prop.name,
resultPrefix: ' DC ',
result,
});
if (errors.length) {
log.content.push({
error: errors.join(', '),
});
return false;
}
if (prop.target === 'self'){
let save = CreaturesProperties.findOne({
'ancestors.id': creature._id,
type: 'skill',
skillType: 'save',
variableName: prop.stat,
removed: {$ne: true},
inactive: {$ne: true},
});
if (!save){
log.content.push({
error: 'No saving throw found: ' + prop.stat,
});
return;
}
let value, values, resultPrefix;
if (save.advantage === 1){
values = roll(2, 20).sort().reverse();
value = values[0];
resultPrefix = `Advantage: 1d20 [${values[0]},~~${values[1]}~~] + ${save.value} = `
} else if (save.advantage === -1){
values = roll(2, 20).sort();
value = values[0];
resultPrefix = `Disadvantage: 1d20 [${values[0]},~~${values[1]}~~] + ${save.value} = `
} else {
values = roll(1, 20);
value = values[0];
resultPrefix = `1d20 [${value}] + ${save.value} = `
}
actionContext.savingThrowRoll = {value};
let result = value + save.value;
actionContext.savingThrow = {value: result};
let saveSuccess = result >= dc;
log.content.push({
name: 'Save',
resultPrefix,
result,
details: saveSuccess ? 'Passed' : 'Failed'
});
return !saveSuccess;
} else {
// TODO
return true;
}
} catch (e){
log.content.push({
error: e.toString(),
});
}
}

View File

@@ -0,0 +1,35 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
export default function applyToggle({
prop,
creature,
actionContext,
log,
}){
let scope = {
...creature.variables,
...actionContext,
};
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;
}
log.content.push({
name: prop.name,
resultPrefix: prop.condition + ' = ',
result,
});
return !!result;
} catch (e){
log.content.push({
error: e.toString(),
});
}
}

View File

@@ -3,12 +3,13 @@ import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/Creatures.js';
import CreatureLogs, { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
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';
const doAction = new ValidatedMethod({
name: 'creatureProperties.doAction',
@@ -43,6 +44,8 @@ const doAction = new ValidatedMethod({
});
doActionWork({action, creature, targets, method: this});
// The acting creature might have used ammo
recomputeInventory(creature._id);
// recompute creatures
recomputeCreatureByDoc(creature);
targets.forEach(target => {
@@ -60,7 +63,6 @@ export function doActionWork({
}){
// Create the log
let log = CreatureLogSchema.clean({
name: action.name,
creatureId: creature._id,
creatureName: creature.name,
});

View File

@@ -2,28 +2,30 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
export default function spendResources(action){
export default function spendResources({prop, log}){
// Check Uses
if (action.usesUsed >= action.usesResult){
if (prop.usesUsed >= prop.usesResult){
throw new Meteor.Error('Insufficient Uses',
'This action has no uses left');
'This prop has no uses left');
}
// Resources
if (action.insufficientResources){
if (prop.insufficientResources){
throw new Meteor.Error('Insufficient Resources',
'This creature doesn\'t have sufficient resources to perform this action');
'This creature doesn\'t have sufficient resources to perform this prop');
}
// Items
let itemQuantityAdjustments = [];
action.resources.itemsConsumed.forEach(itemConsumed => {
let spendLog = [];
let gainLog = [];
prop.resources.itemsConsumed.forEach(itemConsumed => {
if (!itemConsumed.itemId){
throw new Meteor.Error('Ammo not selected',
'No ammo was selected for this action');
'No ammo was selected for this prop');
}
let item = CreatureProperties.findOne(itemConsumed.itemId);
if (!item || item.ancestors[0].id !== action.ancestors[0].id){
if (!item || item.ancestors[0].id !== prop.ancestors[0].id){
throw new Meteor.Error('Ammo not found',
'The action\'s ammo was not found on the creature');
'The prop\'s ammo was not found on the creature');
}
if (!item.equipped){
throw new Meteor.Error('Ammo not equipped',
@@ -35,19 +37,36 @@ export default function spendResources(action){
operation: 'increment',
value: itemConsumed.quantity,
});
let logName = item.name;
if (itemConsumed.quantity > 1 || itemConsumed.quantity < -1){
logName = item.plural || logName;
}
if (itemConsumed.quantity > 0){
spendLog.push(logName + ': ' + itemConsumed.quantity);
} else if (itemConsumed.quantity < 0){
gainLog.push(logName + ': ' + -itemConsumed.quantity);
}
});
// No more errors should be thrown after this line
// Now that we have confirmed that there are no errors, do actual work
//Items
itemQuantityAdjustments.forEach(adjustQuantityWork);
// Use uses
CreatureProperties.update(action._id, {
$inc: {usesUsed: 1}
}, {
selector: action
});
if (prop.usesResult){
CreatureProperties.update(prop._id, {
$inc: {usesUsed: 1}
}, {
selector: prop
});
log.content.push({
name: 'Uses left',
result: prop.usesResult - prop.usesUsed - 1,
});
}
// Damage stats
action.resources.attributesConsumed.forEach(attConsumed => {
prop.resources.attributesConsumed.forEach(attConsumed => {
if (!attConsumed.quantity) return;
let stat = CreatureProperties.findOne(attConsumed.statId);
damagePropertyWork({
@@ -55,5 +74,20 @@ export default function spendResources(action){
operation: 'increment',
value: attConsumed.quantity,
});
if (attConsumed.quantity > 0){
spendLog.push(stat.name + ': ' + attConsumed.quantity);
} else if (attConsumed.quantity < 0){
gainLog.push(stat.name + ': ' + -attConsumed.quantity);
}
});
// Log all the spending
if (gainLog.length) log.content.push({
name: 'Gained',
description: gainLog.join('\n'),
});
if (spendLog.length) log.content.push({
name: 'Spent',
description: spendLog.join('\n'),
});
}

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,7 +1,7 @@
import { parse, CompilationContext } from '/imports/parser/parser.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default function evaluateString(string, scope, fn = 'compile'){
export default function evaluateString(string, scope, fn = 'compile', context){
let errors = [];
if (!string){
errors.push('No string provided');
@@ -18,7 +18,9 @@ export default function evaluateString(string, scope, fn = 'compile'){
errors.push(e);
return {result: string, errors};
}
let context = new CompilationContext();
if (!context){
context = new CompilationContext({});
}
let result = node[fn](scope, context);
if (result instanceof ConstantNode){
return {result: result.value, errors: context.errors}

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

@@ -76,7 +76,7 @@ function combineAttribute(stat, aggregator, memo){
function combineSkill(stat, aggregator, memo){
// Skills are based on some ability Modifier
let ability = memo.statsByVariableName[stat.ability]
let ability = stat.ability && memo.statsByVariableName[stat.ability]
if (stat.ability && ability){
if (!ability.computationDetails.computed){
computeStat(ability, memo);
@@ -87,6 +87,8 @@ function combineSkill(stat, aggregator, memo){
[ability._id],
ability.dependencies,
);
} else {
stat.abilityMod = 0;
}
// Combine all the child proficiencies
stat.proficiency = stat.baseProficiency || 0;

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;
}
}
@@ -111,19 +117,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

@@ -21,6 +21,9 @@ export default function evaluateCalculation({
context,
dependencies,
};
if (typeof string !== 'string'){
string = string.toString();
}
// Parse the string
let calc;
try {
@@ -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

@@ -43,6 +43,13 @@ let CreaturePropertySchema = new SimpleSchema({
optional: true,
index: 1,
},
// Denormalised flag if this property was made inactive because of its own
// state
deactivatedBySelf: {
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

@@ -4,6 +4,8 @@ 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 recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import INVENTORY_TAGS from '/imports/constants/INVENTORY_TAGS.js';
export function getParentRefByTag(creatureId, tag){
@@ -49,7 +51,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 +59,11 @@ const equipItem = new ValidatedMethod({
},
parentRef,
order: Number.MAX_SAFE_INTEGER,
skipRecompute: true,
});
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',
@@ -18,7 +19,7 @@ const insertProperty = new ValidatedMethod({
run({creatureProperty}) {
let rootCreature = getRootCreatureAncestor(creatureProperty);
assertEditPermission(rootCreature, this.userId);
insertPropertyWork({
return insertPropertyWork({
property: creatureProperty,
creature: rootCreature,
});
@@ -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

@@ -5,6 +5,7 @@ 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 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,6 +53,11 @@ 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);
},

View File

@@ -20,9 +20,16 @@ export default function recomputeInactiveProperties(ancestorId){
CreatureProperties.update({
'ancestors.id': ancestorId,
'_id': {$in: disabledIds},
$or: [{inactive: {$ne: true}}, {deactivatedByAncestor: true}],
$or: [
{inactive: {$ne: true}},
{deactivatedBySelf: {$ne: true}},
{deactivatedByAncestor: true},
],
}, {
$set: {inactive: true},
$set: {
inactive: true,
deactivatedBySelf: true,
},
$unset: {deactivatedByAncestor: 1},
}, {
multi: true,
@@ -31,7 +38,10 @@ export default function recomputeInactiveProperties(ancestorId){
// Decendants of inactive properties
CreatureProperties.update({
'ancestors.id': {$eq: ancestorId, $in: disabledIds},
$or: [{inactive: {$ne: true}}, {deactivatedByAncestor: {$ne: true}}],
$or: [
{inactive: {$ne: true}},
{deactivatedByAncestor: {$ne: true}},
],
}, {
$set: {
inactive: true,
@@ -46,7 +56,10 @@ export default function recomputeInactiveProperties(ancestorId){
CreatureProperties.update({
'ancestors.id': {$eq: ancestorId, $nin: disabledIds},
'_id': {$nin: disabledIds},
$or: [{inactive: true}, {deactivatedByAncestor: true}],
$or: [
{inactive: true},
{deactivatedByAncestor: true},
],
}, {
$unset: {
inactive: 1,

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.carried === false){
data.weightCarried = 0;
data.valueCarried = 0;
}
if (node.contentsWeightless){
data.weightCarried = node.weight;
}
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

@@ -18,13 +18,10 @@ if (Meteor.isServer){
let CreatureLogs = new Mongo.Collection('creatureLogs');
let CreatureLogSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
content: {
type: Array,
defaultValue: [],
maxCount: 25,
},
'content.$': {
type: LogContentSchema,
@@ -68,11 +65,45 @@ function removeOldLogs(creatureId){
});
}
function logToMessageData(log){
let embed = {
fields: [],
};
log.content.forEach(c => {
let field = {};
let descriptionField = {};
if (c.name) field.name = c.name;
let valueArray = [];
if (c.error) valueArray.push(`*${c.error}*`);
if (c.resultPrefix) valueArray.push(`${c.resultPrefix}`);
if (c.result) valueArray.push(`\`${c.result}\``);
if (c.details) valueArray.push(c.details);
if (valueArray.length) field.value = valueArray.join(' ');
if (c.description){
if (!field.value){
field.value = c.description;
} else {
descriptionField.value = c.description;
}
}
if (field.name || field.value){
if (!field.name) field.name = '\u200b';
if (!field.value) field.value = '\u200b';
embed.fields.push(field);
}
if (descriptionField.value){
descriptionField.name = '\u200b';
embed.fields.push(descriptionField);
}
});
return { embeds: [embed] };
}
function logWebhook({log, creature}){
if (Meteor.isServer){
sendWebhookAsCreature({
creature,
content: log.text,
data: logToMessageData(log),
});
}
}

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

@@ -38,20 +38,21 @@ let ConstantSchema = new SimpleSchema({
return;
}
let string = calc.value;
if (!string) return [];
// Evaluate the calculation with no scope
let {result, context} = parseString(string);
// Any existing errors will result in an early failure
if (context.errors.length) return context.errors;
if (context && context.errors.length) return context.errors;
// Ban variables in constants if necessary
result && result.traverse(node => {
if (node instanceof SymbolNode || node instanceof AccessorNode){
context.storeError()({
context.storeError({
type: 'error',
message: 'Variables can\'t be used to define a constant'
});
}
});
return context.errors;
return context && context.errors || [];
}
},
'errors.$':{
@@ -62,7 +63,7 @@ let ConstantSchema = new SimpleSchema({
function parseString(string, fn = 'compile'){
let context = new CompilationContext();
if (!string){
return {result: string, errors: []};
return {result: string, context};
}
// Parse the string using mathjs
@@ -72,7 +73,7 @@ function parseString(string, fn = 'compile'){
} catch (e) {
let message = prettifyParseError(e);
context.storeError({type: 'error', message});
return {result: string, errors: context.errors};
return {context};
}
let result = node[fn]({/*empty scope*/}, context);
return {result, context}

View File

@@ -1,5 +1,6 @@
import SimpleSchema from 'simpl-schema';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
/**
* Rolls are children to actions or other rolls, they are triggered with 0 or
@@ -20,6 +21,17 @@ import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
* child rolls are applied
*/
let RollSchema = new SimpleSchema({
name: {
type: String,
defaultValue: 'New Roll',
},
// The technical, lowercase, single-word name used in formulae
variableName: {
type: String,
regEx: VARIABLE_NAME_REGEX,
min: 2,
defaultValue: 'newRoll',
},
// The roll, can be simplified, but only computed in context
roll: {
type: String,

View File

@@ -8,11 +8,22 @@ let SavingThrowSchema = new SimpleSchema ({
type: String,
optional: true,
},
// The computed DC
dc: {
type: String,
optional: true,
},
// The variable name of ability the save to roll
// Who this saving throw applies to
target: {
type: String,
defaultValue: 'every',
allowedValues: [
'self', // the character who took the action
'each', // rolled once for `each` target
'every', // rolled once and applied to `every` target
],
},
// The variable name of save to roll
stat: {
type: String,
optional: true,

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

@@ -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,27 +1,24 @@
import Discord from 'discord.js'
export default function sendWebhook({webhookURL, message, options}){
export default function sendWebhook({webhookURL, data = {}}){
//webhookURL = https://discordapp.com/api/webhooks/<id>/<token>
let urlArray = webhookURL.split('/');
let token = urlArray.pop();
let id = urlArray.pop();
// prevent discord mention exploit
options.disableMentions = 'all';
data.disableMentions = 'all';
const hook = new Discord.WebhookClient(id, token);
// Send a message using the webhook
hook.send(message, options)
hook.send(data);
}
export function sendWebhookAsCreature({creature, content, embeds}){
export function sendWebhookAsCreature({creature, data = {}}){
if (!creature || !creature.settings || !creature.settings.discordWebhook) return;
data.username = creature.name;
data.avatarURL = creature.avatarPicture;
sendWebhook({
webhookURL: creature.settings.discordWebhook,
message: content,
options: {
username: creature.name,
avatarURL: creature.avatarPicture,
embeds,
},
data,
});
}

View File

@@ -52,6 +52,8 @@ Meteor.publish('library', function(libraryId){
libraryCursor,
LibraryNodes.find({
'ancestors.id': libraryId,
}, {
sort: {order: 1},
}),
];
});

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
@@ -73,6 +73,7 @@ Meteor.publish('slotFillers', function(slotId){
}
}
} else {
delete filter.$text
options = {sort: {
name: 1,
order: 1,

View File

@@ -1,6 +1,9 @@
<template lang="html">
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="compiledMarkdown" />
<div
class="markdown"
v-html="compiledMarkdown"
/>
</template>
<script>
@@ -21,3 +24,10 @@
},
}
</script>
<style lang="css">
.markdown img {
max-width: 100%;
margin: 8px 0;
}
</style>

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

@@ -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

@@ -320,7 +320,7 @@
<script>
import Creatures from '/imports/api/creature/Creatures.js';
import { softRemoveProperty } from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import AttributeCard from '/imports/ui/properties/components/attributes/AttributeCard.vue';
import AbilityListTile from '/imports/ui/properties/components/attributes/AbilityListTile.vue';

View File

@@ -205,8 +205,8 @@
creatureProperty.parent = {collection: 'creatures', id: that.creatureId};
creatureProperty.ancestors = [ {collection: 'creatures', id: that.creatureId}];
setDocToLastOrder({collection: CreatureProperties, doc: creatureProperty});
let creaturePropertyId = insertProperty.call({creatureProperty});
return creaturePropertyId;
let id = insertProperty.call({creatureProperty});
return `tree-node-${id}`;
}
});
},
@@ -217,10 +217,11 @@
elementId: 'insert-creature-property-fab',
callback(libraryNode){
if (!libraryNode) return;
insertPropertyFromLibraryNode.call({
let id = insertPropertyFromLibraryNode.call({
nodeId: libraryNode._id,
parentRef: {collection: 'creatures', id: that.creatureId},
});
return `tree-node-${id}`;
}
});
},

View File

@@ -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;

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"
@@ -120,7 +120,7 @@ export default {
'ancestors.id': this.creatureId,
type: 'propertySlot',
$or: [
{slotConditionResult: true},
{slotConditionResult: {$nin: [false, 0, '']}},
{slotConditionResult: {$exists: false}},
],
removed: {$ne: true},

View File

@@ -1,26 +1,19 @@
<template lang="html">
<v-card
class="ma-2"
class="ma-2 log-entry"
>
<v-card-title
v-if="model.name"
class="pa-2"
>
<h3>
{{ model.name }}
</h3>
</v-card-title>
<v-card-text
v-if="model.text || (model.content && model.content.length)"
class="pa-2"
>
{{ model.text }}
<div
v-for="(content, index) in model.content"
:key="index"
class="content-line"
>
{{ content.name }}
<h4>
{{ content.name }}
</h4>
<span
v-if="content.error"
class="error"
@@ -35,7 +28,7 @@
>{{ content.details }}</span>
<markdown-text
v-if="content.description"
class="details"
class="description"
:markdown="content.description"
/>
</div>
@@ -69,3 +62,9 @@ export default {
display: inline-block;
}
</style>
<style lang="css">
.log-entry .description > p:last-of-type{
margin-bottom: 0;
}
</style>

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.contentsWeight ? 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: {
@@ -59,6 +86,8 @@ export default {
removed: {$ne: true},
equipped: {$ne: true},
deactivatedByAncestor: {$ne: true},
}, {
sort: {order: 1},
});
},
}

View File

@@ -1,6 +1,5 @@
<template lang="html">
<v-list
two-line
dense
class="item-list"
>

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

@@ -10,7 +10,7 @@
<v-card-title class="title">
{{ model.name }}
</v-card-title>
<v-card-text>
<v-card-text v-if="model.summary">
<property-description
:string="model.summary"
:calculations="model.summaryCalculations"

View File

@@ -1,5 +1,21 @@
<template lang="html">
<div class="roll-form">
<div class="layout row wrap">
<text-field
label="Name"
:value="model.name"
:error-messages="errors.name"
@change="change('name', ...arguments)"
/>
<text-field
label="Variable name"
:value="model.variableName"
style="flex-basis: 300px;"
hint="Use this name in action formulae to refer to the result of this roll"
:error-messages="errors.variableName"
@change="change('variableName', ...arguments)"
/>
</div>
<text-field
ref="focusFirst"
label="Roll"

View File

@@ -8,6 +8,7 @@
@change="change('name', ...arguments)"
/>
<text-field
ref="focusFirst"
label="DC"
:value="model.dc"
:error-messages="errors.dc"
@@ -21,6 +22,15 @@
:error-messages="errors.stat"
@change="change('stat', ...arguments)"
/>
<smart-select
label="Target"
:hint="targetOptionHint"
:items="targetOptions"
:value="model.target"
:error-messages="errors.target"
:menu-props="{auto: true, lazy: true}"
@change="change('target', ...arguments)"
/>
<smart-combobox
label="Tags"
class="mr-2"
@@ -40,5 +50,34 @@ import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormM
export default {
mixins: [saveListMixin, propertyFormMixin],
computed: {
targetOptions(){
return [
{
text: 'Self',
value: 'self',
}, {
text: 'Roll once for each target',
value: 'each',
}, {
text: 'Roll once and apply to every target',
value: 'every',
},
];
},
targetOptionHint(){
let hints = {
self: 'The damage will be applied to the character\'s own attribute when taking the action',
target: 'The damage will be applied to the target of the action',
each: 'The damage will be rolled separately for each of the targets of the action',
every: 'The damage will be rolled once and applied to each of the targets of the action',
};
if (this.parentTarget === 'singleTarget'){
hints.each = hints.target;
hints.every = hints.target;
}
return hints[this.model.target];
}
},
};
</script>

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

@@ -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

@@ -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

@@ -1,6 +1,12 @@
<template lang="html">
<div class="note-viewer">
<property-name :value="model.name" />
<property-description
:string="model.summary"
:calculations="model.summaryCalculations"
:inactive="model.inactive"
/>
<v-divider class="mt-3 mb-3" />
<property-description
:string="model.description"
:calculations="model.descriptionCalculations"

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,
};