Compare commits

..

75 Commits

Author SHA1 Message Date
Stefan Zermatten
11a2851ac4 Fixed slots with computed expected quantity not hiding when full 2021-03-10 14:51:38 +02:00
Stefan Zermatten
313382fb82 Fixed library subscription issues 2021-03-10 14:40:14 +02:00
Stefan Zermatten
b9ae337a64 Merge branch 'version-2-dev' of https://github.com/ThaumRystra/DiceCloud into version-2-dev 2021-03-02 14:32:08 +02:00
Stefan Zermatten
4dc0a6159b Animated log entries 2021-03-02 14:32:05 +02:00
Stefan Zermatten
e00dfe1532 Changed the color of the log background 2021-03-02 14:31:35 +02:00
Stefan Zermatten
28e1fcabd5 Fixed damage properties by name failing if no properties were found 2021-03-02 14:10:14 +02:00
Stefan Zermatten
2c0496b44b Fixed properties not being made inactive by toggles 2021-03-02 13:56:53 +02:00
Stefan Zermatten
89adda60ec Reworked single page libraries to be more in line with the library view 2021-03-02 13:05:38 +02:00
Stefan Zermatten
8c3710cda3 Started work on single page libraries 2021-03-02 00:24:54 +02:00
Stefan Zermatten
b501b9d830 Fixed crash in skill calculation when level is overridden by an attribute 2021-03-01 18:40:55 +02:00
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
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
123 changed files with 2302 additions and 695 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

@@ -13,34 +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({error: errors.join(', ') || 'Something went wrong'});
}
} catch (e){
log.content.push({error: e.toString()});
}
var {result, context} = evaluateString({
string: prop.amount,
scope,
fn: 'reduce'
});
context.errors.forEach(e => {
log.content.push({
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

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

@@ -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,49 +15,95 @@ export default function applyDamage({
...creature.variables,
...actionContext,
};
try {
var {result, errors} = evaluateString(prop.amount, scope, 'reduce');
if (typeof result !== 'number') {
log.content.push({
error: errors.join(', '),
});
}
} catch (e){
log.content.push({
error: e.toString(),
});
// 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,
});
// 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({
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: `${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}`,
});
// Log the damage received on that creature's log as well
insertCreatureLog.call({
log: {
content: [{
name,
resultPrefix: 'Recieved ',
result: damageDealt,
details: `${prop.damageType}` +
`${prop.damageType !== 'healing'? ' damage': ''}`
details: suffix,
}],
creatureId: target._id,
}
@@ -64,9 +111,11 @@ export default function applyDamage({
}
});
} else {
// There are no targets, just log the result
log.content.push({
result,
details: `${prop.damageType}${prop.damageType !== 'healing'? ' damage': ''}`,
name: prop.damageType === 'healing' ? 'Healing' : 'Damage',
result: result.toString(),
details: suffix,
});
}
}

View File

@@ -1,18 +1,26 @@
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;
if (prop.type === 'buff'){
// ignore only applied buffs
// ignore only applied buffs, don't apply them again
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){
@@ -32,41 +40,42 @@ function applyProperty(options){
break;
case 'buff':
applyBuff(options);
break;
return false;
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,26 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
export default function applyRoll({
prop,
creature,
actionContext,
log,
}){
let scope = {
...creature.variables,
...actionContext,
};
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

@@ -0,0 +1,77 @@
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} = evaluateString({
string: prop.dc,
scope,
fn: 'reduce'
});
let dc = result.value;
log.content.push({
name: prop.name,
resultPrefix: ' DC ',
result: result.toString(),
});
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,34 @@
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;
}
var {result} = evaluateString({
string: prop.condition,
scope,
fn: 'reduce'
});
if (result.constructor.name === 'ErrorNode') {
log.content.push({
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

@@ -3,12 +3,14 @@ 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';
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);
});
},
@@ -60,7 +69,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 || 0) - 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,28 +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'){
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};
context.storeError(e);
return {result: {value: string}, context};
}
let context = new CompilationContext();
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

