Compare commits

...

81 Commits

Author SHA1 Message Date
Stefan Zermatten
0c24238069 Fixed not found page for Vuetify 2 2021-04-11 18:15:56 +02:00
Stefan Zermatten
66847430ad Fixed sign in and register pages not being built with Vuetify 2 components 2021-04-11 18:05:36 +02:00
Stefan Zermatten
bfb860605f Creature properties now duplicate with up to 50 children 2021-04-11 14:47:41 +02:00
Stefan Zermatten
d87524418a Fixed bug where character sheet fab could be permanently hidden by closing the insert from library dialog 2021-04-11 13:52:25 +02:00
Stefan Zermatten
5f97592ed3 Updated package-lock for last commit 2021-04-11 13:33:38 +02:00
Stefan Zermatten
562991216f Moved ignore-styles dep from devDependencies to dependencies 2021-04-11 13:23:47 +02:00
Stefan Zermatten
109b89022e removed id from comment 2021-04-11 13:09:16 +02:00
Stefan Zermatten
d4ca07ce9c Began working on character migration code 2021-04-11 13:08:41 +02:00
Stefan Zermatten
885607f685 Moved the tree fab to the toolbar with smart parenting 2021-04-11 12:36:14 +02:00
Stefan Zermatten
81460f8835 Added file missed in last commit 2021-04-11 12:15:51 +02:00
Stefan Zermatten
cce5f9b926 Merge branch 'version-2-dev' of https://github.com/ThaumRystra/DiceCloud into version-2-dev 2021-04-11 12:15:33 +02:00
Stefan Zermatten
7d3a51de9d Moved ancestry setting responsibility to trusted code 2021-04-11 12:15:30 +02:00
Stefan Zermatten
fc774fcc5e Moved ancestry setting responsibility to trusted code 2021-04-11 12:14:39 +02:00
Stefan Zermatten
0f37a49b95 Fixed bug where wrong fab would show on character tab if spell tab was hidden 2021-04-11 11:01:31 +02:00
Stefan Zermatten
9814e20091 Added the ability to hide spells and tree tab. Tree tab hidden by default 2021-04-11 10:43:33 +02:00
Stefan Zermatten
d89cb77040 Fixed no-experiences Icon 2021-04-11 10:29:09 +02:00
Stefan Zermatten
8590d29abf Improved animation feel of character sheet fab 2021-04-11 10:21:14 +02:00
Stefan Zermatten
9298754dc9 Fixed character sheet title not updating correctly 2021-04-09 12:44:54 +02:00
Stefan Zermatten
e2d6d40bb3 Duplicating library nodes now duplicates up to 50 descendants 2021-04-09 12:36:44 +02:00
Stefan Zermatten
152677b023 Library nodes are now smarter about where in the tree they are inserted based on the currently selected node 2021-04-09 12:36:14 +02:00
Stefan Zermatten
838e2ed35f Fixed toolbar colors for vuetify 2 2021-03-28 14:29:56 +02:00
Stefan Zermatten
60ae1ef604 Fixed bug where editing a field and immediately changing selected property would apply the change to the new property rather than the old one 2021-03-28 13:56:45 +02:00
Stefan Zermatten
ecfe5f1360 Fixed spell casting buttons having the wrong color in dark mode 2021-03-28 13:14:38 +02:00
Stefan Zermatten
292d3c3f37 Tree titles now have hover. Fixed primary color theme switching 2021-03-28 13:09:47 +02:00
Stefan Zermatten
3c26bb2fc6 Reworked log data format, overhauled snackbar 2021-03-28 12:31:39 +02:00
Stefan Zermatten
ada1355c29 Added UI for filtered out slot fillers allowing loading more 2021-03-27 14:39:00 +02:00
Stefan Zermatten
2662af8ea2 Fixed misalignment on ability score tiles 2021-03-27 13:47:02 +02:00
Stefan Zermatten
26b68dccef Fixed style of attribute cards 2021-03-27 13:45:10 +02:00
Stefan Zermatten
0717f8e8d7 Fixed broken and insecure packages 2021-03-27 13:34:33 +02:00
Stefan Zermatten
5cf0330e03 Added library node insert button to library page, no automatic parenting 2021-03-26 12:49:08 +02:00
Stefan Zermatten
1978a2e4c7 Fixed Error when referencing slotLevel in a spell 2021-03-26 11:18:14 +02:00
Stefan Zermatten
5a2e500348 Fixed constants under toggles triggering calculation of those toggles before class levels are defined 2021-03-26 11:11:15 +02:00
Stefan Zermatten
7d4356592a Iterated on card color scheme 2021-03-26 11:06:52 +02:00
Stefan Zermatten
2c448d1748 Fixed some pages background colors 2021-03-26 10:00:37 +02:00
Stefan Zermatten
4f96d817d5 Change card and background color scheme 2021-03-26 09:48:29 +02:00
Stefan Zermatten
f9998eabc4 Fixed tabs using the wrong primary color 2021-03-26 09:35:19 +02:00
Stefan Zermatten
623cff584c Fixed form section expansion 2021-03-26 09:26:31 +02:00
Stefan Zermatten
aa34508cb3 Fixed hovering on toolbar cards 2021-03-26 09:26:16 +02:00
Stefan Zermatten
6678bc1cea Fixed library subscriptions, again 2021-03-25 14:12:02 +02:00
Stefan Zermatten
0324b9f7c3 Fixed some tooltips to vuetify 2 2021-03-25 13:14:40 +02:00
Stefan Zermatten
ccac142ec6 Fixed some cards not animating elevation change 2021-03-25 13:08:48 +02:00
Stefan Zermatten
fe3fa56541 Continued migrating UI to vuetify 2 2021-03-25 12:54:44 +02:00
Stefan Zermatten
480da6fc7d ES Lint fix migration to vuetify 2 2021-03-25 10:20:13 +02:00
Stefan Zermatten
6ffb48b7b6 Began migration to Vuetify 2.x expect a lot to be broken 2021-03-24 16:23:39 +02:00
Stefan Zermatten
82150df5e0 Updated packages and dependencies 2021-03-24 14:38:47 +02:00
Stefan Zermatten
9a120a6e9a Added description to class level viewer 2021-03-23 15:07:47 +02:00
Stefan Zermatten
f385c2857e Fixed library forward arrow being disabled if you can't edit the library 2021-03-12 09:34:20 +02:00
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
287 changed files with 3905 additions and 2953 deletions

View File

@@ -3,50 +3,44 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
accounts-password@1.6.2
accounts-ui@1.3.1
accounts-password@1.7.0
random@1.2.0
dburles:collection-helpers
reactive-var@1.0.11
underscore@1.0.10
momentjs:moment
dburles:mongo-collection-instances
percolate:migrations
accounts-google@1.3.3
email@2.0.0
meteorhacks:subs-manager
chuangbo:marked
meteor-base@1.4.0
mobile-experience@1.1.0
mongo@1.10.0
mongo@1.10.1
session@1.2.0
jquery@1.11.10
tracker@1.2.0
logging@1.1.20
reload@1.3.0
logging@1.2.0
reload@1.3.1
ejson@1.1.1
check@1.3.1
standard-minifier-js@2.6.0
shell-server@0.5.0
seba:minifiers-autoprefixer
templates:array
ecmascript@0.14.3
ecmascript@0.15.0
es5-shim@4.8.0
reactive-dict@1.3.0
percolate:synced-cron
ongoworks:speakingurl
service-configuration@1.0.11
google-config-ui@1.0.1
dynamic-import@0.5.2
dynamic-import@0.6.0
ddp-rate-limiter@1.0.9
rate-limit@1.0.9
meteortesting:mocha
mdg:validated-method
akryum:vue-router2
static-html
aldeed:collection2@3.0.0
aldeed:schema-index
akryum:vue-component
zer0th:meteor-vuetify-loader
accounts-patreon
bozhao:link-accounts
peerlibrary:reactive-publish
@@ -54,3 +48,6 @@ simple:rest
simple:rest-method-mixin
mikowals:batch-insert
peerlibrary:subscription-data
seba:minifiers-autoprefixer
akryum:vue-component
akryum:vue-sass