@@ -7,6 +7,7 @@ export default class ComputationMemo {
constructor(props, creature){
this.statsByVariableName = {};
this.constantsByVariableName = {};
this.constantsById = {};
this.extraStatsByVariableName = {};
this.statsById = {};
this.originalPropsById = {};
@@ -77,11 +78,7 @@ export default class ComputationMemo {
}
addConstant(prop){
prop = this.registerProperty(prop);
if (
!this.constantsByVariableName[prop.variableName]
){
this.constantsByVariableName[prop.variableName] = prop
}
this.constantsById[prop._id] = prop;
}
registerProperty(prop){
this.originalPropsById[prop._id] = cloneDeep(prop);
@@ -256,7 +253,6 @@ const propDetailsByType = {
default(){
return {
toggleAncestors: [],
disabledByToggle: false,
};
},
toggle(){
@@ -264,7 +260,6 @@ const propDetailsByType = {
computed: false,
busyComputing: false,
toggleAncestors: [],
disabledByToggle: false,
};
},
attribute(){
@@ -273,7 +268,6 @@ const propDetailsByType = {
busyComputing: false,
effects: [],
toggleAncestors: [],
disabledByToggle: false,
idsOfSameName: [],
};
},
@@ -284,7 +278,6 @@ const propDetailsByType = {
effects: [],
proficiencies: [],
toggleAncestors: [],
disabledByToggle: false,
idsOfSameName: [],
};
},
@@ -293,26 +286,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

@@ -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;
@@ -94,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;
@@ -110,13 +112,14 @@ function combineSkill(stat, aggregator, memo){
let profBonus = profBonusStat && profBonusStat.value;
if (typeof profBonus !== 'number' && memo.statsByVariableName['level']){
let level = memo.statsByVariableName['level'].value;
let levelProp = memo.statsByVariableName['level'];
let level = levelProp.value;
profBonus = Math.ceil(level / 4) + 1;
if (level._id){
stat.dependencies = union(stat.dependencies, [level._id]);
if (levelProp._id){
stat.dependencies = union(stat.dependencies, [levelProp._id]);
}
if (level.dependencies){
stat.dependencies = union(stat.dependencies, level.dependencies);
if (levelProp.dependencies){
stat.dependencies = union(stat.dependencies, levelProp.dependencies);
}
} else {
stat.dependencies = union(

View File

@@ -0,0 +1,12 @@
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
export default function computeConstant(constant, memo){
// Apply any toggles
applyToggles(constant, memo);
if (constant.deactivatedByToggle) return;
if (
!memo.constantsByVariableName[constant.variableName]
){
memo.constantsByVariableName[constant.variableName] = constant
}
}

View File

@@ -1,8 +1,11 @@
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
import { union } from 'lodash';
export default function computeEndStepProperty(prop, memo){
applyToggles(prop, memo);
switch (prop.type){
case 'action':
case 'spell':
@@ -14,16 +17,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 +78,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 +127,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

@@ -1,4 +1,5 @@
import { forOwn, has, union } from 'lodash';
import applyToggles from '/imports/api/creature/computation/engine/applyToggles.js';
export default function computeLevels(memo){
computeClassLevels(memo);
@@ -7,11 +8,13 @@ export default function computeLevels(memo){
function computeClassLevels(memo){
forOwn(memo.classLevelsById, classLevel => {
applyToggles(classLevel, memo);
// class levels are mutually dependent
classLevel.dependencies = union(
classLevel.dependencies,
Object.keys(memo.classLevelsById)
);
if (classLevel.deactivatedByToggle) return;
let name = classLevel.variableName;
let stat = memo.statsByVariableName[name];
if (!stat){
@@ -29,7 +32,7 @@ function computeClassLevels(memo){
function computeTotalLevel(memo){
let currentLevel = memo.statsByVariableName['level'];
if (!currentLevel){
if (!currentLevel || currentLevel.deactivatedByToggle){
currentLevel = {
value: 0,
dependencies: [],

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.constantsById, constant => {
computeConstant (constant, memo);
});
// Compute level
computeLevels(memo);
// Compute all stats, even if they are overriden

View File

@@ -22,28 +22,25 @@ export default function computeStat(stat, memo){
// Apply any toggles
applyToggles(stat, memo);
if (!stat.computationDetails.disabledByToggle){
// Compute and aggregate all the effects
let aggregator = new EffectAggregator(stat, memo)
each(stat.computationDetails.effects, (effect) => {
computeEffect(effect, memo);
if (effect._id){
stat.dependencies = union(
stat.dependencies,
[effect._id]
);
}
// Compute and aggregate all the effects
let aggregator = new EffectAggregator(stat, memo)
each(stat.computationDetails.effects, (effect) => {
computeEffect(effect, memo);
if (effect.deactivatedByToggle) return;
if (effect._id){
stat.dependencies = union(
stat.dependencies,
effect.dependencies
)
if (!effect.computationDetails.disabledByToggle){
aggregator.addEffect(effect);
}
});
// Conglomerate all the effects to compute the final stat values
combineStat(stat, aggregator, memo);
}
[effect._id]
);
}
stat.dependencies = union(
stat.dependencies,
effect.dependencies
)
aggregator.addEffect(effect);
});
// Conglomerate all the effects to compute the final stat values
combineStat(stat, aggregator, memo);
// Mark the attribute as computed
stat.computationDetails.computed = true;
stat.computationDetails.busyComputing = false;

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

@@ -8,7 +8,10 @@ export default function writeAlteredProperties(memo){
// Loop through all properties on the memo
forOwn(memo.propsById, changed => {
let schema = propertySchemasIndex[changed.type];
if (!schema) return;
if (!schema){
console.warn('No schema for ' + changed.type);
return;
}
let extraIds = changed.computationDetails.idsOfSameName;
let ids;
if (extraIds && extraIds.length){
@@ -19,7 +22,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

@@ -43,6 +43,20 @@ 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 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

@@ -50,7 +50,7 @@ const damagePropertiesByName = new ValidatedMethod({
damagePropertyWork({property, operation, value});
lastProperty = property;
});
recomputePropertyDependencies(lastProperty);
if (lastProperty) recomputePropertyDependencies(lastProperty);
}
});

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

@@ -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}},
],
};
@@ -20,9 +19,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 +37,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,11 +55,18 @@ export default function recomputeInactiveProperties(ancestorId){
CreatureProperties.update({
'ancestors.id': {$eq: ancestorId, $nin: disabledIds},
'_id': {$nin: disabledIds},
$or: [{inactive: true}, {deactivatedByAncestor: true}],
// 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

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

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

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

@@ -4,21 +4,22 @@ import { ComputedOnlyAdjustmentSchema } from '/imports/api/properties/Adjustment
import { ComputedOnlyAttackSchema } from '/imports/api/properties/Attacks.js';
import { ComputedOnlyAttributeSchema } from '/imports/api/properties/Attributes.js';
import { ComputedOnlyBuffSchema } from '/imports/api/properties/Buffs.js';
// import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js';
import { ClassLevelSchema } from '/imports/api/properties/ClassLevels.js';
import { ConstantSchema } from '/imports/api/properties/Constants.js';
import { ComputedOnlyContainerSchema } from '/imports/api/properties/Containers.js';
import { ComputedOnlyDamageSchema } from '/imports/api/properties/Damages.js';
import { DamageMultiplierSchema } from '/imports/api/properties/DamageMultipliers.js';
import { ComputedOnlyEffectSchema } from '/imports/api/properties/Effects.js';
import { ComputedOnlyFeatureSchema } from '/imports/api/properties/Features.js';
// import { FolderSchema } from '/imports/api/properties/Folders.js';
import { FolderSchema } from '/imports/api/properties/Folders.js';
import { ComputedOnlyItemSchema } from '/imports/api/properties/Items.js';
import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes.js';
// import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls.js';
import { ComputedOnlySavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { ComputedOnlySkillSchema } from '/imports/api/properties/Skills.js';
import { ComputedOnlySlotSchema } from '/imports/api/properties/Slots.js';
// import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js';
import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js';
import { ComputedOnlySpellSchema } from '/imports/api/properties/Spells.js';
import { ComputedOnlySpellListSchema } from '/imports/api/properties/SpellLists.js';
import { ComputedOnlyToggleSchema } from '/imports/api/properties/Toggles.js';
@@ -29,23 +30,25 @@ const propertySchemasIndex = {
attack: ComputedOnlyAttackSchema,
attribute: ComputedOnlyAttributeSchema,
buff: ComputedOnlyBuffSchema,
// classLevel: ClassLevelSchema,
classLevel: ClassLevelSchema,
constant: ConstantSchema,
container: ComputedOnlyContainerSchema,
damage: ComputedOnlyDamageSchema,
damageMultiplier: DamageMultiplierSchema,
effect: ComputedOnlyEffectSchema,
feature: ComputedOnlyFeatureSchema,
// folder: FolderSchema,
folder: FolderSchema,
item: ComputedOnlyItemSchema,
note: ComputedOnlyNoteSchema,
// proficiency: ProficiencySchema,
proficiency: ProficiencySchema,
propertySlot: ComputedOnlySlotSchema,
roll: ComputedOnlyRollSchema,
savingThrow: ComputedOnlySavingThrowSchema,
skill: ComputedOnlySkillSchema,
slotFiller: SlotFillerSchema,
spellList: ComputedOnlySpellListSchema,
spell: ComputedOnlySpellSchema,
toggle: ComputedOnlyToggleSchema,
container: ComputedOnlyContainerSchema,
item: ComputedOnlyItemSchema,
any: 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

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

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

@@ -25,33 +25,52 @@ Meteor.publish('libraries', function(){
{owner: this.userId},
{writers: this.userId},
{readers: this.userId},
{_id: {$in: subs}},
{ _id: {$in: subs}, public: true },
]
}, {
sort: {name: 1}
});
});
});
Meteor.publish('library', function(libraryId){
if (!libraryId) return [];
libraryIdSchema.validate({libraryId});
this.autorun(function (){
let userId = this.userId;
let library = Libraries.findOne(libraryId);
try { assertViewPermission(library, userId) }
catch(e){
return this.error(e);
}
return Libraries.find({
_id: libraryId,
});
});
});
let libraryIdSchema = new SimpleSchema({
libraryId: {
libraryId:{
type: String,
regEx: SimpleSchema.RegEx.Id,
},
});
Meteor.publish('library', function(libraryId){
Meteor.publish('libraryNodes', function(libraryId){
if (!libraryId) return [];
libraryIdSchema.validate({libraryId});
this.autorun(function (){
let userId = this.userId;
let libraryCursor = Libraries.find({
_id: libraryId,
});
let library = libraryCursor.fetch()[0];
let library = Libraries.findOne(libraryId);
try { assertViewPermission(library, userId) }
catch(e){ return [] }
catch(e){
return this.error(e);
}
return [
libraryCursor,
LibraryNodes.find({
'ancestors.id': libraryId,
}, {
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
@@ -73,6 +73,7 @@ Meteor.publish('slotFillers', function(slotId){
}
}
} else {
delete filter.$text
options = {sort: {
name: 1,
order: 1,

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

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

@@ -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,15 +5,15 @@
right
clipped
>
<log-tab :creature-id="$route.params.id" />
<character-log :creature-id="$route.params.id" />
</v-navigation-drawer>
</template>
<script>
import LogTab from '/imports/ui/log/CharacterLog.vue';
import CharacterLog from '/imports/ui/log/CharacterLog.vue';
export default {
components: {
LogTab,
CharacterLog,
},
computed: {
drawer: {

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

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

@@ -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"
@@ -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},
@@ -130,7 +130,7 @@ export default {
}).map(slot => {
if (
!this.showHiddenSlots &&
slot.quantityExpected === 0 &&
slot.quantityExpectedResult === 0 &&
slot.hideWhenFull
){
slot.children = []
@@ -144,11 +144,12 @@ export default {
}
return slot;
}).filter(slot => !( // Hide full and ignored slots
!this.showHiddenSlots &&
slot.hideWhenFull &&
slot.quantityExpected > 0 &&
slot.totalFilled >= slot.quantityExpected ||
slot.ignored
!this.showHiddenSlots && (
slot.hideWhenFull &&
slot.quantityExpectedResult > 0 &&
slot.spaceLeft <= 0 ||
slot.ignored
)
));
},
},

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

@@ -19,13 +19,27 @@
>
<v-spacer />
<v-switch
v-if="!libraryId || canEditLibrary"
v-model="organize"
label="Organize"
class="mx-3"
style="flex-grow: 0; height: 32px;"
/>
</v-toolbar>
<div
v-if="libraryId"
style="width: 100%; height: 100%; overflow: auto;"
>
<library-contents-container
:library-id="libraryId"
:organize-mode="organize"
:selected-node-id="selected"
should-subscribe
@selected="clickNode"
/>
</div>
<library-browser
v-else
edit-mode
:organize-mode="organize"
:selected-node-id="selected"
@@ -53,17 +67,24 @@ import LibraryBrowser from '/imports/ui/library/LibraryBrowser.vue';
import LibraryNodeDialog from '/imports/ui/library/LibraryNodeDialog.vue';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import Libraries from '/imports/api/library/Libraries.js';
import LibraryContentsContainer from '/imports/ui/library/LibraryContentsContainer.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
export default {
components: {
TreeDetailLayout,
LibraryBrowser,
LibraryNodeDialog,
LibraryContentsContainer,
},
props: {
selection: Boolean,
libraryId: {
type: String,
default: undefined,
},
},
data(){ return {
organize: false,
@@ -113,13 +134,33 @@ export default {
},
meteor: {
$subscribe: {
'libraries': [],
'library'(){
if (this.libraryId){
return [this.libraryId]
} else {
return [];
}
},
},
libraries(){
return Libraries.find({}, {
sort: {name: 1}
}).fetch();
},
library(){
let libraryId = this.libraryId;
if (!libraryId) return;
return Libraries.findOne(libraryId);
},
canEditLibrary(){
if (!this.libraryId) return;
try {
assertEditPermission(this.library, Meteor.userId());
return true;
} catch (e){
return false;
}
},
selectedNode(){
return LibraryNodes.findOne({
_id: this.selected,

View File

@@ -6,13 +6,13 @@
"
>
<v-expansion-panel
v-model="expandedLibrary"
style="box-shadow: none;"
expand
>
<v-expansion-panel-content
v-for="library in libraries"
v-for="(library, index) in libraries"
:key="library._id"
v-model="expandedLibrary[index]"
lazy
:data-id="library._id"
>
@@ -24,9 +24,10 @@
<v-card flat>
<library-contents-container
:library-id="library._id"
:organize-mode="organizeMode"
:organize-mode="organizeMode && editPermission(library)"
:edit-mode="editMode"
:selected-node-id="selectedNodeId"
:should-subscribe="expandedLibrary[index]"
@selected="e => $emit('selected', e)"
/>
<v-card-actions>
@@ -47,16 +48,16 @@
small
icon
:disabled="!editPermission(library)"
@click="editLibrary(library._id)"
@click="$router.push(`/library/${library._id}`)"
>
<v-icon>create</v-icon>
<v-icon>arrow_forward</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-expansion-panel-content>
</v-expansion-panel>
<v-btn
v-show="expandedLibrary === null"
v-show="noLibrariesExpanded"
v-if="editMode"
flat
color="primary"
@@ -86,11 +87,25 @@ export default {
props: {
organizeMode: Boolean,
editMode: Boolean,
selectedNodeId: String,
selectedNodeId: {
type: String,
default: undefined,
},
},
data(){ return {
expandedLibrary: null,
expandedLibrary: [],
expandedLibraryContent: [],
};},
computed: {
noLibrariesExpanded(){
if (!this.expandedLibrary) return true;
let noneExpanded = true;
this.expandedLibrary.forEach(lib => {
if(lib) noneExpanded = false;
});
return noneExpanded;
},
},
meteor: {
$subscribe: {
'libraries': [],
@@ -106,6 +121,7 @@ export default {
},
},
methods: {
log: console.log,
insertLibrary(){
if (this.paidBenefits){
this.$store.commit('pushDialogStack', {

View File

@@ -1,13 +1,30 @@
<template lang="html">
<tree-node-list
group="library"
:children="libraryChildren"
:organize="organizeMode"
:selected-node-id="selectedNodeId"
@selected="e => $emit('selected', e)"
@reordered="reordered"
@reorganized="reorganized"
/>
<v-fade-transition
hide-on-leave
>
<tree-node-list
v-if="slowShouldSubscribe && $subReady.libraryNodes"
group="library"
:children="libraryChildren"
:organize="organizeMode"
:selected-node-id="selectedNodeId"
@selected="e => $emit('selected', e)"
@reordered="reordered"
@reorganized="reorganized"
/>
<v-layout
v-else
row
align-center
justify-center
style="width: 100%;"
>
<v-progress-circular
color="primary"
:indeterminate="slowShouldSubscribe"
/>
</v-layout>
</v-fade-transition>
</template>
<script>
@@ -25,13 +42,36 @@
libraryId: String,
organizeMode: Boolean,
selectedNodeId: String,
shouldSubscribe: Boolean,
},
data(){return {
slowShouldSubscribe: this.shouldSubscribe,
};},
watch:{
shouldSubscribe(newValue){
if (this.timeoutId){
clearTimeout(this.timeoutId);
delete this.timeoutId;
}
if (newValue){
this.slowShouldSubscribe = newValue
} else {
this.timeoutId = setTimeout(()=>{
this.slowShouldSubscribe = newValue
}, 2000);
}
}
},
meteor: {
$subscribe: {
'library'(){
return [this.libraryId]
},
},
$subscribe: {
'libraryNodes'(){
if (this.slowShouldSubscribe){
return [this.libraryId];
} else {
return [];
}
}
},
library(){
return Libraries.findOne(this.libraryId);
},

View File

@@ -1,121 +0,0 @@
<template lang="html">
<div
class="layout row"
style="background-color: inherit;"
>
<div
class="layout column"
style="
background-color: inherit;
width: initial;
max-width: 100%;
min-width: 320px;
"
>
<v-toolbar
dense
flat
>
<v-spacer />
<v-switch
v-model="organize"
label="Organize"
class="mx-3"
style="flex-grow: 0; height: 32px;"
/>
</v-toolbar>
<library-contents-container
:library-id="$route.params.id"
:organize-mode="organize"
:selected-node-id="selected"
@selected="e => selected = e"
/>
</div>
<v-divider vertical />
<div
style="width: 100%; background-color: inherit;"
data-id="selected-node-card"
>
<v-toolbar
dense
flat
>
<property-icon
:model="selectedNode"
class="mr-2"
/>
<div class="title">
{{ getPropertyName(selectedNode && selectedNode.type) }}
</div>
<v-spacer />
<v-btn
v-if="selectedNode"
flat
icon
@click="editLibraryNode"
>
<v-icon>create</v-icon>
</v-btn>
</v-toolbar>
<v-card-text style="overflow-y: auto;">
<property-viewer :model="selectedNode" />
</v-card-text>
</div>
</div>
</template>
<script>
import PropertyViewer from '/imports/ui/properties/shared/PropertyViewer.vue';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import Libraries from '/imports/api/library/Libraries.js';
import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import LibraryContentsContainer from '/imports/ui/library/LibraryContentsContainer.vue';
export default {
components: {
LibraryContentsContainer,
PropertyViewer,
PropertyIcon,
},
data(){ return {
organize: false,
selected: undefined,
};},
watch:{
selectedNode(val){
this.$emit('selected', val)
},
'library.name'(value){
this.$store.commit('setPageTitle', value || 'Library');
},
},
methods: {
editLibraryNode(){
this.$store.commit('pushDialogStack', {
component: 'library-node-edit-dialog',
elementId: 'selected-node-card',
data: {_id: this.selected},
});
},
getPropertyName,
},
meteor: {
$subscribe: {
'libraries': [],
},
library(){
return Libraries.findOne(this.$route.params.id);
},
selectedNode(){
return LibraryNodes.findOne({
_id: this.selected,
removed: {$ne: true}
});
}
}
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,28 +1,53 @@
<template lang="html">
<v-toolbar-items>
<v-btn
v-if="showSubscribeButton"
flat
:loading="loading"
@click="subscribe(!subscribed)"
>
{{ subscribed ? 'Unsubscribe' : 'Subscribe' }}
</v-btn>
<v-btn
v-if="canEdit"
flat
icon
data-id="library-edit-button"
@click="editLibrary(library._id)"
>
<v-icon>create</v-icon>
</v-btn>
</v-toolbar-items>
<v-toolbar
app
color="secondary"
dark
tabs
extended
dense
>
<v-toolbar-side-icon @click="toggleDrawer" />
<v-toolbar-items>
<v-btn
flat
icon
@click="$router.push('/library')"
>
<v-icon>arrow_back</v-icon>
</v-btn>
</v-toolbar-items>
<v-toolbar-title>
{{ library && library.name }}
</v-toolbar-title>
<v-spacer />
<v-toolbar-items>
<v-btn
v-if="showSubscribeButton"
flat
:loading="loading"
@click="subscribe(!subscribed)"
>
{{ subscribed ? 'Unsubscribe' : 'Subscribe' }}
</v-btn>
<v-btn
v-if="canEdit"
flat
icon
data-id="library-edit-button"
@click="editLibrary(library._id)"
>
<v-icon>settings</v-icon>
</v-btn>
</v-toolbar-items>
</v-toolbar>
</template>
<script>
import Libraries from '/imports/api/library/Libraries.js';
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { mapMutations } from 'vuex';
export default {
data(){ return {
loading: false,
@@ -33,8 +58,10 @@ export default {
},
subscribed(){
let libraryId = this.$route.params.id;
let subs = Meteor.user().subscribedLibraries;
return subs.includes(libraryId);
let user = Meteor.user();
if (!user) return false;
let subs = user.subscribedLibraries;
return subs && subs.includes(libraryId);
},
showSubscribeButton(){
let userId = Meteor.userId();
@@ -60,6 +87,9 @@ export default {
}
},
methods: {
...mapMutations([
'toggleDrawer',
]),
subscribe(value){
this.loading = true;
Meteor.users.subscribeToLibrary.call({

View File

@@ -3,8 +3,10 @@
style="height: 100%; overflow: hidden;"
class="character-log layout column justify-end"
>
<div
class="log flex layout column reverse align-end pa-3"
<v-slide-y-reverse-transition
group
hide-on-leave
class="log-entries flex layout column reverse align-end pa-3"
style="overflow: auto;"
>
<log-entry
@@ -12,7 +14,7 @@
:key="log._id"
:model="log"
/>
</div>
</v-slide-y-reverse-transition>
<v-card>
<v-text-field
v-model="input"
@@ -120,4 +122,10 @@ export default {
.log-tab p:last-child {
margin-bottom: 0;
}
.theme--dark .log-entries {
background: #303030;
}
.log-entries {
background: #fafafa;
}
</style>

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

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

@@ -1,6 +1,8 @@
<template lang="html">
<single-card-layout>
<library-and-node />
<library-and-node
:library-id="$route.params.id"
/>
</single-card-layout>
</template>

View File

@@ -1,16 +0,0 @@
<template lang="html">
<div>
<v-card class="ma-4">
<single-library />
</v-card>
</div>
</template>
<script>
import SingleLibrary from '/imports/ui/library/SingleLibrary.vue';
export default {
components: {
SingleLibrary,
},
};
</script>

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: {
@@ -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,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

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

Some files were not shown because too many files have changed in this diff Show More