View File

@@ -1 +1 @@
METEOR@1.11.1
METEOR@2.1

View File

@@ -1,27 +1,25 @@
accounts-base@1.7.0
accounts-base@1.8.0
accounts-google@1.3.3
accounts-oauth@1.2.0
accounts-password@1.6.2
accounts-password@1.7.0
accounts-patreon@0.1.0
accounts-ui@1.3.1
accounts-ui-unstyled@1.4.2
akryum:npm-check@0.1.2
akryum:vue-component@0.15.2
akryum:vue-component-dev-client@0.4.7
akryum:vue-component-dev-server@0.1.4
akryum:vue-router2@0.2.3
akryum:vue-sass@0.1.2
aldeed:collection2@3.2.1
aldeed:schema-index@3.0.0
allow-deny@1.1.0
autoupdate@1.6.0
babel-compiler@7.5.3
autoupdate@1.7.0
babel-compiler@7.6.0
babel-runtime@1.5.0
base64@1.0.12
binary-heap@1.0.11
blaze@2.3.4
blaze-tools@1.0.10
boilerplate-generator@1.7.1
bozhao:link-accounts@2.2.1
bozhao:link-accounts@2.3.2
caching-compiler@1.2.2
caching-html-compiler@1.1.3
callback-hook@1.3.0
@@ -32,14 +30,14 @@ coffeescript-compiler@2.4.1
dburles:collection-helpers@1.1.0
dburles:mongo-collection-instances@0.3.5
ddp@1.4.0
ddp-client@2.3.3
ddp-client@2.4.0
ddp-common@1.4.0
ddp-rate-limiter@1.0.9
ddp-server@2.3.2
deps@1.0.12
diff-sequence@1.1.1
dynamic-import@0.5.3
ecmascript@0.14.3
dynamic-import@0.6.0
ecmascript@0.15.0
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.11.0
ecmascript-runtime-server@0.10.0
@@ -48,49 +46,40 @@ email@2.0.0
es5-shim@4.8.0
fetch@0.1.1
geojson-utils@1.0.10
google-config-ui@1.0.1
google-oauth@1.3.0
hot-code-push@1.0.4
html-tools@1.0.11
htmljs@1.0.11
http@1.4.2
http@1.4.3
id-map@1.1.0
inter-process-messaging@0.1.1
jquery@1.11.11
lai:collection-extensions@0.2.1_1
launch-screen@1.2.0
less@2.8.0
livedata@1.0.18
lmieulet:meteor-coverage@1.1.4
localstorage@1.2.0
logging@1.1.20
logging@1.2.0
mdg:validated-method@1.2.0
meteor@1.9.3
meteor-base@1.4.0
meteorhacks:picker@1.0.3
meteorhacks:subs-manager@1.6.4
meteortesting:browser-tests@1.3.4
meteortesting:mocha@1.1.5
meteortesting:mocha-core@7.0.1
mikowals:batch-insert@1.2.0
minifier-css@1.5.3
minifier-js@2.6.0
minimongo@1.6.0
minimongo@1.6.1
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.5
modules@0.15.0
modules@0.16.0
modules-runtime@0.12.0
momentjs:moment@2.29.1
mongo@1.10.0
mongo-decimal@0.1.1
mongo@1.10.1
mongo-decimal@0.1.2
mongo-dev-server@1.1.0
mongo-id@1.0.7
npm-bcrypt@0.9.3
npm-mongo@3.8.1
oauth@1.3.2
oauth2@1.3.0
observe-sequence@1.0.16
ongoworks:speakingurl@9.0.0
ordered-dict@1.1.0
patreon-oauth@0.1.0
@@ -104,18 +93,18 @@ peerlibrary:reactive-mongo@0.4.0
peerlibrary:reactive-publish@0.10.0
peerlibrary:server-autorun@0.8.0
peerlibrary:subscription-data@0.8.0
percolate:migrations@0.9.8
percolate:synced-cron@1.3.2
promise@0.11.2
raix:eventemitter@0.1.3
raix:eventemitter@1.0.0
random@1.2.0
rate-limit@1.0.9
react-fast-refresh@0.1.0
reactive-dict@1.3.0
reactive-var@1.0.11
reload@1.3.0
reload@1.3.1
retry@1.1.0
routepolicy@1.1.0
seba:minifiers-autoprefixer@1.2.1
seba:minifiers-autoprefixer@2.0.1
service-configuration@1.0.11
session@1.2.0
sha@1.0.9
@@ -124,19 +113,16 @@ simple:json-routes@2.1.0
simple:rest@1.1.1
simple:rest-method-mixin@1.0.1
socket-stream-client@0.3.1
spacebars@1.0.15
spacebars-compiler@1.1.3
srp@1.1.0
standard-minifier-js@2.6.0
static-html@1.2.2
templates:array@1.0.3
templating@1.3.2
templating-compiler@1.3.3
templating-runtime@1.3.2
templating-tools@1.1.2
tmeasday:check-npm-versions@0.3.2
tracker@1.2.0
underscore@1.0.10
url@1.3.1
webapp@1.9.1
webapp-hashing@1.0.9
webapp@1.10.0
webapp-hashing@1.1.0
zer0th:meteor-vuetify-loader@0.1.30

View File

@@ -35,6 +35,16 @@ let CreatureSettingsSchema = new SimpleSchema({
type: Boolean,
optional: true,
},
// Show the tree tab
showTreeTab: {
type: Boolean,
optional: true,
},
// Hide the spells tab
hideSpellsTab: {
type: Boolean,
optional: true,
},
// How much each hitDice resets on a long rest
hitDiceResetMultiplier: {
type: Number,

View File

@@ -4,7 +4,7 @@ import embedInlineCalculations from '/imports/api/creature/computation/afterComp
export default function applyAction({prop, log}){
let content = { name: prop.name };
if (prop.summary){
content.description = embedInlineCalculations(
content.value = embedInlineCalculations(
prop.summary, prop.summaryCalculations
);
}

View File

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

View File

@@ -17,7 +17,6 @@ export default function applyAttack({
log.content.push({
name: criticalHit ? 'Critical Hit!' : 'To Hit',
resultPrefix: `1d20 [${value}] + ${prop.rollBonusResult} = `,
result,
value: `1d20 [${value}] + ${prop.rollBonusResult} = ` + result,
});
}

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import applySave from '/imports/api/creature/actions/applySave.js';
function applyProperty(options){
let prop = options.prop;
if (prop.type === 'buff'){
// ignore only applied buffs
// ignore only applied buffs, don't apply them again
if (prop.applied === true){
return false;
}
@@ -40,7 +40,7 @@ function applyProperty(options){
break;
case 'buff':
applyBuff(options);
break;
return false;
case 'toggle':
return applyToggle(options);
case 'roll':

View File

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

View File

@@ -14,19 +14,16 @@ export default function applySave({
};
try {
// Calculate the DC
var {result, errors} = evaluateString(prop.dc, scope, 'reduce');
let dc = result;
var {result} = evaluateString({
string: prop.dc,
scope,
fn: 'reduce'
});
let dc = result.value;
log.content.push({
name: prop.name,
resultPrefix: ' DC ',
result,
value: ' DC ' + result.toString(),
});
if (errors.length) {
log.content.push({
error: errors.join(', '),
});
return false;
}
if (prop.target === 'self'){
let save = CreaturesProperties.findOne({
'ancestors.id': creature._id,
@@ -38,7 +35,8 @@ export default function applySave({
});
if (!save){
log.content.push({
error: 'No saving throw found: ' + prop.stat,
name: 'Saving throw error',
value: 'No saving throw found: ' + prop.stat,
});
return;
}
@@ -62,9 +60,7 @@ export default function applySave({
let saveSuccess = result >= dc;
log.content.push({
name: 'Save',
resultPrefix,
result,
details: saveSuccess ? 'Passed' : 'Failed'
value: resultPrefix + result + (saveSuccess ? 'Passed' : 'Failed')
});
return !saveSuccess;
} else {
@@ -73,7 +69,8 @@ export default function applySave({
}
} catch (e){
log.content.push({
error: e.toString(),
name: 'Save error',
value: e.toString(),
});
}
}

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ import { recomputeCreatureByDoc } from '/imports/api/creature/computation/method
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',
@@ -46,9 +47,14 @@ const doAction = new ValidatedMethod({
// The acting creature might have used ammo
recomputeInventory(creature._id);
// The action might add properties which need to be activated
recomputeInactiveProperties(creature._id);
// recompute creatures
recomputeCreatureByDoc(creature);
targets.forEach(target => {
recomputeInactiveProperties(target._id);
recomputeCreatureByDoc(target);
});
},

View File

@@ -51,7 +51,7 @@ export default function spendResources({prop, log}){
// Now that we have confirmed that there are no errors, do actual work
//Items
itemQuantityAdjustments.forEach(adjustQuantityWork);
// Use uses
if (prop.usesResult){
CreatureProperties.update(prop._id, {
@@ -61,7 +61,7 @@ export default function spendResources({prop, log}){
});
log.content.push({
name: 'Uses left',
result: prop.usesResult - prop.usesUsed - 1,
value: prop.usesResult - (prop.usesUsed || 0) - 1,
});
}
@@ -84,10 +84,10 @@ export default function spendResources({prop, log}){
// Log all the spending
if (gainLog.length) log.content.push({
name: 'Gained',
description: gainLog.join('\n'),
value: gainLog.join('\n'),
});
if (spendLog.length) log.content.push({
name: 'Spent',
description: spendLog.join('\n'),
value: spendLog.join('\n'),
});
}

View File

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

View File

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

View File

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

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

View File

@@ -96,7 +96,7 @@ function combineSkill(stat, aggregator, memo){
let prof = stat.computationDetails.proficiencies[i];
applyToggles(prof, memo);
if (
!prof.computationDetails.disabledByToggle &&
!prof.deactivatedByToggle &&
prof.value > stat.proficiency
){
stat.proficiency = prof.value;
@@ -112,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':
@@ -75,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;
}
});
}

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,10 +5,15 @@ 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 level
computeLevels(memo);
// Compute all constants that could be used
forOwn(memo.constantsById, constant => {
computeConstant (constant, memo);
});
// Compute all stats, even if they are overriden
forOwn(memo.statsById, stat => {
computeStat (stat, memo);

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

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

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

@@ -50,6 +50,13 @@ let CreaturePropertySchema = new SimpleSchema({
optional: true,
index: 1,
},
// Denormalised flag if this property was made inactive because of a toggle
// calculation. Either an ancestor toggle calculation or its own.
deactivatedByToggle: {
type: Boolean,
optional: true,
index: 1,
},
// Denormalised list of all properties or creatures this property depends on
dependencies: {
type: Array,
@@ -58,7 +65,6 @@ let CreaturePropertySchema = new SimpleSchema({
},
'dependencies.$': {
type: String,
regEx: SimpleSchema.RegEx.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

@@ -2,9 +2,24 @@ import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
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 { insertPropertyWork } from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
import {
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import recomputeInventory from '/imports/api/creature/denormalise/recomputeInventory.js';
import { recomputeCreatureByDoc } from '/imports/api/creature/computation/methods/recomputeCreature.js';
var snackbar;
if (Meteor.isClient){
snackbar = require(
'/imports/ui/components/snackbars/SnackbarQueue.js'
).snackbar
}
const DUPLICATE_CHILDREN_LIMIT = 50;
const duplicateProperty = new ValidatedMethod({
name: 'creatureProperties.duplicate',
@@ -20,13 +35,70 @@ const duplicateProperty = new ValidatedMethod({
timeInterval: 5000,
},
run({_id}) {
let creatureProperty = CreatureProperties.findOne(_id);
let rootCreature = getRootCreatureAncestor(creatureProperty);
assertEditPermission(rootCreature, this.userId);
insertPropertyWork({
property: creatureProperty,
creature: rootCreature,
let property = CreatureProperties.findOne(_id);
let creature = getRootCreatureAncestor(property);
assertEditPermission(creature, this.userId);
// Renew the doc ID
let randomSrc = DDP.randomStream('duplicateProperty');
let propertyId = randomSrc.id();
property._id = propertyId;
// Get all the descendants
let nodes = CreatureProperties.find({
'ancestors.id': _id,
removed: {$ne: true},
}, {
limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: {order: 1},
}).fetch();
// Alert the user if the limit was hit
if (nodes.length > DUPLICATE_CHILDREN_LIMIT){
nodes.pop();
if (Meteor.isClient){
snackbar({
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
});
}
}
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry : [
...property.ancestors,
{id: propertyId, collection: 'creatureProperties'}
],
oldParent : {id: _id, collection: 'creatureProperties'},
});
// Give the docs new IDs without breaking internal references
renewDocIds({docArray: nodes});
// Order the root node
property.order += 0.5;
// Insert the properties
CreatureProperties.batchInsert([property, ...nodes]);
// Tree structure changed by inserts, reorder the tree
reorderDocs({
collection: CreatureProperties,
ancestorId: property.ancestors[0].id,
});
// Inserting the active status of the property needs to be denormalised
recomputeInactiveProperties(creature._id);
// Recompute the inventory
recomputeInventory(creature._id);
// Inserting a creature property invalidates dependencies: full recompute
recomputeCreatureByDoc(creature);
return propertyId;
},
});

View File

@@ -5,6 +5,7 @@ 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';
@@ -62,6 +63,7 @@ const equipItem = new ValidatedMethod({
skipRecompute: true,
});
recomputeInactiveProperties(creature._id);
recomputeInventory(creature._id);
recomputeCreatureByDoc(creature);
},

View File

@@ -1,4 +1,4 @@
import '//imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
import '/imports/api/creature/creatureProperties/methods/damagePropertiesByName.js';
import '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import '/imports/api/creature/creatureProperties/methods/dealDamage.js';

View File

@@ -7,6 +7,7 @@ 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';
import { getAncestry } from '/imports/api/parenting/parenting.js';
const insertProperty = new ValidatedMethod({
name: 'creatureProperties.insert',
@@ -16,9 +17,24 @@ const insertProperty = new ValidatedMethod({
numRequests: 5,
timeInterval: 5000,
},
run({creatureProperty}) {
let rootCreature = getRootCreatureAncestor(creatureProperty);
run({creatureProperty, parentRef}) {
// get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef});
// Check permission to edit
let rootCreature;
if (parentRef.collection === 'creatures'){
rootCreature = parentDoc;
} else if (parentRef.collection === 'creatureProperties'){
rootCreature = getRootCreatureAncestor(parentDoc);
} else {
throw `${parentRef.collection} is not a valid parent collection`
}
assertEditPermission(rootCreature, this.userId);
creatureProperty.parent = parentRef;
creatureProperty.ancestors = ancestors;
return insertPropertyWork({
property: creatureProperty,
creature: rootCreature,

View File

@@ -27,13 +27,17 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
parentRef: {
type: RefSchema,
},
order: {
type: Number,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({nodeId, parentRef}) {
run({nodeId, parentRef, order}) {
// get the new ancestry for the properties
let {parentDoc, ancestors} = getAncestry({parentRef});
@@ -79,10 +83,14 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
});
// Order the root node
setDocToLastOrder({
collection: CreatureProperties,
doc: node,
});
if (order === undefined){
setDocToLastOrder({
collection: CreatureProperties,
doc: node,
});
} else {
node.order = order;
}
// Insert the creature properties
CreatureProperties.batchInsert(nodes);

View File

@@ -3,7 +3,7 @@ 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';
@@ -59,7 +59,8 @@ const updateCreatureProperty = new ValidatedMethod({
recomputeInventory(rootCreature._id);
}
// Updating a property is likely to change dependencies, do a full recompute
recomputeCreatureByDoc(rootCreature);
// denormalised stats might change, so fetch the creature again
recomputeCreatureById(rootCreature._id);
},
});

View File

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

View File

@@ -84,13 +84,13 @@ function getInventoryData(tree, containersToWrite){
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;
}
if (node.contentsWeightless){
data.weightCarried = node.weight;
}
return data
}

View File

@@ -58,6 +58,7 @@ function removeOldLogs(creatureId){
sort: {date: -1},
skip: PER_CREATURE_LOG_LIMIT,
});
if (!firstExpiredLog) return;
// Remove all logs older than the one over the limit
CreatureLogs.remove({
creatureId,
@@ -69,32 +70,10 @@ 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);
}
log.content.forEach(field => {
if (!field.name) field.name = '\u200b';
if (!field.value) field.value = '\u200b';
embed.fields.push(field);
});
return { embeds: [embed] };
}
@@ -109,7 +88,7 @@ function logWebhook({log, creature}){
}
const insertCreatureLog = new ValidatedMethod({
name: 'creatureLogs.methods.insertCreatureLog',
name: 'creatureLogs.methods.insert',
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
@@ -138,7 +117,7 @@ const insertCreatureLog = new ValidatedMethod({
export function insertCreatureLogWork({log, creature, method}){
// Build the new log
if (typeof log === 'string'){
log = {text: log};
log = {content: [{value: log}]};
}
log.date = new Date();
// Insert it
@@ -190,30 +169,30 @@ const logRoll = new ValidatedMethod({
parsedResult = parse(roll);
} catch (e){
let error = prettifyParseError(e);
logContent.push({error});
logContent.push({name: 'Parse Error', value: error});
}
if (parsedResult) try {
let rollContext = new CompilationContext();
let compiled = parsedResult.compile(creature.variables, rollContext);
let compiledString = compiled.toString();
if (!equalIgnoringWhitespace(compiledString, roll)) logContent.push({
details: roll
value: roll
});
logContent.push({
details: compiledString
value: compiledString
});
let rolled = compiled.roll(creature.variables, rollContext);
let rolledString = rolled.toString();
if (rolledString !== compiledString) logContent.push({
result: rolled.toString()
value: rolled.toString()
});
let result = rolled.reduce(creature.variables, rollContext);
let resultString = result.toString();
if (resultString !== rolledString) logContent.push({
result: resultString
value: resultString
});
} catch (e){
logContent = [{error: 'Calculation error'}];
logContent = [{name: 'Calculation error'}];
}
const log = {
content: logContent,

View File

@@ -3,31 +3,14 @@ import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
import RollDetailsSchema from '/imports/api/properties/subSchemas/RollDetailsSchema.js';
let LogContentSchema = new SimpleSchema({
// The name of the field, included in discord webhook message
name: {
type: String,
optional: true,
},
error: {
type: String,
optional: true,
},
resultPrefix: {
type: String,
optional: true,
},
result: {
type: String,
optional: true,
},
expandedResult: {
type: String,
optional: true,
},
details: {
type: String,
optional: true,
},
description: {
// The details of the field, included in discord webhook message
// Markdown support
value: {
type: String,
optional: true,
},

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

@@ -0,0 +1,12 @@
import { fetch } from 'meteor/fetch'
export default function importCharacter(url){
// Using v1's JSON API to fetch the character data in a usable format
// url -> https://dicecloud.com/character/<id>/json?key=<key>
fetch(url)
.then(response => response.json())
.then(data => {
let character = data.characters[0];
console.log(character.name + ' fetched successfuly')
});
}

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

@@ -11,6 +11,7 @@ import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js
import { softRemove } from '/imports/api/parenting/softRemove.js';
import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import '/imports/api/library/methods/index.js';
let LibraryNodes = new Mongo.Collection('libraryNodes');
@@ -79,27 +80,6 @@ const insertNode = new ValidatedMethod({
},
});
const duplicateNode = new ValidatedMethod({
name: 'libraryNodes.duplicate',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
}
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
let libraryNode = LibraryNodes.findOne(_id);
assertNodeEditPermission(libraryNode, this.userId);
delete libraryNode._id;
return LibraryNodes.insert(libraryNode);
},
})
const updateLibraryNode = new ValidatedMethod({
name: 'libraryNodes.update',
validate({_id, path}){
@@ -195,7 +175,6 @@ export default LibraryNodes;
export {
LibraryNodeSchema,
insertNode,
duplicateNode,
updateLibraryNode,
pullFromLibraryNode,
pushToLibraryNode,

View File

@@ -0,0 +1,87 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import SimpleSchema from 'simpl-schema';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import {
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
var snackbar;
if (Meteor.isClient){
snackbar = require(
'/imports/ui/components/snackbars/SnackbarQueue.js'
).snackbar
}
const DUPLICATE_CHILDREN_LIMIT = 50;
const duplicateLibraryNode = new ValidatedMethod({
name: 'libraryNodes.duplicate',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
}
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}) {
let libraryNode = LibraryNodes.findOne(_id);
assertDocEditPermission(libraryNode, this.userId);
let randomSrc = DDP.randomStream('duplicateLibraryNode');
let libraryNodeId = randomSrc.id();
libraryNode._id = libraryNodeId;
let nodes = LibraryNodes.find({
'ancestors.id': _id,
removed: {$ne: true},
}, {
limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: {order: 1},
}).fetch();
if (nodes.length > DUPLICATE_CHILDREN_LIMIT){
nodes.pop();
if (Meteor.isClient){
snackbar({
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
});
}
}
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry : [
...libraryNode.ancestors,
{id: libraryNodeId, collection: 'libraryNodes'}
],
oldParent : {id: _id, collection: 'libraryNodes'},
});
// Give the docs new IDs without breaking internal references
renewDocIds({docArray: nodes});
// Order the root node
libraryNode.order += 0.5;
LibraryNodes.batchInsert([libraryNode, ...nodes]);
// Tree structure changed by inserts, reorder the tree
reorderDocs({
collection: LibraryNodes,
ancestorId: libraryNode.ancestors[0].id,
});
return libraryNodeId;
},
});
export default duplicateLibraryNode;

View File

@@ -0,0 +1 @@
import '/imports/api/library/methods/duplicateLibraryNode.js';

View File

@@ -33,7 +33,7 @@ const AdjustmentSchema = new SimpleSchema({
const ComputedOnlyAdjustmentSchema = new SimpleSchema({
amountResult: {
type: SimpleSchema.Integer,
type: SimpleSchema.oneOf(String, Number),
optional: true,
},
amountErrors: {

View File

@@ -29,7 +29,7 @@ const DamageSchema = new SimpleSchema({
const ComputedOnlyDamageSchema = new SimpleSchema({
amountResult: {
type: SimpleSchema.oneOf(String, SimpleSchema.Integer),
type: SimpleSchema.oneOf(String, Number),
optional: true,
},
amountErrors: {

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

@@ -10,7 +10,11 @@ const InlineComputationSchema = new SimpleSchema({
type: String,
optional: true,
},
errors: ErrorSchema,
errors: {
type: Array,
optional: true,
},
'errors.$': ErrorSchema,
});
export default InlineComputationSchema;

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

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

@@ -53,6 +53,10 @@ export default class OperatorNode extends ParseNode {
}
toString(){
let {left, right, operator} = this;
// special case of adding a negative number
if (operator === '+' && right.isNumber && right.value < 0){
return `${left.toString()} - ${-right.value}`
}
return `${left.toString()} ${operator} ${right.toString()}`;
}
traverse(fn){

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

@@ -0,0 +1,6 @@
import { SyncedCron } from 'meteor/percolate:synced-cron';
SyncedCron.config({
// Log job run details to console
log: false,
});

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,31 +25,48 @@ 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,
}, {

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

@@ -18,7 +18,7 @@
</div>
</template>
<script>
<script lang="js">
import valueToCoins from '/imports/ui/utility/valueToCoins.js';
export default {

View File

@@ -3,19 +3,19 @@
v-model="opened"
:close-on-content-click="false"
transition="slide-y-transition"
lazy
left
>
<v-btn
slot="activator"
icon
>
<v-icon>format_paint</v-icon>
</v-btn>
<template #activator="{ on }">
<v-btn
icon
v-on="on"
>
<v-icon>format_paint</v-icon>
</v-btn>
</template>
<v-card class="overflow-hidden">
<v-card-text>
<v-layout
row
wrap
>
<div
@@ -72,14 +72,14 @@
</v-card-text>
<v-card-actions>
<v-btn
flat
text
@click="$emit('input')"
>
Clear
</v-btn>
<v-spacer />
<v-btn
flat
text
@click="opened = false"
>
Done
@@ -89,7 +89,7 @@
</v-menu>
</template>
<script>
<script lang="js">
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
import vuetifyColors from 'vuetify/es5/util/colors';
import { kebabToCamelCase, camelToKebabCase } from '/imports/ui/utility/swapCase.js';

View File

@@ -10,7 +10,7 @@
</div>
</template>
<script>
<script lang="js">
export default {
props: {
wideColumns: Boolean,

View File

@@ -30,7 +30,7 @@
</v-menu>
</template>
<script>
<script lang="js">
import IncrementMenu from '/imports/ui/components/IncrementMenu.vue';
export default {

View File

@@ -1,6 +1,5 @@
<template>
<v-layout
row
align-center
justify-center
class="increment-menu"
@@ -44,9 +43,9 @@
<v-btn
:small="!flat"
:fab="!flat"
:flat="flat"
:text="flat"
:icon="flat"
class="filled"
class="mx-2 filled"
@click="commitEdit"
>
<v-icon>done</v-icon>
@@ -54,9 +53,9 @@
<v-btn
:small="!flat"
:fab="!flat"
:flat="flat"
:text="flat"
:icon="flat"
class="mx-0 filled"
class="filled"
@click="cancelEdit"
>
<v-icon>close</v-icon>
@@ -65,7 +64,7 @@
</v-layout>
</template>
<script>
<script lang="js">
export default {
inject: {
context: { default: {} }
@@ -73,7 +72,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>
@@ -12,13 +14,13 @@
</v-btn>
</template>
<script>
<script lang="js">
/*
* Because speed dials only work well with v-btn's as children, this hacky
* component creates a v-btn with a label.
*/
export default {
props: ['icon', 'label'],
props: ['icon', 'label', 'disabled'],
}
</script>

View File

@@ -6,7 +6,7 @@
/>
</template>
<script>
<script lang="js">
import marked from 'marked';
export default {

View File

@@ -1,9 +1,9 @@
<template lang="html">
<v-card
:hover="hasClickListener"
:elevation="hovering ? 8 : undefined"
class="toolbar-card"
@click="$emit('click')"
:class="hovering ? 'elevation-8': ''"
@click.native="$emit('click')"
>
<v-toolbar
flat
@@ -23,14 +23,16 @@
</v-card>
</template>
<script>
<script lang="js">
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
import getThemeColor from '/imports/ui/utility/getThemeColor.js';
export default {
props: {
color: {
type: String,
default(){
return this.$vuetify.theme.secondary;
return getThemeColor('secondary');
},
},
},
@@ -62,4 +64,7 @@
.toolbar-card .v-toolbar__title {
font-size: 14px;
}
.toolbar-card {
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
</style>

View File

@@ -1,6 +1,6 @@
<template lang="html">
<div
class="layout row"
class="layout"
style="height: 100%;"
>
<div
@@ -22,7 +22,7 @@
</div>
</template>
<script>
<script lang="js">
export default {
computed:{
computedTreeStyle(){

View File

@@ -4,7 +4,7 @@
/>
</template>
<script>
<script lang="js">
import MarkdownText from '/imports/ui/components/MarkdownText.vue';
import embedInlineCalculations from '/imports/api/creature/computation/afterComputation/embedInlineCalculations.js';

View File

@@ -16,7 +16,7 @@
:loading="loading"
:error-messages="errors"
:disabled="isDisabled"
box
filled
v-on="on"
@focus="focused = true"
@blur="focused = false"
@@ -29,7 +29,7 @@
</v-menu>
</template>
<script>
<script lang="js">
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
import { format } from 'date-fns';

View File

@@ -2,13 +2,12 @@
<v-menu
v-model="menu"
:close-on-content-click="false"
lazy
transition="slide-y-transition"
min-width="290px"
style="overflow-y: auto;"
>
<template #activator="{ on }">
<div class="layout row align-center">
<div class="layout align-center">
<v-label>{{ label }}</v-label>
<v-btn
:loading="loading"
@@ -32,7 +31,7 @@
</template>
<v-card>
<v-card-text>
<div class="layout row">
<div class="layout">
<text-field
ref="iconSearchField"
label="Search icons"
@@ -51,7 +50,6 @@
</v-btn>
</div>
<v-layout
row
wrap
style="max-height: 400px; overflow-y: auto;"
>
@@ -78,7 +76,7 @@
</v-menu>
</template>
<script>
<script lang="js">
import SvgIcon from '/imports/ui/components/global/SvgIcon.vue';
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
import { findIcons } from '/imports/api/icons/Icons.js';

View File

@@ -9,7 +9,7 @@
/>
</template>
<script>
<script lang="js">
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
export default {

View File

@@ -7,7 +7,7 @@
:menu-props="{auto: true, lazy: true}"
:search-input.sync="searchInput"
:disabled="isDisabled"
box
filled
@change="customChange"
@focus="focused = true"
@blur="focused = false"
@@ -19,7 +19,7 @@
</v-combobox>
</template>
<script>
<script lang="js">
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
export default {

View File

@@ -6,7 +6,7 @@
:value="safeValue"
:menu-props="{auto: true, lazy: true}"
:disabled="isDisabled"
box
filled
@change="change"
@focus="focused = true"
@blur="focused = false"
@@ -18,7 +18,7 @@
</v-select>
</template>
<script>
<script lang="js">
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
export default {

View File

@@ -9,7 +9,7 @@
/>
</template>
<script>
<script lang="js">
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
export default {

View File

@@ -19,7 +19,7 @@
</i>
</template>
<script>
<script lang="js">
const SIZE_MAP = {
xSmall: '12px',
small: '16px',

View File

@@ -6,14 +6,14 @@
:value="safeValue"
:disabled="isDisabled"
:auto-grow="autoGrow"
box
filled
@input="input"
@focus="focused = true"
@blur="focused = false"
/>
</template>
<script>
<script lang="js">
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
export default {

View File

@@ -6,7 +6,7 @@
:error-messages="errors"
:value="safeValue"
:disabled="isDisabled"
:box="!regular"
:filled="!regular"
@input="input"
@focus="focused = true"
@blur="focused = false"
@@ -14,7 +14,7 @@
/>
</template>
<script>
<script lang="js">
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
export default {

View File

@@ -47,45 +47,45 @@
</v-btn>
</template>
<v-list>
<v-list-tile
<v-list-item
v-if="$listeners && $listeners.duplicate"
@click="$emit('duplicate')"
>
<v-list-tile-content>
<v-list-tile-title>
<v-list-item-content>
<v-list-item-title>
Duplicate
</v-list-tile-title>
</v-list-tile-content>
<v-list-tile-action>
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-icon>file_copy</v-icon>
</v-list-tile-action>
</v-list-tile>
<v-list-tile
</v-list-item-action>
</v-list-item>
<v-list-item
v-if="$listeners && $listeners.move"
@click="$emit('move')"
>
<v-list-tile-content>
<v-list-tile-title>
<v-list-item-content>
<v-list-item-title>
Move
</v-list-tile-title>
</v-list-tile-content>
<v-list-tile-action>
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-icon>send</v-icon>
</v-list-tile-action>
</v-list-tile>
<v-list-tile
</v-list-item-action>
</v-list-item>
<v-list-item
v-if="$listeners && $listeners.remove"
@click="$emit('remove')"
>
<v-list-tile-content>
<v-list-tile-title>
<v-list-item-content>
<v-list-item-title>
Delete
</v-list-tile-title>
</v-list-tile-content>
<v-list-tile-action>
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-icon>delete</v-icon>
</v-list-tile-action>
</v-list-tile>
</v-list-item-action>
</v-list-item>
</v-list>
</v-menu>
</v-layout>
@@ -118,11 +118,12 @@
</v-toolbar>
</template>
<script>
<script lang="js">
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import ColorPicker from '/imports/ui/components/ColorPicker.vue';
import getThemeColor from '/imports/ui/utility/getThemeColor.js';
export default {
components: {
@@ -142,7 +143,7 @@ export default {
return isDarkColor(this.color);
},
color(){
return this.model && this.model.color || this.$vuetify.theme.secondary;
return this.model && this.model.color || getThemeColor('secondary');
},
title(){
let model = this.model;

View File

@@ -2,22 +2,22 @@
<v-card>
<template v-if="!result">
<v-btn-toggle v-model="advantage">
<v-btn flat>
<v-btn text>
Advantage
</v-btn>
<v-btn flat>
<v-btn text>
Disadvantage
</v-btn>
</v-btn-toggle>
<v-card-text>
<div class="layout row justify-center align-center">
<div class="layout justify-center align-center">
<v-btn
large
fab
outline
outlined
@click="makeRoll"
>
<div class="display-1">
<div class="text-h4">
{{ numberToSignedString(bonus) }}
</div>
</v-btn>
@@ -26,7 +26,7 @@
</template>
<template v-else>
<div>
<div class="title">
<div class="text-h6">
<span
v-for="(roll, index) of rolls"
:key="index"
@@ -39,7 +39,7 @@
{{ numberToSignedString(bonus) }}
</span>
</div>
<div class="display-1">
<div class="text-h4">
{{ result }}
</div>
</div>
@@ -47,7 +47,7 @@
</v-card>
</template>
<script>
<script lang="js">
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
export default {
props: {

View File

@@ -0,0 +1,16 @@
// Modified from https://gitlab.com/tozd/vue/snackbar-que
import Vue from 'vue';
const globalState = Vue.observable({queue: []});
let lastSnackbarId = 0;
function snackbar(data) {
globalState.queue.push({
data,
id: ++lastSnackbarId,
enqueuedAt: new Date(),
shown: false,
});
}
export {snackbar, globalState}

View File

@@ -0,0 +1,128 @@
<template lang="html">
<v-snackbar
bottom
left
v-bind="$attrs"
:value="isShown"
:timeout="timeout"
@input="value => isShown = value"
>
<div class="layout align-center">
<template v-if="snackbar && snackbar.data">
<div v-if="snackbar.data.text">
{{ snackbar.data.text }}
</div>
<template v-else-if="snackbar.data.content">
<log-content :model="snackbar.data.content" />
</template>
<v-spacer />
<v-btn
v-if="snackbar.data.callback"
color="primary"
text
@click="closeSnackbar(); snackbar.data.callback()"
>
{{ snackbar.data.callbackName }}
</v-btn>
</template>
</div>
<template #action="{ attrs }">
<v-btn
icon
v-bind="attrs"
@click="closeSnackbar"
>
<v-icon>close</v-icon>
</v-btn>
</template>
</v-snackbar>
</template>
<script lang="js">
// Modified from https://gitlab.com/tozd/vue/snackbar-queue
import { globalState } from '/imports/ui/components/snackbars/SnackbarQueue.js';
import LogContent from '/imports/ui/log/LogContent.vue';
export default {
components: {
LogContent,
},
props: {
timeout: {
type: Number,
default: 6000,
},
pause: {
type: Number,
default: 300,
},
},
data() {
return {
isShown: false,
snackbar: null,
};
},
watch: {
isShown(newValue) {
if (newValue === false && this.snackbar) {
const snackbarIndex = globalState.queue.findIndex((element) => element.id === this.snackbar.id);
if (snackbarIndex > -1) {
globalState.queue.splice(snackbarIndex, 1);
}
this.snackbar = null;
}
},
},
created() {
this.handle = null;
this.unwait = null;
this.showNextSnackbar();
},
methods: {
clearSnackbarState() {
if (this.handle) {
clearTimeout(this.handle);
this.handle = null;
}
if (this.unwait) {
this.unwait();
this.unwait = null;
}
},
showNextSnackbar() {
this.clearSnackbarState();
// Wait for the first next snackbar to be available.
this.unwait = this.$wait(function () {
// Snackbars are enqueued from oldest to newest and "find" searches array elements in
// same order as well, so the first one which matches is also the oldest one.
return globalState.queue.find((element) => element.shown === false);
}, function (snackbar) {
this.unwait = null;
snackbar.shown = true;
this.snackbar = snackbar;
this.isShown = true;
this.handle = setTimeout(() => {
this.handle = null;
this.showNextSnackbar();
}, this.timeout + this.pause);
});
},
closeSnackbar() {
this.clearSnackbarState();
this.isShown = false;
setTimeout(() => {
this.showNextSnackbar();
}, this.pause);
},
},
};
</script>

View File

@@ -1,48 +0,0 @@
<template lang="html">
<v-snackbar
v-if="snackbar"
:key="snackbar.text"
auto-height
bottom
:value="true"
:timeout="0"
>
{{ snackbar.text }}
<v-btn
v-if="snackbar.callback"
class="primary--text"
flat
icon
@click="doCallback"
>
{{ snackbar.callbackName }}
</v-btn>
<v-btn
v-if="snackbar.showCloseButton"
flat
icon
@click="$store.dispatch('closeSnackbar')"
>
<v-icon>close</v-icon>
</v-btn>
</v-snackbar>
</template>
<script>
export default {
computed: {
snackbar(){
return this.$store.state.snackbars.snackbars[0];
}
},
methods: {
doCallback(){
this.snackbar.callback();
this.$store.dispatch('closeSnackbar')
}
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,47 +0,0 @@
const snackbarStore = {
state: {
snackbars: [],
snackbarTimout: undefined,
},
mutations: {
addSnackbar(state, value){
state.snackbars.push(value)
},
closeCurrentSnackbar (state){
state.snackbars.shift();
},
cancelSnackbarTimeout (state){
if(state.snackbarTimout){
clearTimeout(state.snackbarTimout);
}
},
setSnackbarTimout(state, value){
state.snackbarTimout = value;
},
},
actions: {
snackbar({dispatch, commit}, value){
// value = {
// text,
// showCloseButton,
// callback,
// callbackName
// }
commit('addSnackbar', value);
commit('setSnackbarTimout', setTimeout(() => {
dispatch('closeSnackbar');
}, 5000));
},
closeSnackbar({dispatch, commit, state}){
commit('closeCurrentSnackbar');
commit('cancelSnackbarTimeout');
if (state.snackbars.length){
commit('setSnackbarTimout', setTimeout(() => {
dispatch('closeSnackbar');
}, 5000));
}
},
}
};
export default snackbarStore;

View File

@@ -5,7 +5,7 @@
:data-id="`tree-node-${node._id}`"
>
<div
class="layout row align-center justify-start tree-node-title"
class="layout align-center justify-start tree-node-title"
style="cursor: pointer;"
:class="selected && 'primary--text'"
@click.stop="$emit('selected', node._id)"
@@ -22,7 +22,7 @@
</v-icon>
</v-btn>
<div
class="layout row align-center justify-start pr-1"
class="layout align-center justify-start pr-1"
style="flex-grow: 0;"
>
<v-icon
@@ -70,7 +70,7 @@
</v-sheet>
</template>
<script>
<script lang="js">
/**
* TreeNode's are list item views of character properties. Every property which
* can belong to the character is shown in the tree view of the character
@@ -100,7 +100,7 @@
}},
computed: {
hasChildren(){
return this.children && this.children.length || this.lazy && !this.expanded;
return this.children && !!this.children.length || this.lazy && !this.expanded;
},
showExpanded(){
return this.expanded && (this.organize || this.hasChildren)
@@ -159,9 +159,15 @@
transition: none !important;
}
.theme--light .tree-node-title:hover {
background: rgba(0,0,0,.04);
background-color: rgba(0,0,0,.04);
}
.dummy-node {
.theme--dark .tree-node-title:hover {
background-color: rgba(255,255,255,.04);
}
.tree-node-title{
transition: background ease 0.3s, color ease 0.15s;
}
.tree-node-title, .dummy-node {
height: 40px;
}
</style>

View File

@@ -30,7 +30,7 @@
</draggable>
</template>
<script>
<script lang="js">
import draggable from 'vuedraggable';
import TreeNode from '/imports/ui/components/tree/TreeNode.vue';
import { isParentAllowed } from '/imports/api/parenting/parenting.js';

View File

@@ -39,6 +39,28 @@
:input-value="model.settings.hideUnusedStats"
@change="value => $emit('change', {path: ['settings','hideUnusedStats'], value: !!value})"
/>
<v-switch
label="Show spells tab"
:input-value="!model.settings.hideSpellsTab"
@change="value => {
$emit('change', {path: ['settings','hideSpellsTab'], value: !value});
$store.commit(
'setTabForCharacterSheet',
{id: model._id, tab: 0}
);
}"
/>
<v-switch
label="Show tree tab"
:input-value="model.settings.showTreeTab"
@change="value => {
$emit('change', {path: ['settings','showTreeTab'], value: !!value});
$store.commit(
'setTabForCharacterSheet',
{id: model._id, tab: 0}
);
}"
/>
<text-field
label="Hit Dice reset multiplier"
hint="What fraction of your hit dice are reset every long rest"
@@ -82,7 +104,7 @@
</div>
</template>
<script>
<script lang="js">
import FormSection, {FormSections} from '/imports/ui/properties/forms/shared/FormSection.vue';
export default {

View File

@@ -20,7 +20,7 @@
<v-spacer slot="actions" />
<v-btn
slot="actions"
flat
text
@click="$store.dispatch('popDialogStack')"
>
Done
@@ -28,7 +28,7 @@
</dialog-base>
</template>
<script>
<script lang="js">
import Creatures from '/imports/api/creature/Creatures.js';
import {updateCreature} from '/imports/api/creature/Creatures.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';

View File

@@ -2,8 +2,8 @@
lang="html"
functional
>
<v-list-tile v-bind="$attrs">
<v-list-tile-avatar :color="model.color || 'grey'">
<v-list-item v-bind="$attrs">
<v-list-item-avatar :color="model.color || 'grey'">
<img
v-if="model.avatarPicture"
:src="model.avatarPicture"
@@ -12,25 +12,25 @@
<template v-else>
{{ model.initial }}
</template>
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
{{ model.name }}
</v-list-tile-title>
<v-list-tile-sub-title>
</v-list-item-title>
<v-list-item-subtitle>
{{ model.alignment }} {{ model.gender }} {{ model.race }}
</v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action v-if="selection">
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action v-if="selection">
<v-checkbox
:input-value="selected && selected.has(model._id)"
@change="$emit('select')"
/>
</v-list-tile-action>
</v-list-tile>
</v-list-item-action>
</v-list-item>
</template>
<script type="text/javascript">
<script lang="js">
export default {
props: {
model: {

View File

@@ -2,7 +2,7 @@
<v-btn
:loading="loading"
:disabled="context.editPermission === false"
outline
outlined
style="width: 160px;"
@click="rest"
>
@@ -13,7 +13,7 @@
</v-btn>
</template>
<script>
<script lang="js">
import restCreature from '/imports/api/creature/restCreature.js';
export default {

View File

@@ -52,7 +52,6 @@
label="Race"
/>
<v-layout
row
justify-center
align-center
>
@@ -225,20 +224,19 @@
:items="hitDiceItems"
label="Hit Dice"
/>
</v-text-field>
</v-stepper-content>
</v-stepper-items>
</v-stepper>
<template slot="actions">
<v-btn
flat
text
@click="$emit('pop')"
>
Cancel
</v-btn>
<v-btn
v-if="step > 1"
flat
text
@click="step--"
>
Back
@@ -262,7 +260,7 @@
</dialog-base>
</template>
<script>
<script lang="js">
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
const getCost = function(score){
const costs = {

View File

@@ -22,7 +22,7 @@
<v-spacer slot="actions" />
<v-btn
slot="actions"
flat
text
@click="$store.dispatch('popDialogStack')"
>
Cancel
@@ -30,7 +30,7 @@
</dialog-base>
</template>
<script>
<script lang="js">
import Creatures from '/imports/api/creature/Creatures.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import removeCreature from '/imports/api/creature/removeCreature.js';

View File

@@ -36,6 +36,7 @@
>
<v-tabs-items
:value="$store.getters.tabById($route.params.id)"
class="card-background"
@change="e => $store.commit(
'setTabForCharacterSheet',
{id: $route.params.id, tab: e}
@@ -50,13 +51,13 @@
<v-tab-item>
<inventory-tab :creature-id="creatureId" />
</v-tab-item>
<v-tab-item>
<v-tab-item v-show="!creature.settings.hideSpellsTab">
<spells-tab :creature-id="creatureId" />
</v-tab-item>
<v-tab-item>
<character-tab :creature-id="creatureId" />
</v-tab-item>
<v-tab-item>
<v-tab-item v-if="creature.settings.showTreeTab">
<tree-tab :creature-id="creatureId" />
</v-tab-item>
</v-tabs-items>
@@ -65,7 +66,7 @@
</div>
</template>
<script>
<script lang="js">
//TODO add a "no character found" screen if shown on a false address
// or on a character the user does not have permission to view
import Creatures from '/imports/api/creature/Creatures.js';
@@ -77,6 +78,7 @@
import TreeTab from '/imports/ui/creature/character/characterSheetTabs/TreeTab.vue';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default {
components: {
@@ -118,13 +120,10 @@
this.logObserver = CreatureLogs.find({
creatureId: this.creatureId,
}).observe({
added(doc){
added({content}){
if (!that.$subReady.singleCharacter) return;
if (that.$store.state.rightDrawer) return;
that.$store.dispatch('snackbar', {
text: doc.name,
showCloseButton: true,
});
snackbar({content});
},
});
},

View File

@@ -0,0 +1,235 @@
<template lang="html">
<v-speed-dial
v-if="speedDials"
v-model="fab"
direction="bottom"
>
<template #activator>
<v-btn
v-model="fab"
color="primary"
fab
data-id="insert-creature-property-fab"
class="insert-creature-property-fab"
small
>
<transition
name="fab-rotate"
>
<v-icon
style="transition: transform 0.2s ease-in-out"
:style="fab && 'transform: rotate(45deg)'"
>
add
</v-icon>
</transition>
</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)"
/>
<template v-if="tabNumber === 5">
<labeled-fab
key="property"
color="primary"
data-id="insert-creature-property-btn"
label="New Property"
icon="create"
:disabled="!editPermission"
@click="insertTreeProperty"
/>
<labeled-fab
key="property"
color="primary"
data-id="insert-creature-property-from-library-btn"
label="Property From Library"
icon="book"
:disabled="!editPermission"
@click="propertyFromLibrary"
/>
</template>
</v-speed-dial>
</template>
<script lang="js">
import LabeledFab from '/imports/ui/components/LabeledFab.vue';
import { getHighestOrder } 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';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
function getParentAndOrderFromSelectedTreeNode(creatureId){
// find the parent based on the currently selected property
let el = document.querySelector('.tree-tab .tree-node-title.primary--text');
let selectedComponent = el && el.parentElement.__vue__.$parent;
let parentRef, order;
if (selectedComponent){
if (selectedComponent.showExpanded){
parentRef = {
id: selectedComponent.node._id,
collection: 'creatureProperties',
};
order = getHighestOrder({
collection: CreatureProperties,
ancestorId: parentRef.id,
}) + 0.5;
} else {
parentRef = selectedComponent.node.parent;
order = selectedComponent.node.order + 0.5;
}
} else {
parentRef = {collection: 'creatures', id: creatureId};
order = getHighestOrder({
collection: CreatureProperties,
ancestorId: parentRef.id,
}) + 0.5;
}
return {parentRef, order}
}
function hideFab(){
let fab = document.querySelector('.insert-creature-property-fab');
if (fab) fab.style.opacity = '0';
return fab;
}
function revealFab(fab){
if (!fab) return;
// Bring back the fab with scale up animation
fab.style.transition = 'none';
fab.style.opacity = '';
fab.style.transform = 'scale(0)';
setTimeout(()=> {
fab.style.transform = '';
fab.style.transition = '';
}, 400);
}
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'],
'tree': [],
};},
properties(){
return PROPERTIES;
},
},
methods: {
insertPropertyOfType(type){
let creatureId = this.creatureId;
let fab = hideFab();
// Open the dialog to insert the property
this.$store.commit('pushDialogStack', {
component: 'creature-property-creation-dialog',
elementId: 'insert-creature-property-type-' + type,
data: {
forcedType: type,
},
callback(creatureProperty){
if (!creatureProperty) return 'insert-creature-property-fab';
revealFab(fab);
// Insert the property
creatureProperty.order = getHighestOrder({
collection: CreatureProperties,
ancestorId: creatureId
}) + 1;
let id = insertProperty.call({
creatureProperty,
parentRef: {collection: 'creatures', id: creatureId},
});
return id;
}
});
},
insertTreeProperty(){
let creatureId = this.creatureId;
let fab = hideFab();
// Open the dialog to insert the property
this.$store.commit('pushDialogStack', {
component: 'creature-property-creation-dialog',
elementId: 'insert-creature-property-btn',
callback(creatureProperty){
if (!creatureProperty) return 'insert-creature-property-fab';
revealFab(fab);
// Get order and parent
let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId);
creatureProperty.order = order;
// Insert the property
let id = insertProperty.call({creatureProperty, parentRef});
return `tree-node-${id}`;
}
});
},
propertyFromLibrary(){
let creatureId = this.creatureId;
let fab = hideFab();
this.$store.commit('pushDialogStack', {
component: 'creature-property-from-library-dialog',
elementId: 'insert-creature-property-from-library-btn',
callback(libraryNode){
if (!libraryNode) return 'insert-creature-property-fab';
revealFab(fab);
let nodeId = libraryNode._id;
let {parentRef, order } = getParentAndOrderFromSelectedTreeNode(creatureId);
let id = insertPropertyFromLibraryNode.call({nodeId, parentRef, order});
return `tree-node-${id}`;
}
});
},
}
}
</script>
<style lang="css" scoped>
.insert-creature-property-fab {
transition: transform 0.07s cubic-bezier(0.5, 0.2, 0.8, 0.4) 0s;
}
</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';
<script lang="js">
import CharacterLog from '/imports/ui/log/CharacterLog.vue';
export default {
components: {
LogTab,
CharacterLog,
},
computed: {
drawer: {

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