Compare commits

...

104 Commits

Author SHA1 Message Date
Stefan Zermatten
27f1f4e720 Fixed a bug with dialog animations hiding buttons by failing to clean up 2021-01-12 13:51:36 +02:00
Stefan Zermatten
d63b8c835d Fixed bug in last release where unlimited slots were always hidden on hide when full 2021-01-12 13:51:11 +02:00
Stefan Zermatten
85f3881935 Removed alert about data loss, most data structures are stable and it's being alarmist 2021-01-12 13:40:02 +02:00
Stefan Zermatten
5e4299e6db removed stray console logging 2021-01-12 13:24:08 +02:00
Stefan Zermatten
0e663f36db Slot fillers that count as more than one slot now update the space left correctly.
Slot fillers removed from a library can no longer be added to a slot.
If a slot has limited space left, this will be reflected on the error 
finding slots message
2021-01-12 13:22:48 +02:00
Stefan Zermatten
de9ea5922c Fixed bugs when a spell list does not have limit on prepared spells 2021-01-12 13:10:36 +02:00
Stefan Zermatten
a2cfe43e74 Disabled the direct damage input in the attribute form: it can't be edited anyway 2021-01-12 13:04:25 +02:00
Stefan Zermatten
026c11c13b Made sure attributes show their currentValue instead of Value 2021-01-12 13:03:33 +02:00
Stefan Zermatten
403f2663c2 Fixed bugs with item display, equipment will now automatically move to the first property with the 'equipment' tag, carried items will move to the first property with the 'carried' tag 2021-01-12 12:54:02 +02:00
Stefan Zermatten
28c042343e All children of infinite slots will now hide when "hide when full" is active 2021-01-12 10:36:07 +02:00
Stefan Zermatten
3f116875a1 Children of slots now display in the correct order 2021-01-12 10:35:37 +02:00
Stefan Zermatten
ae5b4b7d5c Made inactive toggle decendants specifically included when recomputing active properties 2021-01-11 22:03:54 +02:00
Stefan Zermatten
75835d74f6 Merge pull request #259 from Ganonsmasher/version-2
Found a fix for toggles.
2020-12-17 17:19:54 +02:00
Ganonsmasher
583b652fc4 Found a fix for toggles.
It doesn't fix the problem of their property being absent on the stats page cards, but they can compute properly and disable their children with this.
2020-12-15 18:08:29 -05:00
Stefan Zermatten
df317a8942 Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2020-11-20 14:21:52 +02:00
Stefan Zermatten
ff0e9b1ff9 Added Lord of Junk to Patreon Paragons 2020-11-20 14:13:47 +02:00
Stefan Zermatten
fa24430a7f Fixed parsing of variable names with numbers and stacked dice rolls like dd8-> 1d(1d8) 2020-11-13 10:04:37 +02:00
Stefan Zermatten
fde2f821e7 Fixed parser not handling whitespace 2020-11-12 21:44:08 +02:00
Stefan Zermatten
827430c987 Fixed edit permission errors for some creature prop methods 2020-11-12 21:25:48 +02:00
Stefan Zermatten
2a1aa02e97 Added true and false keywords, fixed grammar ambiguity in if statements 2020-11-12 15:11:24 +02:00
Stefan Zermatten
aeb347084f Fixed ambiguitiy in grammar caused by previous fixes 2020-11-12 13:47:10 +02:00
Stefan Zermatten
005bc162cb Fixed != not being matched because ! was matched first. Fixed presedence for & | and relational operators 2020-11-12 13:45:14 +02:00
Stefan Zermatten
9941d91bb8 Fixed != operator, separated == and ===, != and !== for strictness control 2020-11-12 13:44:01 +02:00
Stefan Zermatten
23c77690a1 Healing damage type now heals instead of damaging 2020-11-12 13:01:09 +02:00
Stefan Zermatten
525b528d9a Added attribute damage and self damage results to actions and log. 2020-11-12 12:57:48 +02:00
Stefan Zermatten
3917f63d5e Increased subscription rate limit to prevent infinitely spinning characters 2020-11-12 11:04:04 +02:00
Stefan Zermatten
bd056ab042 Improved subscription permissions, should now work as expected for public documents 2020-11-12 10:48:46 +02:00
Stefan Zermatten
cd84b2562a Fixed an error with finding deployed version SHA 2020-11-10 14:48:09 +02:00
Stefan Zermatten
c7436ffb1e Caught error where git couldn't be used to get version 2020-11-10 14:37:32 +02:00
Stefan Zermatten
be7d7f898f disabled short and log rest buttons if user has no edit permission 2020-11-10 14:22:09 +02:00
Stefan Zermatten
3024168e95 Replaced old parser with new parser 2020-11-10 14:07:22 +02:00
Stefan Zermatten
1f0678b50b Added not operator to the parser 2020-11-05 15:32:01 +02:00
Stefan Zermatten
4dad2c41e5 Updated parser to accept underscores in variable names 2020-11-05 14:50:44 +02:00
Stefan Zermatten
46385dd9b2 Build details with no slots hidden moved to a dialog 2020-11-05 14:27:01 +02:00
Stefan Zermatten
7cb65954b5 Added the ability to hide slots when full 2020-11-05 14:05:17 +02:00
Stefan Zermatten
749799d869 Denormalised slot fill total to database 2020-11-05 13:35:55 +02:00
Stefan Zermatten
3293dad671 Limited what fields are included when fetching recompute documents to improve performance 2020-11-05 12:59:48 +02:00
Stefan Zermatten
88df942c59 Fixed an error with missing identity details in patreon request 2020-11-05 12:59:26 +02:00
Stefan Zermatten
9722bbc667 Characters now recompute on subscribe if they haven't been computed in the current version 2020-11-04 14:27:31 +02:00
Stefan Zermatten
2fb0ba79c6 began work to get inactive state of properties denormalised 2020-11-03 15:57:14 +02:00
Stefan Zermatten
3f7ddd62fc Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud1 into version-2 2020-10-27 10:41:06 +02:00
Stefan Zermatten
227d6c5aae Markdown and calculations now supported in slot filler descriptions 2020-10-27 10:39:20 +02:00
Stefan Zermatten
147ef97576 Markdown now supported in slot filler descriptions 2020-10-27 10:34:01 +02:00
Stefan Zermatten
54806b0f3c Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud1 into version-2 2020-10-19 11:39:24 +02:00
Stefan Zermatten
1165158d46 Forced creatures to reorder their docs before recomputing 2020-10-19 11:39:21 +02:00
Stefan Zermatten
32de70cd45 Class levels now have a description field 2020-10-17 19:33:56 +02:00
Stefan Zermatten
68499e4de5 You can now click on properties filling slots to view their details dialog 2020-10-17 19:28:54 +02:00
Stefan Zermatten
3f4cb8e26b Added undo buttons for deleting properties off a creature 2020-10-17 19:10:37 +02:00
Stefan Zermatten
ebab41838c Used tree node views in slot fill selection 2020-10-17 16:56:23 +02:00
Stefan Zermatten
e8da7a6c17 Moved snackbars to their own store and component 2020-10-17 16:06:27 +02:00
Stefan Zermatten
46189c68df All property forms now allow tags 2020-10-17 13:42:24 +02:00
Stefan Zermatten
9fa997ed24 Started moving snackbars to vue store, still needs to be separated into its own module 2020-10-16 13:38:58 +02:00
Stefan Zermatten
e3bf6557ec fixed bug in dialog store 2020-10-16 10:35:51 +02:00
Stefan Zermatten
7aa3e5a217 Stringified errors from scheduled deletion 2020-10-16 10:23:34 +02:00
Stefan Zermatten
dc1b025090 Increased delete job frequency to 10 minutes 2020-10-16 10:00:07 +02:00
Stefan Zermatten
2e370a9884 Fixed removed slots not being hidden 2020-10-15 16:42:57 +02:00
Stefan Zermatten
384fa076f1 hotfix tags not filtering correctly 2020-10-15 16:12:32 +02:00
Stefan Zermatten
7922e30ddc Added tags to some properties. Added condition to class levels 2020-10-15 16:00:32 +02:00
Stefan Zermatten
1ba4f76763 Class levels can now have conditions 2020-10-15 15:57:19 +02:00
Stefan Zermatten
839f91c3b2 Fixed icons not going to dark mode when slot filling cards are selected 2020-10-15 15:30:11 +02:00
Stefan Zermatten
26567ce840 slot fill cards with pictures no longer get icons 2020-10-15 15:25:10 +02:00
Stefan Zermatten
2a729a4eca Made slot fill dialog a list of cards to leverage pretty slotFillers 2020-10-15 15:24:14 +02:00
Stefan Zermatten
ed17d9e2d2 Added slotfiller property type to increase control over slot filling 2020-10-15 14:54:58 +02:00
Stefan Zermatten
8e9405b5ad Allowed slots with unlimited children, improved slot ui text 2020-10-15 13:50:46 +02:00
Stefan Zermatten
7fc783dcad Removed recompute button 2020-10-15 13:50:14 +02:00
Stefan Zermatten
b15ad7e51a Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud1 into version-2 2020-10-15 13:49:49 +02:00
Stefan Zermatten
8a3d2474fc Merge pull request #254 from JoeZwet/version-2
fix: prevent discord mention exploit
2020-10-15 13:48:30 +02:00
Joe van der Zwet
09371e7d54 add requested changes 2020-10-16 00:47:32 +13:00
Joe van der Zwet
0776d33909 fix: prevent discord mention exploit 2020-10-16 00:36:32 +13:00
Stefan Zermatten
6e98d71c3c Improved slot UI look and feel 2020-10-15 13:00:29 +02:00
Stefan Zermatten
8f89f4b63f Ensured all subscriptions return empty arrays instead of errors or ready 2020-10-15 12:34:46 +02:00
Stefan Zermatten
c0070d017e Removed debugging code 2020-10-14 15:54:16 +02:00
Stefan Zermatten
51569592ab First implementation on Slots UI 2020-10-14 14:45:26 +02:00
Stefan Zermatten
d2cb86ac27 Fixed broken logging for actions 2020-10-14 11:33:25 +02:00
Stefan Zermatten
bde9183158 Log optimistic UI now fixed, rolls are now instant 2020-10-14 11:25:05 +02:00
Stefan Zermatten
0cc9e01754 Renamed, moved LogTab to CharacterLog 2020-10-14 09:37:00 +02:00
Stefan Zermatten
9856471202 Stopped log making toast if it's visible 2020-10-14 09:33:17 +02:00
Stefan Zermatten
4f77782a7a log messages are now aligned right 2020-10-13 13:53:30 +02:00
Stefan Zermatten
5f13aaa031 Fixed empty strings in log input 2020-10-13 13:44:06 +02:00
Stefan Zermatten
1321cf6a96 Moved log tab to right drawer 2020-10-13 13:42:18 +02:00
Stefan Zermatten
dee8249f61 Creature logs are now removed with creatures 2020-10-13 12:43:55 +02:00
Stefan Zermatten
0af0afc0d0 Discord webhooks now mirror character log 2020-10-13 12:42:02 +02:00
Stefan Zermatten
a104fc8a87 Fixing broken casing on file pt.2 2020-10-06 10:55:12 +02:00
Stefan Zermatten
46f452987f Fixing broken casing on file 2020-10-06 10:54:52 +02:00
Stefan Zermatten
a87cb1286a Improved custom rolls on log tab 2020-10-06 09:53:08 +02:00
Stefan Zermatten
844588cdbf Started adding text input to log tab 2020-09-30 16:24:33 +02:00
Stefan Zermatten
a6a96fc19f Started work on character log for rolls to be stored 2020-09-29 22:34:30 +02:00
Stefan Zermatten
75ab43da00 Started work on UI for rolling checks 2020-09-29 16:37:28 +02:00
Stefan Zermatten
df7000889b fixed security deps 2020-09-29 10:54:37 +02:00
Stefan Zermatten
65754dea80 removed damage multipliers from health bar card, it has its own card 2020-09-29 10:54:27 +02:00
Stefan Zermatten
30cca39e7c Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud1 into version-2 2020-09-28 13:58:03 +02:00
Stefan Zermatten
5ad5c914fb Added gitignore file for renders 2020-09-24 19:00:28 +02:00
Stefan Zermatten
f27550362a Fixed various parser bugs, implemented unary operators 2020-09-18 22:13:12 +02:00
Stefan Zermatten
50f7977a60 Fixed patreon update write location 2020-09-18 20:37:16 +02:00
Stefan Zermatten
bc5c465a32 Started work on checks 2020-09-18 14:00:29 +02:00
Stefan Zermatten
c8ddf9d547 Added the ability to double all number of dice to roll using context 2020-09-18 12:24:08 +02:00
Stefan Zermatten
6570665c1e Added functions and ensured the context was being passed around correctly 2020-09-18 11:52:44 +02:00
Stefan Zermatten
06f17a6d33 Parser now uses context to store details of the computation 2020-09-18 10:14:53 +02:00
Stefan Zermatten
b69ad6c306 Removed unused parser code 2020-09-10 11:39:27 +02:00
Stefan Zermatten
5dec760452 Parser now works with variables passed into scope 2020-09-10 11:38:28 +02:00
Stefan Zermatten
ede4e1367d Continued work on parser, now calling functions and rolling correctly 2020-09-10 00:14:24 +02:00
Stefan Zermatten
81645df2a6 Lots of work on the parser including testing interface 2020-09-09 17:09:50 +02:00
Stefan Zermatten
445171ce80 Added preferences subheader to accounts page 2020-09-09 13:58:48 +02:00
Stefan Zermatten
dedab7b046 Added patreon tier refresh button, autorefresh tier on login daily 2020-09-09 13:55:21 +02:00
155 changed files with 4013 additions and 1209 deletions

View File

@@ -3,7 +3,7 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
accounts-password@1.6.0
accounts-password@1.6.2
accounts-ui@1.3.1
random@1.2.0
dburles:collection-helpers
@@ -13,7 +13,7 @@ momentjs:moment
dburles:mongo-collection-instances
percolate:migrations
accounts-google@1.3.3
email@1.2.3
email@2.0.0
meteorhacks:subs-manager
chuangbo:marked
meteor-base@1.4.0
@@ -38,7 +38,7 @@ ongoworks:speakingurl
service-configuration@1.0.11
google-config-ui@1.0.1
dynamic-import@0.5.2
ddp-rate-limiter@1.0.7
ddp-rate-limiter@1.0.9
rate-limit@1.0.9
meteortesting:mocha
mdg:validated-method

View File

@@ -1 +1 @@
METEOR@1.10.2
METEOR@1.11.1

View File

@@ -1,7 +1,7 @@
accounts-base@1.6.0
accounts-base@1.7.0
accounts-google@1.3.3
accounts-oauth@1.2.0
accounts-password@1.6.0
accounts-password@1.6.2
accounts-patreon@0.1.0
accounts-ui@1.3.1
accounts-ui-unstyled@1.4.2
@@ -10,7 +10,7 @@ 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
aldeed:collection2@3.0.6
aldeed:collection2@3.2.1
aldeed:schema-index@3.0.0
allow-deny@1.1.0
autoupdate@1.6.0
@@ -20,8 +20,8 @@ base64@1.0.12
binary-heap@1.0.11
blaze@2.3.4
blaze-tools@1.0.10
boilerplate-generator@1.7.0
bozhao:link-accounts@2.1.1
boilerplate-generator@1.7.1
bozhao:link-accounts@2.2.1
caching-compiler@1.2.2
caching-html-compiler@1.1.3
callback-hook@1.3.0
@@ -34,17 +34,17 @@ dburles:mongo-collection-instances@0.3.5
ddp@1.4.0
ddp-client@2.3.3
ddp-common@1.4.0
ddp-rate-limiter@1.0.7
ddp-server@2.3.1
ddp-rate-limiter@1.0.9
ddp-server@2.3.2
deps@1.0.12
diff-sequence@1.1.1
dynamic-import@0.5.2
dynamic-import@0.5.3
ecmascript@0.14.3
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.10.0
ecmascript-runtime-server@0.9.0
ecmascript-runtime-client@0.11.0
ecmascript-runtime-server@0.10.0
ejson@1.1.1
email@1.2.3
email@2.0.0
es5-shim@4.8.0
fetch@0.1.1
geojson-utils@1.0.10
@@ -69,11 +69,11 @@ 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.3
meteortesting:browser-tests@1.3.4
meteortesting:mocha@1.1.5
meteortesting:mocha-core@7.0.1
mikowals:batch-insert@1.1.9
minifier-css@1.5.0
mikowals:batch-insert@1.2.0
minifier-css@1.5.3
minifier-js@2.6.0
minimongo@1.6.0
mobile-experience@1.1.0
@@ -81,14 +81,14 @@ mobile-status-bar@1.1.0
modern-browsers@0.1.5
modules@0.15.0
modules-runtime@0.12.0
momentjs:moment@2.24.0
momentjs:moment@2.29.1
mongo@1.10.0
mongo-decimal@0.1.1
mongo-dev-server@1.1.0
mongo-id@1.0.7
npm-bcrypt@0.9.3
npm-mongo@3.7.0
oauth@1.3.0
npm-mongo@3.8.1
oauth@1.3.2
oauth2@1.3.0
observe-sequence@1.0.16
ongoworks:speakingurl@9.0.0
@@ -111,7 +111,7 @@ reactive-var@1.0.11
reload@1.3.0
retry@1.1.0
routepolicy@1.1.0
seba:minifiers-autoprefixer@1.1.2
seba:minifiers-autoprefixer@1.2.1
service-configuration@1.0.11
session@1.2.0
sha@1.0.9
@@ -119,7 +119,7 @@ shell-server@0.5.0
simple:json-routes@2.1.0
simple:rest@1.1.1
simple:rest-method-mixin@1.0.1
socket-stream-client@0.3.0
socket-stream-client@0.3.1
spacebars@1.0.15
spacebars-compiler@1.1.3
srp@1.1.0
@@ -133,6 +133,6 @@ templating-tools@1.1.2
tmeasday:check-npm-versions@0.3.2
tracker@1.2.0
underscore@1.0.10
url@1.3.0
url@1.3.1
webapp@1.9.1
webapp-hashing@1.0.9

View File

@@ -9,7 +9,7 @@ import { recomputeCreature } from '/imports/api/creature/computation/recomputeCr
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import Creatures from '/imports/api/creature/Creatures.js';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { softRemove } from '/imports/api/parenting/softRemove.js';
import { softRemove, restore } from '/imports/api/parenting/softRemove.js';
import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js';
import propertySchemasIndex from '/imports/api/properties/computedPropertySchemasIndex.js';
import {
@@ -19,8 +19,10 @@ import {
} from '/imports/api/parenting/parenting.js';
import {setDocToLastOrder} from '/imports/api/parenting/order.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import '/imports/api/creature/actions/doAction.js';
import '/imports/api/creature/creatureProperties/manageEquipment.js';
let CreatureProperties = new Mongo.Collection('creatureProperties');
@@ -43,7 +45,22 @@ let CreaturePropertySchema = new SimpleSchema({
icon: {
type: storedIconsSchema,
optional: true,
}
},
// Denormalised flag if this property is inactive on the sheet for any reason
// Including being disabled, or a decendent of a disabled property
inactive: {
type: Boolean,
optional: true,
index: 1,
},
// Denormalised flag if this property was made inactive by an inactive
// ancestor. True if this property has an inactive ancestor even if this
// property is itself inactive
deactivatedByAncestor: {
type: Boolean,
optional: true,
index: 1,
},
});
for (let key in propertySchemasIndex){
@@ -73,6 +90,7 @@ function assertPropertyEditPermission(property, userId){
function recomputeCreatures(property){
for (let ref of property.ancestors){
if (ref.collection === 'creatures') {
reorderDocs({collection: CreatureProperties, ancestorId: ref.id});
recomputeCreature.call({charId: ref.id});
}
}
@@ -197,7 +215,7 @@ const insertPropertyFromLibraryNode = new ValidatedMethod({
const updateProperty = new ValidatedMethod({
name: 'creatureProperties.update',
validate({_id, path}){
if (!_id) return false;
if (!_id) throw new Meteor.Error('No _id', '_id is required');
// We cannot change these fields with a simple update
switch (path[0]){
case 'type':
@@ -247,6 +265,7 @@ export function damagePropertyWork({property, operation, value}){
}, {
selector: property
});
return currentValue - damage;
} else if (operation === 'increment'){
let currentValue = property.value - (property.damage || 0);
let currentDamage = property.damage;
@@ -260,9 +279,55 @@ export function damagePropertyWork({property, operation, value}){
}, {
selector: property
});
return increment;
}
}
const damagePropertiesByName = new ValidatedMethod({
name: 'CreatureProperties.damagePropertiesByName',
validate: new SimpleSchema({
creatureId: SimpleSchema.RegEx.Id,
variableName: {
type: String,
},
operation: {
type: String,
allowedValues: ['set', 'increment']
},
value: Number,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 20,
timeInterval: 5000,
},
run({creatureId, variableName, operation, value}) {
// Check permissions
let creature = Creatures.findOne(creatureId, {
fields: {
damageMultipliers: 1,
owner: 1,
readers: 1,
writers: 1,
},
});
assertEditPermission(creature, this.userId);
CreatureProperties.find({
'ancestors.id': creatureId,
variableName,
removed: {$ne: false},
inactive: {$ne: true},
}).forEach(property => {
// Check if property can take damage
let schema = CreatureProperties.simpleSchema(property);
if (!schema.allowsKey('damage')) return;
// Damage the property
damagePropertyWork({property: property, operation, value})
});
recomputeCreature.call({charId: creatureId});
}
})
const damageProperty = new ValidatedMethod({
name: 'creatureProperties.damage',
validate: new SimpleSchema({
@@ -295,6 +360,59 @@ const damageProperty = new ValidatedMethod({
},
});
const dealDamage = new ValidatedMethod({
name: 'creatureProperties.dealDamage',
validate: new SimpleSchema({
creatureId: SimpleSchema.RegEx.Id,
damageType: {
type: String,
},
amount: Number,
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 20,
timeInterval: 5000,
},
run({creatureId, damageType, amount}) {
let creature = Creatures.findOne(creatureId, {
fields: {
damageMultipliers: 1,
owner: 1,
readers: 1,
writers: 1,
},
});
// Check permissions
assertEditPermission(creature, this.userId);
let healthBars = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'attribute',
attributeType:'healthBar',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: -1},
});
let multiplier = creature.damageMultipliers[damageType];
if (multiplier === undefined) multiplier = 1;
let totalDamage = Math.floor(amount * multiplier);
let damageLeft = totalDamage;
if (damageType === 'healing') damageLeft = -totalDamage;
healthBars.forEach(healthBar => {
if (damageLeft === 0) return;
let damageAdded = damagePropertyWork({
property: healthBar,
operation: 'increment',
value: damageLeft,
});
damageLeft -= damageAdded;
});
recomputeCreature.call({charId: creatureId});
return totalDamage;
},
});
export function adjustQuantityWork({property, operation, value}){
// Check if property has quantity
let schema = CreatureProperties.simpleSchema(property);
@@ -443,6 +561,23 @@ const softRemoveProperty = new ValidatedMethod({
}
});
const restoreProperty = new ValidatedMethod({
name: 'creatureProperties.restore',
validate: new SimpleSchema({
_id: SimpleSchema.RegEx.Id
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id}){
let property = CreatureProperties.findOne(_id);
assertPropertyEditPermission(property, this.userId);
restore({_id, collection: CreatureProperties});
recomputeCreatures(property);
}
});
export default CreatureProperties;
export {
@@ -451,10 +586,13 @@ export {
duplicateProperty,
insertPropertyFromLibraryNode,
updateProperty,
dealDamage,
damagePropertiesByName,
damageProperty,
adjustQuantity,
selectAmmoItem,
pushToProperty,
pullFromProperty,
softRemoveProperty,
softRemoveProperty,
restoreProperty,
};

View File

@@ -40,7 +40,12 @@ let CreatureSettingsSchema = new SimpleSchema({
optional: true,
min: 0,
max: 1,
}
},
discordWebhook: {
type: String,
optional: true,
max: 200,
},
});
let CreatureSchema = new SimpleSchema({
@@ -91,6 +96,11 @@ let CreatureSchema = new SimpleSchema({
type: Number,
defaultValue: 0,
},
// Version of computation engine that was last used to compute this creature
computeVersion: {
type: String,
optional: true,
},
type: {
type: String,
defaultValue: 'pc',

View File

@@ -1,5 +1,11 @@
import spendResources from '/imports/api/creature/actions/spendResources.js'
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
export default function applyAction({prop}){
export default function applyAction({prop, creature}){
spendResources(prop);
insertCreatureLog.call({
log: {
text: prop.name,
creatureId: creature._id},
});
}

View File

@@ -0,0 +1,56 @@
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import { damagePropertiesByName } from '/imports/api/creature/CreatureProperties.js';
export default function applyAdjustment({
prop,
creature,
targets,
actionContext
}){
let damageTargets = prop.target === 'self' ? [creature] : targets;
let scope = {
...creature.variables,
...actionContext,
};
try {
var {result, errors} = evaluateString(prop.amount, scope, 'reduce');
if (typeof result !== 'number') {
return insertCreatureLog.call({ log: {
text: errors.join(', ') || 'Something went wrong',
creatureId: creature._id,
}});
}
} catch (e){
return insertCreatureLog.call({ log: {
text: e.toString(),
creatureId: creature._id,
}});
}
if (damageTargets) {
damageTargets.forEach(target => {
if (prop.target === 'each'){
result = evaluateString(prop.amount, scope, 'reduce');
}
damagePropertiesByName.call({
creatureId: target._id,
variableName: prop.stat,
operation: prop.operation || 'increment',
value: result
});
insertCreatureLog.call({
log: {
text: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''} ${-result}`,
creatureId: target._id,
}
});
});
} else {
insertCreatureLog.call({
log: {
text: `${prop.stat} ${prop.operation === 'set' ? 'set to' : ''} ${-result}`,
creatureId: creature._id,
}
});
}
}

View File

@@ -1,7 +1,5 @@
import math from '/imports/math.js';
//if (Meteor.isServer){
// var sendWebhook = require('/imports/server/discord/webhook.js').default;
//}
import roll from '/imports/parser/roll.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
export default function applyAttack({
prop,
@@ -10,12 +8,11 @@ export default function applyAttack({
//targets,
//actionContext
}){
let result = math.roll(1, 20) + prop.rollBonusResult;
if (Meteor.isClient){
console.log(`${creature.name} makes a ${prop.name} attack! Rolls ${result} to hit`);
}
//if (Meteor.isServer) sendWebhook({
// webhook: creature.webhook,
// message: `${creature.name} makes a ${prop.name} attack! Rolls ${result} to hit`,
//});
let result = roll(1, 20)[0] + prop.rollBonusResult;
insertCreatureLog.call({
log: {
text: `${prop.name} attack. ${result} to hit`,
creatureId: creature._id,
}
});
}

View File

@@ -1,27 +1,63 @@
import evaluateAndRollString from '/imports/api/creature/computation/afterComputation/evaluateAndRollString.js';
//if (Meteor.isServer){
// var sendWebhook = require('/imports/server/discord/webhook.js').default;
//}
import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import { dealDamage } from '/imports/api/creature/CreatureProperties.js';
export default function applyDamage({
prop,
creature,
//targets,
targets,
actionContext
}){
//let damageTargets = prop.target === 'self' ? [creature] : targets;
let damageTargets = prop.target === 'self' ? [creature] : targets;
let scope = {
...creature.variables,
...actionContext,
};
let {result, errors} = evaluateAndRollString(prop.amount, scope);
if (Meteor.isClient){
errors.forEach(e => console.error(e));
console.log(`${result} ${prop.damageType}${prop.damageType !== 'healing'? ' damage': ''}`);
try {
var {result, errors} = evaluateString(prop.amount, scope, 'reduce');
if (typeof result !== 'number') {
return insertCreatureLog.call({ log: {
text: errors.join(', '),
creatureId: creature._id,
}});
}
} catch (e){
return insertCreatureLog.call({ log: {
text: e.toString(),
creatureId: creature._id,
}});
}
if (damageTargets) {
damageTargets.forEach(target => {
if (prop.target === 'each'){
result = evaluateString(prop.amount, scope, 'reduce');
}
let damageDealt = dealDamage.call({
creatureId: target._id,
damageType: prop.damageType,
amount: result,
});
insertCreatureLog.call({
log: {
text: `Recieved ${damageDealt} ${prop.damageType}${prop.damageType !== 'healing'? ' damage': ''}`,
creatureId: target._id,
}
});
if (target._id !== creature._id){
insertCreatureLog.call({
log: {
text: `Dealt ${damageDealt} ${prop.damageType}${prop.damageType !== 'healing'? ' damage': ''}`,
creatureId: creature._id,
}
});
}
});
} else {
insertCreatureLog.call({
log: {
text: `${result} ${prop.damageType}${prop.damageType !== 'healing'? ' damage': ''}`,
creatureId: creature._id,
}
});
}
//if (Meteor.isServer) sendWebhook({
// webhook: creature.webhook,
// message: `${result} ${prop.damageType}${prop.damageType !== 'healing'? ' damage': ''}`,
//});
}

View File

@@ -1,4 +1,5 @@
import applyAction from '/imports/api/creature/actions/applyAction.js';
import applyAdjustment from '/imports/api/creature/actions/applyAdjustment.js';
import applyAttack from '/imports/api/creature/actions/applyAttack.js';
import applyDamage from '/imports/api/creature/actions/applyDamage.js';
import applyBuff from '/imports/api/creature/actions/applyBuff.js';
@@ -19,14 +20,14 @@ function applyProperty(options){
applyAction(options);
return true;
case 'attack':
applyAttack(options);
applyAction(options);
applyAttack(options);
return true;
case 'damage':
applyDamage(options);
return true;
case 'adjustment':
// applyAdjustment(options);
applyAdjustment(options);
return true;
case 'buff':
applyBuff(options);

View File

@@ -0,0 +1,56 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/Creatures.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import roll from '/imports/parser/roll.js';
const doCheck = new ValidatedMethod({
name: 'creature.doCheck',
validate: new SimpleSchema({
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
optional: true,
},
attributeName: {
type: String,
optional: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
run({creatureId, attributeName}) {
let creature = Creatures.findOne(creatureId);
assertEditPermission(creature, this.userId);
let bonus = getAttributeValue({creature, attributeName})
return doCheckWork({bonus});
},
});
function getAttributeValue({creature, attributeName}){
let att = creature.variables[attributeName];
if (!att) throw new Meteor.Error('No such attribute',
`This creature does not have a ${attributeName} property`);
let bonus = att.attributeType === 'ability'? att.modifier : att.value;
return bonus || 0;
}
export function doCheckWork({bonus, advantage = 0}){
let rolls = roll(2,20);
let chosenRoll;
if (advantage === 1){
chosenRoll = Math.max.apply(rolls);
} else if (advantage === -1){
chosenRoll = Math.min.apply(rolls);
} else {
chosenRoll = rolls[0];
}
let result = chosenRoll + bonus;
return {rolls, bonus, chosenRoll, result};
}
export default doCheck;

View File

@@ -4,10 +4,10 @@ export default class EffectAggregator{
constructor(stat, memo){
delete this.baseValueErrors;
if (stat.baseValueCalculation){
let {value, errors} = evaluateCalculation(stat.baseValueCalculation, memo);
this.statBaseValue = value;
if (errors.length){
this.baseValueErrors = errors;
let {result, context} = evaluateCalculation(stat.baseValueCalculation, memo);
this.statBaseValue = result.value;
if (context.errors.length){
this.baseValueErrors = context.errors;
}
this.base = this.statBaseValue;
} else {

View File

@@ -1,77 +0,0 @@
import math from '/imports/math.js';
import bareSymbolSubtitutor from '/imports/api/creature/computation/utility/bareSymbolSubtitutor.js';
import substituteRollsWithFunctions from '/imports/api/creature/computation/afterComputation/substituteRollsWithFunctions.js'
export default function evaluateAndRollString(string, scope){
let errors = [];
if (!string){
errors.push('No string provided');
return {result: string, errors};
}
if (!scope) errors.push('No scope provided');
// Parse the string using mathjs
let calc;
try {
calc = math.parse(string);
} catch (e) {
errors.push(e);
return {result: string, errors};
}
// Replace all bare symbols with symbol.value
let transformedCalc = calc.transform(bareSymbolSubtitutor(scope));
// Replace all rolls with the function to call them
transformedCalc = calc.transform(substituteRollsWithFunctions);
// Evaluate the expression to a number or return with substitutions
try {
let result = transformedCalc.evaluate(scope);
return {result, errors};
} catch (e1){
errors.push(e1);
try {
let result = simplifyWithAccessors(transformedCalc, scope).toHTML();
return {result, errors};
} catch (e2){
errors.push(e2);
return {result: transformedCalc.toHTML(), errors};
}
}
}
function simplifyWithAccessors(calc, scope){
let noAccessorCalc = calc.transform(substituteAccessors(scope));
return math.simplify(noAccessorCalc);
}
// returns a function to replace all accessors with either their resolved value
// or a symbol to simplify with
function substituteAccessors(scope){
return function(node){
if (node.isAccessorNode){
try {
return evaluateAccessor(node, scope);
} catch (e) {
return replaceAccessorWithSymbol(node);
}
} else {
return node;
}
}
}
// Throws error if symbol is undefined in scope
function evaluateAccessor(node, scope){
let value = node.evaluate(scope);
if (value === undefined){
throw 'Undefined symbol'
}
return new math.ConstantNode(value);
}
function replaceAccessorWithSymbol(node){
let symbolNode = new math.SymbolNode(node.toString());
return symbolNode;
}

View File

@@ -1,7 +1,7 @@
import math from '/imports/math.js';
import bareSymbolSubtitutor from '/imports/api/creature/computation/utility/bareSymbolSubtitutor.js';
import { parse, CompilationContext } from '/imports/parser/parser.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default function evaluateString(string, scope){
export default function evaluateString(string, scope, fn = 'compile'){
let errors = [];
if (!string){
errors.push('No string provided');
@@ -11,88 +11,19 @@ export default function evaluateString(string, scope){
if (!scope) errors.push('No scope provided');
// Parse the string using mathjs
let calc;
let node;
try {
calc = math.parse(string);
node = parse(string);
} catch (e) {
errors.push(e);
return {result: string, errors};
}
// Replace all bare symbols with symbol.value
let transformedCalc = calc.transform(bareSymbolSubtitutor(scope));
// Evaluate the expression to a number or return with substitutions
try {
let result = transformedCalc.evaluate(scope);
return {result, errors};
} catch (e1){
errors.push(e1);
try {
let result = simplifyWithAccessors(transformedCalc, scope).toHTML();
return {result, errors};
} catch (e2){
errors.push(e2);
return {result: transformedCalc.toHTML(), errors};
}
let context = new CompilationContext();
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};
}
}
function simplifyWithAccessors(calc, scope){
let noAccessorCalc = calc.transform(substituteAccessors(scope));
return math.simplify(noAccessorCalc);
}
// returns a function to replace all accessors with either their resolved value
// or a symbol to simplify with
function substituteAccessors(scope){
return function(node){
if (node.isAccessorNode){
try {
return evaluateAccessor(node, scope);
} catch (e) {
return replaceAccessorWithSymbol(node);
}
} else {
return node;
}
}
}
// Throws error if symbol is undefined in scope
function evaluateAccessor(node, scope){
let value = node.evaluate(scope);
if (value === undefined){
throw 'Undefined symbol'
}
return new math.ConstantNode(value);
}
function replaceAccessorWithSymbol(node){
let symbolNode = new math.SymbolNode(node.toString());
return symbolNode;
}
/*
function overrideSymbolNodeHTML(symbolNode){
let safeName = escape(symbolNode.name);
symbolNode.toHTML = function(){
console.log('running custom tohtml function')
return `<span class="math-symbol math-substitution-failed">${safeName}</span>`
}
return symbolNode;
}
// Escape special HTML characters
// Copied directly from math.js source to help with overriding toHTML
function escape (value) {
let text = String(value)
text = text.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
return text
}
*/

View File

@@ -1,28 +0,0 @@
import math from '/imports/math.js';
const diceRegex = /d\d+/;
export default function substituteRollsWithFunctions(node){
// TODO also replace dx as 1dx
if (
node.isOperatorNode &&
node.fn === 'multiply' &&
node.implicit &&
node.args[1].isSymbolNode &&
diceRegex.test(node.args[1].name)
){
let diceSize = node.args[1].name.slice(1);
let diceSizeNode = new math.ConstantNode(diceSize);
return new math.FunctionNode('roll', [node.args[0], diceSizeNode]);
} else if (
node.isSymbolNode &&
diceRegex.test(node.name)
) {
let diceSize = node.name.slice(1);
let diceSizeNode = new math.ConstantNode(diceSize);
let diceNumberNode = new math.ConstantNode(1);
return new math.FunctionNode('roll', [diceNumberNode, diceSizeNode]);
} else {
return node;
}
}

View File

@@ -33,15 +33,15 @@ function combineAttribute(stat, aggregator, memo){
stat.value = getAggregatorResult(stat, aggregator);
stat.baseValue = aggregator.statBaseValue;
stat.baseValueErrors = aggregator.baseValueErrors;
if (stat.attributeType === 'ability') {
stat.modifier = Math.floor((stat.value - 10) / 2);
}
if (stat.attributeType === 'spellSlot'){
let {value, errors} = evaluateCalculation(stat.spellSlotLevelCalculation, memo);
stat.spellSlotLevelValue = value,
stat.spellSlotLevelErrors = errors;
let {result, context} = evaluateCalculation(stat.spellSlotLevelCalculation, memo);
stat.spellSlotLevelValue = result.value;
stat.spellSlotLevelErrors = context.errors;
}
stat.currentValue = stat.value - (stat.damage || 0);
if (stat.attributeType === 'ability') {
stat.modifier = Math.floor((stat.currentValue - 10) / 2);
}
stat.hide = aggregator.hasNoEffects &&
stat.baseValue === undefined ||
undefined

View File

@@ -34,10 +34,10 @@ export default function computeEffect(effect, memo){
} else if(_.contains(['advantage', 'disadvantage', 'fail'], effect.operation)){
effect.result = 1;
} else {
let {value, errors} = evaluateCalculation(effect.calculation, memo);
effect.result = value;
if (errors.length){
effect.errors = errors;
let {result, context} = evaluateCalculation(effect.calculation, memo);
effect.result = result.value;
if (context.errors.length){
effect.errors = context.errors;
}
}
effect.computationDetails.computed = true;

View File

@@ -23,10 +23,10 @@ export default function computeEndStepProperty(prop, memo){
function computeAction(prop, memo){
// Uses
let {value, errors} = evaluateCalculation(prop.uses, memo);
prop.usesResult = value;
if (errors.length){
prop.usesErrors = errors;
let {result, context} = evaluateCalculation(prop.uses, memo);
prop.usesResult = result.value;
if (context.errors.length){
prop.usesErrors = context.errors;
} else {
delete prop.usesErrors;
}
@@ -34,6 +34,7 @@ function computeAction(prop, memo){
if (prop.usesUsed >= prop.usesResult){
prop.insufficientResources = true;
}
if (!prop.resources) return;
// Attributes consumed
prop.resources.attributesConsumed.forEach((attConsumed, i) => {
if (attConsumed.variableName){
@@ -66,43 +67,28 @@ function computeAction(prop, memo){
});
}
function computeAttack(prop, memo){
// Roll bonus
let {value, errors} = evaluateCalculation(prop.rollBonus, memo);
prop.rollBonusResult = value;
if (errors.length){
prop.rollBonusErrors = errors;
function computePropertyField(prop, memo, fieldName, fn){
let {result, context} = evaluateCalculation(prop[fieldName], memo, fn);
prop[`${fieldName}Result`] = result.value;
if (context.errors.length){
prop[`${fieldName}Errors`] = context.errors;
} else {
delete prop.rollBonusErrors;
delete prop[`${fieldName}Errors`];
}
}
function computeAttack(prop, memo){
computePropertyField(prop, memo, 'rollBonus');
}
function computeSavingThrow(prop, memo){
let {value, errors} = evaluateCalculation(prop.dc, memo);
prop.dcResult = value;
if (errors.length){
prop.dcErrors = errors;
} else {
delete prop.dcErrors;
}
computePropertyField(prop, memo, 'dc');
}
function computeSpellList(prop, memo){
let {value, errors} = evaluateCalculation(prop.maxPrepared, memo);
prop.maxPreparedResult = value;
if (errors.length){
prop.maxPreparedErrors = errors;
} else {
delete prop.maxPreparedErrors;
}
computePropertyField(prop, memo, 'maxPrepared');
}
function computeSlot(prop, memo){
let {value, errors} = evaluateCalculation(prop.slotCondition, memo);
prop.slotConditionResult = value;
if (errors.length){
prop.slotConditionErrors = errors;
} else {
delete prop.slotConditionErrors;
}
computePropertyField(prop, memo, 'slotCondition');
}

View File

@@ -26,10 +26,10 @@ export default function computeToggle(toggle, memo){
} else if (Number.isFinite(+toggle.condition)){
toggle.toggleResult = !!+toggle.condition;
} else {
let {value, errors} = evaluateCalculation(toggle.condition, memo);
toggle.toggleResult = value;
if (errors.length){
toggle.errors = errors;
let {result, context} = evaluateCalculation(toggle.condition, memo);
toggle.toggleResult = !!result.value;
if (context.errors.length){
toggle.errors = context.errors;
}
}
toggle.computationDetails.computed = true;

View File

@@ -1,98 +1,41 @@
import computeStat from '/imports/api/creature/computation/computeStat.js';
import math from '/imports/math.js';
import { parse, CompilationContext } from '/imports/parser/parser.js';
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
import AccessorNode from '/imports/parser/parseTree/AccessorNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
/* Convert a calculation into a constant output and errors*/
export default function evaluateCalculation(string, memo){
if (!string) return {errors: [], value: string};
export default function evaluateCalculation(string, memo, fn = 'reduce'){
if (!string) return {
context: {errors: []},
result: new ConstantNode({value: string, type: 'string'}),
};
let errors = [];
// Parse the string using mathjs
// Parse the string
let calc;
try {
calc = math.parse(string);
calc = parse(string);
} catch (e) {
errors.push({
type: 'parsing',
message: e.message || e
});
return {errors, value: string};
return {
context: {errors},
result: new ConstantNode({value: string, type: 'string'}),
};
}
// Ensure all symbol nodes are defined and coputed
calc.traverse(node => {
if (node.isSymbolNode){
if (node instanceof SymbolNode || node instanceof AccessorNode){
let stat = memo.statsByVariableName[node.name];
if (stat && !stat.computationDetails.computed){
computeStat(stat, memo);
}
}
});
// Replace all symbols with their subtitution
let substitutedCalc = calc.transform(
symbolSubtitutor(memo.statsByVariableName, errors)
);
// Evaluate the expression to a number or return with substitutions
try {
let value = substitutedCalc.evaluate(memo.statsByVariableName);
if (typeof value === 'object') value = value.toString();
return {errors, value};
} catch (e){
errors.push({
type: 'evaluation',
message: e.message || e
});
let value = substitutedCalc.toString();
return {errors, value};
}
}
// returns a function to replace all symbols with either their resolved value
// or zero, keeping the errors
function symbolSubtitutor(scope, errors){
return function(node){
// mark symbol nodes that are children of function nodes to be skipped
if (node.isFunctionNode){
let fn = node.fn;
if (fn && fn.isSymbolNode){
fn.skipReplacement = true;
}
return node;
} else if (node.isSymbolNode && node.skipReplacement !== true){
//bare symbols of name "stat", should search for stat.value
let stat = scope[node.name];
if (stat){
if (stat.value === undefined){
errors.push({
type: 'subsitution',
message: `${node.name} does not have a value, set to 0`
});
return new math.ConstantNode(0);
} else {
return new math.ConstantNode(stat.value);
}
} else {
try {
return new math.ConstantNode(node.evaluate(scope));
} catch (e) {
errors.push({
type: 'subsitution',
message: `${node.name} not found, set to 0`
});
return new math.ConstantNode(0);
}
}
} else if (node.isAccessorNode && node.object.isSymbolNode){
try {
let value = node.evaluate(scope);
if (value === undefined) throw 'Not found';
return new math.ConstantNode(value);
} catch (e) {
errors.push({
type: 'subsitution',
message: `${node.toString()} not found, set to 0`
});
return new math.ConstantNode(0);
}
} else {
return node;
}
}
// Evaluate
let context = new CompilationContext();
let result = calc[fn](memo.statsByVariableName, context);
return {result, context};
}

View File

@@ -3,11 +3,13 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import SimpleSchema from 'simpl-schema';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import ComputationMemo from '/imports/api/creature/computation/ComputationMemo.js';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
import computeMemo from '/imports/api/creature/computation/computeMemo.js';
import getActiveProperties from '/imports/api/creature/getActiveProperties.js';
import writeAlteredProperties from '/imports/api/creature/computation/writeAlteredProperties.js';
import writeCreatureVariables from '/imports/api/creature/computation/writeCreatureVariables.js';
import { recomputeDamageMultipliersById } from '/imports/api/creature/damageMultiplierDenormalise/recomputeDamageMultipliers.js';
import { recomputeDamageMultipliersById } from '/imports/api/creature/denormalise/recomputeDamageMultipliers.js';
import recomputeInactiveProperties from '/imports/api/creature/denormalise/recomputeInactiveProperties.js';
import recomputeSlotFullness from '/imports/api/creature/denormalise/recomputeSlotFullness.js';
import Creatures from '/imports/api/creature/Creatures.js';
export const recomputeCreature = new ValidatedMethod({
@@ -95,16 +97,42 @@ export function recomputeCreatureById(creatureId){
*/
export function recomputeCreatureByDoc(creature){
const creatureId = creature._id;
let props = getActiveProperties({
ancestorId: creatureId,
filter: {type: {$in: calculationPropertyTypes}},
includeUntoggled: true,
// TODO filter out expensive fields, particularly icon field
});
// find 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 active properties
let props = CreatureProperties.find({
'ancestors.id': creatureId,
removed: {$ne: true},
type: {$in: calculationPropertyTypes},
$or: [
{inactive: {$ne: true}},
// But also the inactive computed toggles and their decendants
{'ancestors.id': {$in: toggleIds}},
{_id: {$in: toggleIds}},
]
}, {
fields: { // Filter out potentially large fields
icon: 0,
summary: 0,
description: 0,
},
sort: {
order: 1,
}
}).fetch();
let computationMemo = new ComputationMemo(props, creature);
recomputeInactiveProperties(creatureId);
computeMemo(computationMemo);
writeAlteredProperties(computationMemo);
writeCreatureVariables(computationMemo, creatureId);
recomputeDamageMultipliersById(creatureId);
recomputeSlotFullness(creatureId);
return computationMemo;
}

View File

@@ -1,24 +0,0 @@
import math from '/imports/math.js';
export default function bareSymbolSubtitutor(scope){
return function(node, path){
if (!scope) return node;
if (node.isFunctionNode){
let fn = node.fn;
if (fn && fn.isSymbolNode){
fn.skipReplacement = true;
}
return node;
} else if (
node.isSymbolNode &&
path !== 'object' &&
node.skipReplacement !== true
) {
let stat = scope[node.name];
if (!stat) return node;
return new math.ConstantNode(stat.value);
} else {
return node;
}
}
}

View File

@@ -1,5 +1,6 @@
import { pick, forOwn } from 'lodash';
import Creatures from '/imports/api/creature/Creatures.js';
import VERSION from '/imports/constants/VERSION.js';
export default function writeCreatureVariables(memo, creatureId) {
const fields = [
@@ -31,5 +32,8 @@ export default function writeCreatureVariables(memo, creatureId) {
let condensedStat = pick(stat, fields);
memo.creatureVariables[variableName] = condensedStat;
});
Creatures.update(creatureId, {$set: {variables: memo.creatureVariables}});
Creatures.update(creatureId, {$set: {
variables: memo.creatureVariables,
computeVersion: VERSION,
}});
}

View File

@@ -24,6 +24,6 @@ export function assertEditPermission(creature, userId) {
}
export function assertViewPermission(creature, userId) {
creature = getCreature(creature, {owner: 1, writers: 1, public: 1});
creature = getCreature(creature, {owner: 1, readers:1, writers: 1, public: 1});
viewPermission(creature, userId);
}

View File

@@ -0,0 +1,7 @@
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import getClosestPropertyAncestorCreature from '/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreature.js';
export default function assertPropertyEditPermission(prop, userId){
let creature = getClosestPropertyAncestorCreature(prop);
assertEditPermission(creature, userId);
}

View File

@@ -0,0 +1,7 @@
import Creatures from '/imports/api/creature/Creatures.js';
import getClosestPropertyAncestorCreatureId from '/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreatureId.js';
export default function getClosestPropertyAncestorCreature(prop){
let creatureId = getClosestPropertyAncestorCreatureId(prop);
return Creatures.findOne(creatureId);
}

View File

@@ -0,0 +1,13 @@
export default function getClosestPropertyAncestorCreatureId(prop){
if (!prop.ancestors) throw 'Property has no ancestors';
let creatureId;
// Find the last ancestor in the creature collection
for (let i = prop.ancestors.length - 1; i >= 0; i--){
if (prop.ancestors[i].collection === 'creatures'){
creatureId = prop.ancestors[i].id;
break;
}
}
if (!creatureId) throw 'This property has no creature ancestors';
return creatureId;
}

View File

@@ -0,0 +1,64 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { assertEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
import getClosestPropertyAncestorCreature from '/imports/api/creature/creatureProperties/getClosestPropertyAncestorCreature.js';
import INVENTORY_TAGS from '/imports/constants/INVENTORY_TAGS.js';
function getParentRefByTag(creatureId, tag){
let prop = CreatureProperties.findOne({
'ancestors.id': creatureId,
removed: {$ne: true},
inactive: {$ne: true},
tags: tag,
}, {
sort: {order: 1},
});
if (prop){
return {id: prop._id, collection: 'creatureProperties'};
} else {
return {id: creatureId, collection: 'creatures'};
}
}
// Equipping or unequipping an item will also change its parent
const equipItem = new ValidatedMethod({
name: 'creatureProperties.equip',
validate({_id, equipped}){
if (!_id) throw new Meteor.Error('No _id', '_id is required');
if (equipped !== true && equipped !== false) {
throw new Meteor.Error('No equipped', 'equipped is required to be true or false');
}
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({_id, equipped}) {
let item = CreatureProperties.findOne(_id);
if (item.type !== 'item') throw new Meteor.Error('wrong type',
'Equip and unequip can only be performed on items');
let creature = getClosestPropertyAncestorCreature(item);
assertEditPermission(creature, this.userId);
CreatureProperties.update(_id, {
$set: {equipped},
}, {
selector: {type: 'item'},
});
let tag = equipped ? INVENTORY_TAGS.equipment : INVENTORY_TAGS.carried;
let parentRef = getParentRefByTag(creature._id, tag);
// organizeDoc handles recompuation
organizeDoc.call({
docRef: {
id: _id,
collection: 'creatureProperties',
},
parentRef,
order: Number.MAX_SAFE_INTEGER,
});
},
});
export { equipItem, getParentRefByTag }

View File

@@ -0,0 +1,59 @@
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
export default function recomputeInactiveProperties(ancestorId){
let disabledFilter = {
'ancestors.id': ancestorId,
$or: [
{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}},
],
};
let disabledIds = CreatureProperties.find(disabledFilter, {
fields: {_id: 1},
}).map(prop => prop._id);
// Deactivate relevant properties
// Inactive properties
CreatureProperties.update({
'ancestors.id': ancestorId,
'_id': {$in: disabledIds},
$or: [{inactive: {$ne: true}}, {deactivatedByAncestor: true}],
}, {
$set: {inactive: true},
$unset: {deactivatedByAncestor: 1},
}, {
multi: true,
selector: {type: 'any'},
});
// Decendants of inactive properties
CreatureProperties.update({
'ancestors.id': {$eq: ancestorId, $in: disabledIds},
$or: [{inactive: {$ne: true}}, {deactivatedByAncestor: {$ne: true}}],
}, {
$set: {
inactive: true,
deactivatedByAncestor: true,
},
}, {
multi: true,
selector: {type: 'any'},
});
// Remove inactive from all the properties that are inactive but shouldn't be
CreatureProperties.update({
'ancestors.id': {$eq: ancestorId, $nin: disabledIds},
'_id': {$nin: disabledIds},
$or: [{inactive: true}, {deactivatedByAncestor: true}],
}, {
$unset: {
inactive: 1,
deactivatedByAncestor: 1,
},
}, {
multi: true,
selector: {type: 'any'},
});
}

View File

@@ -0,0 +1,39 @@
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
// n + 1 database queries + n potential updates for n slots. Could be sped up.
export default function recomputeSlotFullness(ancestorId){
CreatureProperties.find({
'ancestors.id': ancestorId,
type: 'propertySlot',
}).forEach(slot => {
let children = CreatureProperties.find({
'parent.id': slot._id,
removed: {$ne: true},
}, {
fields: {
slotQuantityFilled: 1,
type: 1
}
}).fetch();
let totalFilled = 0;
children.forEach(child => {
if (child.type === 'slotFiller'){
totalFilled += child.slotQuantityFilled;
} else {
totalFilled++;
}
});
let spaceLeft;
if (slot.quantityExpected === 0){
spaceLeft = null;
} else {
spaceLeft = slot.quantityExpected - totalFilled;
}
if (slot.totalFilled !== totalFilled || slot.spaceLeft !== spaceLeft){
CreatureProperties.update(slot._id, {
$set: {totalFilled, spaceLeft},
}, {
selector: {type: 'propertySlot'}
});
}
});
}

View File

@@ -0,0 +1,176 @@
import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/Creatures.js';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import {assertEditPermission} from '/imports/api/creature/creaturePermissions.js';
import { parse, CompilationContext } from '/imports/parser/parser.js';
const PER_CREATURE_LOG_LIMIT = 100;
if (Meteor.isServer){
var sendWebhookAsCreature = require('/imports/server/discord/sendWebhook.js').sendWebhookAsCreature;
}
let CreatureLogs = new Mongo.Collection('creatureLogs');
let CreatureLogSchema = new SimpleSchema({
text: {
type: String,
},
type: {
type: String,
allowedValues: ['roll', 'change', 'damage', 'info'],
defaultValue: 'info',
},
// The real-world date that it occured, usually sorted by date
date: {
type: Date,
autoValue: function() {
// If the date isn't set, set it to now
if (!this.isSet) {
return new Date();
}
},
index: 1,
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
index: 1,
},
});
CreatureLogs.attachSchema(CreatureLogSchema);
function removeOldLogs(creatureId){
// Find the first log that is over the limit
let firstExpiredLog = CreatureLogs.find({
creatureId
}, {
sort: {date: -1},
skip: PER_CREATURE_LOG_LIMIT,
});
// Remove all logs older than the one over the limit
CreatureLogs.remove({
creatureId,
date: {$lte: firstExpiredLog.date},
});
}
function logWebhook({log, creature}){
if (Meteor.isServer){
sendWebhookAsCreature({
creature,
content: log.text,
});
}
}
const insertCreatureLog = new ValidatedMethod({
name: 'creatureLogs.methods.insertCreatureLog',
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
validate: new SimpleSchema({
log: CreatureLogSchema.omit('type', 'date'),
}).validator(),
run({log}){
const creatureId = log.creatureId;
const creature = Creatures.findOne(creatureId, {fields: {
readers: 1,
writers: 1,
owner: 1,
'settings.discordWebhook': 1,
name: 1,
avatarPicture: 1,
}});
assertEditPermission(creature, this.userId);
// Build the new log
if (typeof log === 'string'){
log = {text: log};
}
log.date = new Date();
// Insert it
let id = CreatureLogs.insert(log);
if (Meteor.isServer){
this.unblock();
removeOldLogs(creatureId);
logWebhook({log, creature});
}
return id;
},
});
function equalIgnoringWhitespace(a, b){
if (typeof a !== 'string' || typeof b !== 'string') return a === b;
return a.replace(/\s/g,'') === b.replace(/\s/g, '');
}
const logRoll = new ValidatedMethod({
name: 'creatureLogs.methods.logForCreature',
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
validate: new SimpleSchema({
roll: {
type: String,
},
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
}).validator(),
run({roll, creatureId}){
const creature = Creatures.findOne(creatureId, {fields: {
variables: 1,
readers: 1,
writers: 1,
owner: 1,
'settings.discordWebhook': 1,
name: 1,
avatarPicture: 1,
}});
assertEditPermission(creature, this.userId);
let parsedResult = parse(roll);
let logText;
if (parsedResult === null) {
logText = 'Unexpected end of input';
}
else try {
logText = [];
let rollContext = new CompilationContext();
let compiled = parsedResult.compile(creature.variables, rollContext);
let compiledString = compiled.toString();
if (!equalIgnoringWhitespace(compiledString, roll)) logText.push(roll);
logText.push(compiledString);
let rolled = compiled.roll(creature.variables, rollContext);
let rolledString = rolled.toString();
if (rolledString !== compiledString) logText.push(rolled.toString());
let result = rolled.reduce(creature.variables, rollContext);
let resultString = result.toString();
if (resultString !== rolledString) logText.push(resultString);
logText = logText.join('\n\n');
} catch (e){
logText = 'Calculation error';
}
const log = {
text: logText,
creatureId,
date: new Date(),
};
let id = CreatureLogs.insert(log);
if (Meteor.isServer){
this.unblock();
removeOldLogs(creatureId);
logWebhook({log, creature});
}
return id;
},
});
export default CreatureLogs;
export { CreatureLogSchema, insertCreatureLog, logRoll};

View File

@@ -1,13 +1,15 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import Creatures from '/imports/api/creature/Creatures.js';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js'
import { assertOwnership } from '/imports/api/creature/creaturePermissions.js';
import Creatures from '/imports/api/creature/Creatures.js';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
function removeRelatedDocuments(creatureId){
CreatureProperties.remove({'ancestors.id': creatureId});
CreatureLogs.remove({creatureId});
Experiences.remove({creatureId});
}

View File

@@ -41,16 +41,21 @@ const restoreError = function(){
};
export function restore({_id, collection}){
collection = getCollectionByName(collection);
if (typeof collection === 'string') {
collection = getCollectionByName(collection);
}
let numUpdated = collection.update({
_id,
removedWith: {$exists: false}
}, { $unset: {
removed: 1,
removedAt: 1,
}});
}}, {
selector: {type: 'any'},
},);
if (numUpdated === 0) restoreError();
updateDescendants({
collection,
ancestorId: _id,
filter: {
removedWith: _id,

View File

@@ -23,6 +23,11 @@ const AdjustmentSchema = new SimpleSchema({
type: String,
optional: true,
},
operation: {
type: String,
allowedValues: ['set', 'increment'],
defaultValue: 'increment',
},
});
export { AdjustmentSchema };

View File

@@ -6,6 +6,10 @@ let ClassLevelSchema = new SimpleSchema({
type: String,
optional: true,
},
description: {
type: String,
optional: true,
},
// The name of this class level's variable
variableName: {
type: String,
@@ -23,6 +27,11 @@ let ClassLevelSchema = new SimpleSchema({
'nextLevelTags.$': {
type: String,
},
// Same as in SlotFillers.js
slotFillerCondition: {
type: String,
optional: true,
},
});
export { ClassLevelSchema };

View File

@@ -0,0 +1,38 @@
// SlotFiller fillers specifically fill a slot with a bit more control than
// other properties
import SimpleSchema from 'simpl-schema';
let SlotFillerSchema = new SimpleSchema({
name: {
type: String,
optional: true,
},
picture: {
type: String,
optional: true,
},
description: {
type: String,
optional: true,
},
// Overrides the type when searching for properties
slotFillerType: {
type: String,
optional: true,
},
// Fill more than one quantity in a slot, like feats and ability score
// improvements, filtered out of UI if there isn't space in quantityExpected
slotQuantityFilled: {
type: SimpleSchema.Integer,
defaultValue: 1,
min: 0,
},
// Filters out of UI if condition isn't met, but isn't otherwise enforced
slotFillerCondition: {
type: String,
optional: true,
},
});
export { SlotFillerSchema };

View File

@@ -24,6 +24,7 @@ let SlotSchema = new SimpleSchema({
quantityExpected: {
type: SimpleSchema.Integer,
defaultValue: 1,
min: 0,
},
ignored: {
type: Boolean,
@@ -33,11 +34,10 @@ let SlotSchema = new SimpleSchema({
type: String,
optional: true,
},
// How many properties have been selected to fill this slot
quantityFilled: {
type: SimpleSchema.Integer,
defaultValue: 0,
},
hideWhenFull: {
type: Boolean,
optional: true,
}
});
const ComputedOnlySlotSchema = new SimpleSchema({
@@ -54,6 +54,14 @@ const ComputedOnlySlotSchema = new SimpleSchema({
'slotConditionErrors.$':{
type: ErrorSchema,
},
totalFilled: {
type: SimpleSchema.Integer,
defaultValue: 0,
},
spaceLeft: {
type: SimpleSchema.Integer,
optional: true,
},
});
const ComputedSlotSchema = new SimpleSchema()

View File

@@ -25,7 +25,7 @@ const ToggleSchema = new SimpleSchema({
const ComputedOnlyToggleSchema = new SimpleSchema({
// The computed result of the effect
toggleResult: {
type: SimpleSchema.oneOf(Number, String, Boolean),
type: Boolean,
optional: true,
},
// The errors encountered while computing the result

View File

@@ -18,6 +18,7 @@ import { RollSchema } from '/imports/api/properties/Rolls.js';
import { ComputedSavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { ComputedSkillSchema } from '/imports/api/properties/Skills.js';
import { ComputedSlotSchema } from '/imports/api/properties/Slots.js';
import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js';
import { ComputedSpellSchema } from '/imports/api/properties/Spells.js';
import { ComputedSpellListSchema } from '/imports/api/properties/SpellLists.js';
import { ToggleSchema } from '/imports/api/properties/Toggles.js';
@@ -36,10 +37,11 @@ const propertySchemasIndex = {
folder: FolderSchema,
note: NoteSchema,
proficiency: ProficiencySchema,
propertySlot: ComputedSlotSchema,
roll: RollSchema,
savingThrow: ComputedSavingThrowSchema,
skill: ComputedSkillSchema,
propertySlot: ComputedSlotSchema,
slotFiller: SlotFillerSchema,
spellList: ComputedSpellListSchema,
spell: ComputedSpellSchema,
toggle: ToggleSchema,

View File

@@ -16,6 +16,7 @@ import { RollSchema } from '/imports/api/properties/Rolls.js';
import { SavingThrowSchema } from '/imports/api/properties/SavingThrows.js';
import { SkillSchema } from '/imports/api/properties/Skills.js';
import { SlotSchema } from '/imports/api/properties/Slots.js';
import { SlotFillerSchema } from '/imports/api/properties/SlotFillers.js';
import { SpellListSchema } from '/imports/api/properties/SpellLists.js';
import { SpellSchema } from '/imports/api/properties/Spells.js';
import { ToggleSchema } from '/imports/api/properties/Toggles.js';
@@ -36,10 +37,11 @@ const propertySchemasIndex = {
folder: FolderSchema,
note: NoteSchema,
proficiency: ProficiencySchema,
propertySlot: SlotSchema,
roll: RollSchema,
savingThrow: SavingThrowSchema,
skill: SkillSchema,
propertySlot: SlotSchema,
slotFiller: SlotFillerSchema,
spellList: SpellListSchema,
spell: SpellSchema,
toggle: ToggleSchema,

View File

@@ -88,18 +88,18 @@ export function assertDocEditPermission(doc, userId){
}
export function assertViewPermission(doc, userId) {
assertIdValid(userId);
assertdocExists(doc);
if (doc.public) return true;
assertIdValid(userId);
if (
doc.owner === userId ||
doc.public ||
_.contains(doc.readers, userId) ||
_.contains(doc.writers, userId)
){
return true;
} else {
throw new Meteor.Error('View permission denied',
'You do not have permission to view this character');
'You do not have permission to view this document');
}
}

View File

@@ -1,5 +1,4 @@
import request from 'request';
if (!Meteor.isServer) throw 'Server only, do not import this code in the client';
const config = ServiceConfiguration.configurations.findOne({service: 'patreon'});
@@ -62,7 +61,7 @@ const updateIdentity = Meteor.wrapAsync(function(accessToken, userId, callback){
}
try {
let identity = JSON.parse(body);
let membership = identity.included[0];
let membership = identity.included && identity.included[0];
let entitledAmount = membership && membership.attributes
.currently_entitled_amount_cents || 0;
writeEntitledCents(userId, entitledAmount);
@@ -85,7 +84,7 @@ const updatePatreonDetails = function(user){
throw new Meteor.Error('no-patreon-access', 'Patreon access token not found for this user');
}
let accessToken = user.services.patreon.accessToken;
if (user.services.patreon.tokenExpiryDate < new Date()){
if (user.services.patreon.expiresAt < new Date()){
// Token expired, refresh it before continuing
accessToken = refreshAccessToken(user.services.patreon.refreshToken, user._id);
}
@@ -96,7 +95,7 @@ Meteor.methods({
updateMyPatreonDetails(){
const userId = this.userId;
if (!userId) throw new Meteor.Error('not-logged-in', 'You must be logged in to update Patreon details');
const user = Meteor.users.findOne(userId, {fields: {patreon: 1}});
const user = Meteor.users.findOne(userId, {fields: {services: 1}});
updatePatreonDetails(user);
},
});
@@ -113,9 +112,9 @@ const writePatreonToken = function(userId, {
// Write
Meteor.users.update(userId, {
$set: {
'patreon.accessToken': access_token,
'patreon.refreshToken': refresh_token,
'patreon.tokenExpiryDate': expiryDate,
'services.patreon.accessToken': access_token,
'services.patreon.refreshToken': refresh_token,
'services.patreon.expiresAt': expiryDate,
},
$unset: {
'patreon.error': 1,
@@ -127,6 +126,7 @@ const writeEntitledCents = function(userId, amount){
Meteor.users.update(userId, {
$set: {
'services.patreon.entitledCents': amount,
'services.patreon.lastUpdatedIdentity': new Date(),
},
$unset: {
'patreon.error': 1,
@@ -134,4 +134,4 @@ const writeEntitledCents = function(userId, amount){
});
};
export { updatePatreonDetails };
export default updatePatreonDetails;

View File

@@ -0,0 +1,12 @@
import updatePatreonDetails from '/imports/api/users/patreon/updatePatreonDetails.js';
const ONE_DAY = 24 * 60 * 60 * 1000;
Accounts.onLogin(({user}) => {
let patreon = user.services && user.services.patreon;
if (patreon){
const timeSinceIdentityUpdate = new Date() - patreon.lastUpdatedIdentity;
if (timeSinceIdentityUpdate > ONE_DAY){
updatePatreonDetails(user);
}
}
});

View File

@@ -0,0 +1,7 @@
const INVENTORY_TAGS = Object.freeze({
inventory: 'inventory',
equipment: 'equipment',
carried: 'carried',
});
export default INVENTORY_TAGS;

View File

@@ -75,6 +75,10 @@ const PROPERTIES = Object.freeze({
icon: 'tab_unselected',
name: 'Slot'
},
slotFiller: {
icon: 'picture_in_picture',
name: 'Slot filler'
},
spellList: {
icon: '$vuetify.icons.spell_list',
name: 'Spell list'

View File

@@ -0,0 +1,16 @@
const VERSION = Meteor.isClient ?
'CLIENT' :
process.env.CONTAINER_VERSION || getVersionFromGit();
export default VERSION;
function getVersionFromGit(){
try {
return require('child_process')
.execSync('git rev-parse --short HEAD')
.toString().trim();
} catch (e){
return 'GIT_VERSION_FAIL'
}
}

View File

@@ -1,7 +0,0 @@
// All of the compile functions are provided for use in compiling parse trees
// Every compile function takes in ParseNodes as arguements and returns a single
// ConstantNode as a result
const compileFunctions = {};
export compileFunctions;

View File

@@ -1,15 +0,0 @@
export default function sum(inputNode) {
let node = inputNode.roll();
if (node.type === 'numberArray'){
let total = node.value.reduce((total, num) => total + num, 0);
return new ConstantNode({type: 'number', value: total});
} else {
let errors = node.errors || [];
errors.push(`Could not sum ${node.value}`);
return new ConstantNode({
type: 'uncompiledNode',
value: node.value,
errors,
});
}
}

View File

@@ -0,0 +1,94 @@
export default {
'abs': {
comment: 'Returns the absolute value of a number',
examples: [
{input: 'abs(9)', result: '9'},
{input: 'abs(-3)', result: '3'},
],
argumentType: 'number',
resultType: 'number',
fn: Math.abs,
},
'sqrt': {
comment: 'Returns the square root of a number',
examples: [
{input: 'sqrt(16)', result: '4'},
{input: 'sqrt(10)', result: '3.1622776601683795'},
],
argumentType: 'number',
resultType: 'number',
fn: Math.sqrt,
},
'max': {
comment: 'Returns the largest of the given numbers',
examples: [{input: 'min(12, 6, 3, 168)', result: '168'}],
argumentType: 'number',
resultType: 'number',
fn: Math.max,
},
'min': {
comment: 'Returns the smallest of the given numbers',
examples: [{input: 'min(12, 6, 3, 168)', result: '3'}],
argumentType: 'number',
resultType: 'number',
fn: Math.min,
},
'round': {
comment: 'Returns the value of a number rounded to the nearest integer',
examples: [
{input: 'round(5.95)', result: '6'},
{input: 'round(5.5)', result: '6'},
{input: 'round(5.05)', result: '5'},
],
argumentType: 'number',
resultType: 'number',
fn: Math.round,
},
'floor': {
comment: 'Rounds a number down to the next smallest integer',
examples: [
{input: 'floor(5.95)', result: '5'},
{input: 'floor(5.05)', result: '5'},
{input: 'floor(5)', result: '5'},
{input: 'floor(-5.5)', result: '-6'},
],
argumentType: 'number',
resultType: 'number',
fn: Math.floor,
},
'ceil': {
comment: 'Rounds a number up to the next largest integer',
examples: [
{input: 'ceil(5.95)', result: '6'},
{input: 'ceil(5.05)', result: '6'},
{input: 'ceil(5)', result: '5'},
{input: 'ceil(-5.5)', result: '-5'},
],
argumentType: 'number',
resultType: 'number',
fn: Math.ceil,
},
'trunc': {
comment: 'Returns the integer part of a number by removing any fractional digits',
examples: [
{input: 'trunc(5.95)', result: '5'},
{input: 'trunc(5.05)', result: '5'},
{input: 'trunc(5)', result: '5'},
{input: 'trunc(-5.5)', result: '-5'},
],
argumentType: 'number',
resultType: 'number',
fn: Math.trunc,
},
'sign': {
comment: 'Returns either a positive or negative 1, indicating the sign of a number, or zero',
examples: [
{input: 'sign(-3)', result: '-1'},
{input: 'sign(3)', result: '1'},
{input: 'sign(0)', result: '0'},
],
argumentType: 'number',
resultType: 'number',
fn: Math.sign,
}
}

View File

@@ -2,11 +2,19 @@
// http://github.com/Hardmath123/nearley
function id(x) { return x[0]; }
import AccessorNode from '/imports/parser/parseTree/AccessorNode.js';
import ArrayNode from '/imports/parser/parseTree/ArrayNode.js';
import CallNode from '/imports/parser/parseTree/CallNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
import IfNode from '/imports/parser/parseTree/IfNode.js';
import IfNode from '/imports/parser/parseTree/IfNode.js';
import IndexNode from '/imports/parser/parseTree/IndexNode.js';
import OperatorNode from '/imports/parser/parseTree/OperatorNode.js';
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
import ParenthesisNode from '/imports/parser/parseTree/ParenthesisNode.js';
import RollNode from '/imports/parser/parseTree/RollNode.js';
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
import UnaryOperatorNode from '/imports/parser/parseTree/UnaryOperatorNode.js';
import NotOperatorNode from '/imports/parser/parseTree/NotOperatorNode.js';
import moo from 'moo';
const lexer = moo.compile({
@@ -16,26 +24,30 @@ function id(x) { return x[0]; }
value: s => s.slice(1, -1),
},
name: {
match: /[a-zA-Z]+\w*?/,
match: /[a-zA-Z_]*[a-ce-zA-Z_][a-zA-Z0-9_]*/,
type: moo.keywords({
'keywords': ['if', 'else', 'd'],
'keywords': ['true', 'false'],
}),
},
space: {
match: /\s+/,
lineBreaks: true,
},
separators: [',', '.'],
diceOperator: ['d'],
separator: [',', ';'],
period: ['.'],
ifOperator: ['?'],
elseOperator: [':'],
multiplicativeOperator: ['*', '/'],
exponentOperator: ['^'],
additiveOperator: ['+', '-'],
unaryOperator: ['-'],
andOperator: ['&', '&&'],
orOperator: ['|', '||'],
stringDelimiters: ['\"', '\''],
equalityOperator: ['=', '==', '===', '!=', '!=='],
notOperator: ['!'],
relationalOperator: ['>', '<', '>=', '<='],
brackets: ['(', ')', '{', '}'],
brackets: ['(', ')', '{', '}', '[', ']'],
});
function nuller() { return null; }
@@ -49,50 +61,78 @@ function id(x) { return x[0]; }
}
let Lexer = lexer;
let ParserRules = [
{"name": "ifStatement", "symbols": [{"literal":"if"}, "_", {"literal":"("}, "_", "expression", "_", {"literal":")"}, "_", "ifStatement", "_", {"literal":"else"}, "_", "ifStatement"], "postprocess":
d => new IfNode({condition: d[4], consequent: d[8], alternative: d[12]})
{"name": "spacedExpression", "symbols": ["_", "expression", "_"], "postprocess": d => d[1]},
{"name": "expression", "symbols": ["ifStatement"], "postprocess": id},
{"name": "ifStatement", "symbols": ["orExpression", "_", (lexer.has("ifOperator") ? {type: "ifOperator"} : ifOperator), "_", "orExpression", "_", (lexer.has("elseOperator") ? {type: "elseOperator"} : elseOperator), "_", "ifStatement"], "postprocess":
d => new IfNode({condition: d[0], consequent: d[4], alternative: d[8]})
},
{"name": "ifStatement", "symbols": ["expression"], "postprocess": id},
{"name": "expression", "symbols": ["equalityExpression"], "postprocess": d => d[0]},
{"name": "equalityExpression", "symbols": ["equalityExpression", "_", (lexer.has("equalityOperator") ? {type: "equalityOperator"} : equalityOperator), "_", "relationalExpression"], "postprocess": d => operator(d, 'equality')},
{"name": "equalityExpression", "symbols": ["relationalExpression"], "postprocess": id},
{"name": "relationalExpression", "symbols": ["relationalExpression", "_", (lexer.has("relationalOperator") ? {type: "relationalOperator"} : relationalOperator), "_", "orExpression"], "postprocess": d => operator(d, 'relation')},
{"name": "relationalExpression", "symbols": ["orExpression"], "postprocess": id},
{"name": "ifStatement", "symbols": ["orExpression"], "postprocess": id},
{"name": "orExpression", "symbols": ["orExpression", "_", (lexer.has("orOperator") ? {type: "orOperator"} : orOperator), "_", "andExpression"], "postprocess": d => operator(d, 'or')},
{"name": "orExpression", "symbols": ["andExpression"], "postprocess": id},
{"name": "andExpression", "symbols": ["andExpression", "_", (lexer.has("andOperator") ? {type: "andOperator"} : andOperator), "_", "additiveExpression"], "postprocess": d => operator(d, 'and')},
{"name": "andExpression", "symbols": ["additiveExpression"], "postprocess": id},
{"name": "andExpression", "symbols": ["andExpression", "_", (lexer.has("andOperator") ? {type: "andOperator"} : andOperator), "_", "equalityExpression"], "postprocess": d => operator(d, 'and')},
{"name": "andExpression", "symbols": ["equalityExpression"], "postprocess": id},
{"name": "equalityExpression", "symbols": ["equalityExpression", "_", (lexer.has("equalityOperator") ? {type: "equalityOperator"} : equalityOperator), "_", "relationalExpression"], "postprocess": d => operator(d, 'equality')},
{"name": "equalityExpression", "symbols": ["relationalExpression"], "postprocess": id},
{"name": "relationalExpression", "symbols": ["relationalExpression", "_", (lexer.has("relationalOperator") ? {type: "relationalOperator"} : relationalOperator), "_", "additiveExpression"], "postprocess": d => operator(d, 'relation')},
{"name": "relationalExpression", "symbols": ["additiveExpression"], "postprocess": id},
{"name": "additiveExpression", "symbols": ["additiveExpression", "_", (lexer.has("additiveOperator") ? {type: "additiveOperator"} : additiveOperator), "_", "multiplicativeExpression"], "postprocess": d => operator(d, 'add')},
{"name": "additiveExpression", "symbols": ["multiplicativeExpression"], "postprocess": id},
{"name": "multiplicativeExpression", "symbols": ["multiplicativeExpression", "_", (lexer.has("multiplicativeOperator") ? {type: "multiplicativeOperator"} : multiplicativeOperator), "_", "rollExpression"], "postprocess": d => operator(d, 'multiply')},
{"name": "multiplicativeExpression", "symbols": ["rollExpression"], "postprocess": id},
{"name": "rollExpression", "symbols": ["rollExpression", "_", {"literal":"d"}, "_", "exponentExpression"], "postprocess": d => operator(d, 'roll')},
{"name": "rollExpression", "symbols": ["exponentExpression"], "postprocess": id},
{"name": "rollExpression", "symbols": ["rollExpression", "_", (lexer.has("diceOperator") ? {type: "diceOperator"} : diceOperator), "_", "exponentExpression"], "postprocess": d => new RollNode({left: d[0], right: d[4]})},
{"name": "rollExpression", "symbols": ["singleRollExpression"], "postprocess": id},
{"name": "singleRollExpression", "symbols": [{"literal":"d"}, "_", "singleRollExpression"], "postprocess": d => new RollNode({left: new ConstantNode({value: 1, type: 'number'}), right: d[2]})},
{"name": "singleRollExpression", "symbols": ["exponentExpression"], "postprocess": id},
{"name": "exponentExpression", "symbols": ["callExpression", "_", (lexer.has("exponentOperator") ? {type: "exponentOperator"} : exponentOperator), "_", "exponentExpression"], "postprocess": d => operator(d, 'exponent')},
{"name": "exponentExpression", "symbols": ["callExpression"], "postprocess": id},
{"name": "exponentExpression", "symbols": ["unaryExpression"], "postprocess": id},
{"name": "unaryExpression", "symbols": [(lexer.has("additiveOperator") ? {type: "additiveOperator"} : additiveOperator), "_", "unaryExpression"], "postprocess": d => new UnaryOperatorNode({operator: d[0].value, right: d[2]})},
{"name": "unaryExpression", "symbols": ["notExpression"], "postprocess": id},
{"name": "notExpression", "symbols": [(lexer.has("notOperator") ? {type: "notOperator"} : notOperator), "_", "notExpression"], "postprocess": d => new NotOperatorNode({right: d[2]})},
{"name": "notExpression", "symbols": ["callExpression"], "postprocess": id},
{"name": "callExpression", "symbols": ["name", "_", "arguments"], "postprocess":
d => new CallNode ({type: "call", fn: d[0], arguments: d[2]})
d => new CallNode ({functionName: d[0].name, args: d[2]})
},
{"name": "callExpression", "symbols": ["parenthesizedExpression"], "postprocess": id},
{"name": "callExpression", "symbols": ["indexExpression"], "postprocess": id},
{"name": "arguments$ebnf$1$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]},
{"name": "arguments$ebnf$1", "symbols": ["arguments$ebnf$1$subexpression$1"], "postprocess": id},
{"name": "arguments$ebnf$1", "symbols": [], "postprocess": function(d) {return null;}},
{"name": "arguments$ebnf$2", "symbols": []},
{"name": "arguments$ebnf$2$subexpression$1", "symbols": ["_", {"literal":","}, "_", "expression"], "postprocess": d => d[3]},
{"name": "arguments$ebnf$2$subexpression$1", "symbols": ["_", (lexer.has("separator") ? {type: "separator"} : separator), "_", "expression"], "postprocess": d => d[3]},
{"name": "arguments$ebnf$2", "symbols": ["arguments$ebnf$2", "arguments$ebnf$2$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
{"name": "arguments", "symbols": [{"literal":"("}, "_", "arguments$ebnf$1", "arguments$ebnf$2", "_", {"literal":")"}], "postprocess":
d => [d[2], ...d[3]]
},
{"name": "indexExpression", "symbols": ["arrayExpression", {"literal":"["}, "_", "expression", "_", {"literal":"]"}], "postprocess": d => new IndexNode ({array: d[0], index: d[3]})},
{"name": "indexExpression", "symbols": ["arrayExpression"], "postprocess": id},
{"name": "arrayExpression$ebnf$1$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]},
{"name": "arrayExpression$ebnf$1", "symbols": ["arrayExpression$ebnf$1$subexpression$1"], "postprocess": id},
{"name": "arrayExpression$ebnf$1", "symbols": [], "postprocess": function(d) {return null;}},
{"name": "arrayExpression$ebnf$2", "symbols": []},
{"name": "arrayExpression$ebnf$2$subexpression$1", "symbols": ["_", (lexer.has("separator") ? {type: "separator"} : separator), "_", "expression"], "postprocess": d => d[3]},
{"name": "arrayExpression$ebnf$2", "symbols": ["arrayExpression$ebnf$2", "arrayExpression$ebnf$2$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
{"name": "arrayExpression", "symbols": [{"literal":"["}, "_", "arrayExpression$ebnf$1", "arrayExpression$ebnf$2", "_", {"literal":"]"}], "postprocess":
d => new ArrayNode({values: d[2] ? [d[2], ...d[3]] : []})
},
{"name": "parenthesizedExpression", "symbols": [{"literal":"("}, "_", "expression", "_", {"literal":")"}], "postprocess": d => d[2]},
{"name": "parenthesizedExpression", "symbols": ["valueExpression"], "postprocess": id},
{"name": "arrayExpression", "symbols": ["parenthesizedExpression"], "postprocess": id},
{"name": "parenthesizedExpression", "symbols": [{"literal":"("}, "_", "expression", "_", {"literal":")"}], "postprocess": d => new ParenthesisNode({content: d[2]})},
{"name": "parenthesizedExpression", "symbols": ["accessorExpression"], "postprocess": id},
{"name": "accessorExpression$ebnf$1$subexpression$1", "symbols": [{"literal":"."}, "name"], "postprocess": d => d[1].name},
{"name": "accessorExpression$ebnf$1", "symbols": ["accessorExpression$ebnf$1$subexpression$1"]},
{"name": "accessorExpression$ebnf$1$subexpression$2", "symbols": [{"literal":"."}, "name"], "postprocess": d => d[1].name},
{"name": "accessorExpression$ebnf$1", "symbols": ["accessorExpression$ebnf$1", "accessorExpression$ebnf$1$subexpression$2"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
{"name": "accessorExpression", "symbols": ["name", "accessorExpression$ebnf$1"], "postprocess": d=> new AccessorNode({name: d[0], path: d[1]})},
{"name": "accessorExpression", "symbols": ["valueExpression"], "postprocess": id},
{"name": "valueExpression", "symbols": ["name"], "postprocess": id},
{"name": "valueExpression", "symbols": ["number"], "postprocess": id},
{"name": "valueExpression", "symbols": ["string"], "postprocess": id},
{"name": "number", "symbols": [(lexer.has("number") ? {type: "number"} : number)], "postprocess": d => new ConstantNode({value: d[0].value, type: 'number'})},
{"name": "valueExpression", "symbols": ["boolean"], "postprocess": id},
{"name": "number", "symbols": [(lexer.has("number") ? {type: "number"} : number)], "postprocess": d => new ConstantNode({value: +d[0].value, type: 'number'})},
{"name": "name", "symbols": [(lexer.has("name") ? {type: "name"} : name)], "postprocess": d => new SymbolNode({name: d[0].value})},
{"name": "string", "symbols": [(lexer.has("string") ? {type: "string"} : string)], "postprocess": d => new ConstantNode({value: d[0].value, type: 'string'})},
{"name": "boolean", "symbols": [{"literal":"true"}], "postprocess": d => new ConstantNode({value: true, type: 'boolean'})},
{"name": "boolean", "symbols": [{"literal":"false"}], "postprocess": d => new ConstantNode({value: false, type: 'boolean'})},
{"name": "_", "symbols": []},
{"name": "_", "symbols": [(lexer.has("space") ? {type: "space"} : space)], "postprocess": nuller}
];
let ParserStart = "ifStatement";
let ParserStart = "spacedExpression";
export default { Lexer, ParserRules, ParserStart };

View File

@@ -1,10 +1,18 @@
@preprocessor esmodule
@{%
import AccessorNode from '/imports/parser/parseTree/AccessorNode.js';
import ArrayNode from '/imports/parser/parseTree/ArrayNode.js';
import CallNode from '/imports/parser/parseTree/CallNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
import IfNode from '/imports/parser/parseTree/IfNode.js';
import IfNode from '/imports/parser/parseTree/IfNode.js';
import IndexNode from '/imports/parser/parseTree/IndexNode.js';
import OperatorNode from '/imports/parser/parseTree/OperatorNode.js';
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
import ParenthesisNode from '/imports/parser/parseTree/ParenthesisNode.js';
import RollNode from '/imports/parser/parseTree/RollNode.js';
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
import UnaryOperatorNode from '/imports/parser/parseTree/UnaryOperatorNode.js';
import NotOperatorNode from '/imports/parser/parseTree/NotOperatorNode.js';
import moo from 'moo';
const lexer = moo.compile({
@@ -14,26 +22,30 @@
value: s => s.slice(1, -1),
},
name: {
match: /[a-zA-Z]+\w*?/,
match: /[a-zA-Z_]*[a-ce-zA-Z_][a-zA-Z0-9_]*/,
type: moo.keywords({
'keywords': ['if', 'else', 'd'],
'keywords': ['true', 'false'],
}),
},
space: {
match: /\s+/,
lineBreaks: true,
},
separators: [',', '.'],
diceOperator: ['d'],
separator: [',', ';'],
period: ['.'],
ifOperator: ['?'],
elseOperator: [':'],
multiplicativeOperator: ['*', '/'],
exponentOperator: ['^'],
additiveOperator: ['+', '-'],
unaryOperator: ['-'],
andOperator: ['&', '&&'],
orOperator: ['|', '||'],
stringDelimiters: ['\"', '\''],
equalityOperator: ['=', '==', '===', '!=', '!=='],
notOperator: ['!'],
relationalOperator: ['>', '<', '>=', '<='],
brackets: ['(', ')', '{', '}'],
brackets: ['(', ')', '{', '}', '[', ']'],
});
function nuller() { return null; }
@@ -50,21 +62,16 @@
# Use the Moo lexer
@lexer lexer
ifStatement ->
"if" _ "(" _ expression _ ")" _ ifStatement _ "else" _ ifStatement {%
d => new IfNode({condition: d[4], consequent: d[8], alternative: d[12]})
%}
| expression {% id %}
spacedExpression ->
_ expression _ {% d => d[1] %}
expression ->
equalityExpression {% d => d[0] %}
ifStatement {% id %}
equalityExpression ->
equalityExpression _ %equalityOperator _ relationalExpression {% d => operator(d, 'equality') %}
| relationalExpression {% id %}
relationalExpression ->
relationalExpression _ %relationalOperator _ orExpression {% d => operator(d, 'relation') %}
ifStatement ->
orExpression _ %ifOperator _ orExpression _ %elseOperator _ ifStatement {%
d => new IfNode({condition: d[0], consequent: d[4], alternative: d[8]})
%}
| orExpression {% id %}
orExpression ->
@@ -72,7 +79,15 @@ orExpression ->
| andExpression {% id %}
andExpression ->
andExpression _ %andOperator _ additiveExpression {% d => operator(d, 'and') %}
andExpression _ %andOperator _ equalityExpression {% d => operator(d, 'and') %}
| equalityExpression {% id %}
equalityExpression ->
equalityExpression _ %equalityOperator _ relationalExpression {% d => operator(d, 'equality') %}
| relationalExpression {% id %}
relationalExpression ->
relationalExpression _ %relationalOperator _ additiveExpression {% d => operator(d, 'relation') %}
| additiveExpression {% id %}
additiveExpression ->
@@ -84,36 +99,63 @@ multiplicativeExpression ->
| rollExpression {% id %}
rollExpression ->
rollExpression _ "d" _ exponentExpression {% d => operator(d, 'roll') %}
rollExpression _ %diceOperator _ exponentExpression {% d => new RollNode({left: d[0], right: d[4]}) %}
| singleRollExpression {% id %}
singleRollExpression ->
"d" _ singleRollExpression {% d => new RollNode({left: new ConstantNode({value: 1, type: 'number'}), right: d[2]}) %}
| exponentExpression {% id %}
exponentExpression ->
callExpression _ %exponentOperator _ exponentExpression {% d => operator(d, 'exponent') %}
| unaryExpression {% id %}
unaryExpression ->
%additiveOperator _ unaryExpression {% d => new UnaryOperatorNode({operator: d[0].value, right: d[2]})%}
| notExpression {% id %}
notExpression ->
%notOperator _ notExpression {% d => new NotOperatorNode({right: d[2]})%}
| callExpression {% id %}
callExpression ->
name _ arguments {%
d => new CallNode ({type: "call", fn: d[0], arguments: d[2]})
d => new CallNode ({functionName: d[0].name, args: d[2]})
%}
| indexExpression {% id %}
arguments ->
"(" _ (expression {% d => d[0] %}):? ( _ %separator _ expression {% d => d[3] %} ):* _ ")" {%
d => [d[2], ...d[3]]
%}
indexExpression ->
arrayExpression "[" _ expression _ "]" {% d => new IndexNode ({array: d[0], index: d[3]}) %}
| arrayExpression {% id %}
arrayExpression ->
"[" _ (expression {% d => d[0] %}):? ( _ %separator _ expression {% d => d[3] %} ):* _ "]" {%
d => new ArrayNode({values: d[2] ? [d[2], ...d[3]] : []})
%}
| parenthesizedExpression {% id %}
arguments ->
"(" _ (expression {% d => d[0] %}):? ( _ "," _ expression {% d => d[3] %} ):* _ ")" {%
d => [d[2], ...d[3]]
%}
parenthesizedExpression ->
"(" _ expression _ ")" {% d => d[2] %}
"(" _ expression _ ")" {% d => new ParenthesisNode({content: d[2]}) %}
| accessorExpression {% id %}
accessorExpression ->
name ( "." name {% d => d[1].name %} ):+ {% d=> new AccessorNode({name: d[0], path: d[1]}) %}
| valueExpression {% id %}
valueExpression ->
name {% id %}
| number {% id %}
| string {% id %}
| boolean {% id %}
# A number or a function of a number
number ->
%number {% d => new ConstantNode({value: d[0].value, type: 'number'}) %}
%number {% d => new ConstantNode({value: +d[0].value, type: 'number'}) %}
name ->
%name {% d => new SymbolNode({name: d[0].value}) %}
@@ -121,6 +163,10 @@ name ->
string ->
%string {% d => new ConstantNode({value: d[0].value, type: 'string'}) %}
boolean ->
"true" {% d => new ConstantNode({value: true, type: 'boolean'}) %}
| "false" {% d => new ConstantNode({value: false, type: 'boolean'}) %}
_ ->
null
| %space {% nuller %}

View File

@@ -0,0 +1,47 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default class AccessorNode extends ParseNode {
constructor({name, path}) {
super(...arguments);
this.name = name;
this.path = path;
}
compile(scope){
let value = scope && scope[this.name];
// For objects, get their value
this.path.forEach(name => {
if (value === undefined) return;
value = value[name];
});
let type = typeof value;
if (type === 'string' || type === 'number' || type === 'boolean'){
return new ConstantNode({value, type, previousNodes: [this]});
} else if (type === 'undefined'){
return new AccessorNode({
name: this.name,
path: this.path,
});
} else {
throw new Meteor.Error(`Unexpected case: ${this.name} resolved to ${value}`);
}
}
reduce(scope, context){
let result = this.compile(scope, context);
if (result instanceof AccessorNode){
if (context) context.storeError({
type: 'info',
message: `${result.toString()} not found, set to 0`
});
return new ConstantNode({
type: 'number',
value: 0,
});
} else {
return result;
}
}
toString(){
return `${this.name}.${this.path.join('.')}`;
}
}

View File

@@ -0,0 +1,19 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
export default class ArrayNode extends ParseNode {
constructor({values}) {
super(...arguments);
this.values = values;
}
resolve(fn, scope, context){
let values = this.values.map(node => node[fn](scope, context));
return new ArrayNode({values});
}
toString(){
return `[${this.values.map(node => node.toString()).join(', ')}]`;
}
traverse(fn){
fn(this);
this.values.forEach(value => value.traverse(fn));
}
}

View File

@@ -1 +1,73 @@
//TODO
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
import functions from '/imports/parser/functions.js';
export default class CallNode extends ParseNode {
constructor({functionName, args}) {
super(...arguments);
this.functionName = functionName;
this.args = args;
}
resolve(fn, scope, context){
let func = functions[this.functionName];
if (!func) return new ErrorNode({
node: this,
error: `${this.functionName} is not a function`,
context,
});
let args = castArgsToType({fn, scope, context, args: this.args, type: func.argumentType});
if (args.failed){
if (fn === 'reduce'){
return new ErrorNode({
node: this,
error: 'Could not convert all arguments to the correct type',
context,
});
} else {
return new CallNode({
functionName: this.functionName,
args: args,
});
}
} else {
try {
let value = func.fn.apply(null, args);
return new ConstantNode({
value,
type: 'number',
previousNodes: [this],
});
} catch (error) {
return new ErrorNode({
node: this,
error,
context,
});
}
}
}
toString(){
return `${this.functionName}(${this.args.map(node => node.toString()).join(', ')})`;
}
traverse(fn){
fn(this);
this.args.forEach(arg => arg.traverse(fn));
}
}
function castArgsToType({fn, scope, context, args, type}){
let resolvedArgs = args.map(node => node[fn](scope, context))
let result = [];
if (type === 'number'){
resolvedArgs.forEach(node => {
if (node.isNumber){
result.push(node.value);
} else {
resolvedArgs.failed = true;
}
})
}
if (resolvedArgs.failed) return resolvedArgs;
return result;
}

View File

@@ -1,21 +1,22 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
export default class ConstantNode extends ParseNode {
constructor({value, type, errors}){
super();
// string, number, boolean, numberArray, uncompiledNode
constructor({value, type}){
super(...arguments);
// string, number, boolean, uncompiledNode
this.type = type;
this.value = value;
if (errors) this.errors = errors;
}
compile(){
return this;
}
reduce(){
if (this.type === 'numberArray'){
return this.value.reduce((total, num) => total + num, 0);
} else {
return this;
}
toString(){
return `${this.value}`;
}
get isNumber(){
return this.type === 'number';
}
get isInteger(){
return this.type === 'number' && Number.isInteger(this.value);
}
}

View File

@@ -0,0 +1,21 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
export default class ErrorNode extends ParseNode {
constructor({node, error, context}) {
super(...arguments);
this.node = node;
this.error = error;
if (context){
context.storeError({
type: 'error',
message: error,
});
}
}
compile(){
return this;
}
toString(){
return '###';
}
}

View File

@@ -3,38 +3,35 @@ import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default class IfNode extends ParseNode {
constructor({condition, consequent, alternative}){
super();
super(...arguments);
this.condition = condition;
this.consequent = consequent;
this.alternative = alternative;
}
compile(){
let condition = this.condition.compile();
let consequent = this.consequent.compile();
let alternative = this.alternative.compile();
if (
condition.type !== 'string' &&
condition.type !== 'number' &&
condition.type !== 'boolean'
){
// Handle unresolved condition
return new ConstantNode({
value: `if (${condition.value}) ${consequent.value} else ${alternative.value}`,
type: 'uncompiledNode',
errors: [
...condition.errors,
...consequent.errors,
...alternative.errors,
],
});
} else {
// So long as the condition reolves, return the correct alternative,
// even if it's unresolved
toString(){
let {condition, consequent, alternative} = this;
return `${condition.toString()} ? ${consequent.toString()} : ${alternative.toString()}`
}
resolve(fn, scope, context){
let condition = this.condition[fn](scope, context);
if (condition instanceof ConstantNode){
if (condition.value){
return consequent;
return this.consequent[fn](scope, context);
} else {
return alternative;
return this.alternative[fn](scope, context);
}
} else {
return new IfNode({
condition: condition,
consequent: this.consequent,
alternative: this.alternative,
});
}
}
traverse(fn){
fn(this);
this.condition.traverse(fn);
this.consequent.traverse(fn);
this.alternative.traverse(fn);
}
}

View File

@@ -0,0 +1,32 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
export default class IndexNode extends ParseNode {
constructor({array, index}) {
super(...arguments);
this.array = array;
this.index = index;
}
resolve(fn, scope, context){
let index = this.index[fn](scope, context);
if (index.isInteger){
let selection = this.array.values[index.value - 1];
if (selection){
let result = selection[fn](scope, context);
return result;
}
}
return new IndexNode({
index,
array: this.array[fn](scope, context),
previousNodes: [this],
});
}
toString(){
return `${this.array.toString()}[${this.index.toString()}]`;
}
traverse(fn){
fn(this);
this.array.traverse(fn);
this.index.traverse(fn);
}
}

View File

@@ -0,0 +1,31 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default class NotOperatorNode extends ParseNode {
constructor({right}) {
super(...arguments);
this.right = right;
}
resolve(fn, scope, context){
let rightNode = this.right[fn](scope, context);
if (!(rightNode instanceof ConstantNode)){
return new NotOperatorNode({
right: rightNode,
});
}
let right = rightNode.value;
let result = !right;
return new ConstantNode({
value: result,
type: typeof result,
});
}
toString(){
let {right} = this;
return `!${right.toString()}`;
}
traverse(fn){
fn(this);
this.right.traverse(fn);
}
}

View File

@@ -1,11 +1,62 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default class OperatorNode extends ParseNode {
constructor({left, right, operator, fn}) {
super();
super(...arguments);
this.left = left;
this.right = right;
this.fn = fn;
this.operator = operator;
}
resolve(fn, scope, context){
let leftNode = this.left[fn](scope, context);
let rightNode = this.right[fn](scope, context);
let left, right;
if (!(leftNode instanceof ConstantNode) || !(rightNode instanceof ConstantNode)){
return new OperatorNode({
left: leftNode,
right: rightNode,
operator: this.operator,
fn: this.fn,
});
} else {
left = leftNode.value;
right = rightNode.value;
}
let result;
switch(this.operator){
case '+': result = left + right; break;
case '-': result = left - right; break;
case '*': result = left * right; break;
case '/': result = left / right; break;
case '^': result = Math.pow(left, right); break;
case '&':
case '&&': result = left && right; break;
case '|':
case '||': result = left || right; break;
case '=':
case '==': result = left == right; break;
case '===': result = left === right; break;
case '!=': result = left != right; break;
case '!==': result = left !== right; break;
case '>': result = left > right; break;
case '<': result = left < right; break;
case '>=': result = left >= right; break;
case '<=': result = left <= right; break;
}
return new ConstantNode({
value: result,
type: typeof result,
});
}
toString(){
let {left, right, operator} = this;
return `${left.toString()} ${operator} ${right.toString()}`;
}
traverse(fn){
fn(this);
this.left.traverse(fn);
this.right.traverse(fn);
}
}

View File

@@ -0,0 +1,27 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
export default class ParenthesisNode extends ParseNode {
constructor({content}) {
super(...arguments);
this.content = content;
}
resolve(fn, scope, context){
let content = this.content[fn](scope, context);
if (
content.constructor.name === 'IfNode' ||
content.constructor.name === 'OperatorNode' ||
content.constructor.name === 'RollNode'
){
return new ParenthesisNode({content, previousNodes: [this]});
} else {
return content;
}
}
toString(){
return `(${this.content.toString()})`;
}
traverse(fn){
fn(this);
this.content.traverse(fn);
}
}

View File

@@ -1,14 +1,33 @@
export default class ParseNode {
// Compiling a node must return a ConstantNode
compile(){
throw new Meteor.Error('Compile not implemented on ' + this);
toString(){
throw new Meteor.Error('toString not implemented on ' + this.constructor.name);
}
compile(scope, context){
// Returns a ParseNode, a ConstantNode if possible
if(this.resolve) {
return this.resolve('compile', scope, context);
} else {
throw new Meteor.Error('Compile not implemented on ' + this.constructor.name);
}
}
// Compile, but turn rolls into arrays
roll(){
return this.compile();
roll(scope, context){
if (this.resolve){
return this.resolve('roll', scope, context);
} else {
return this.compile(scope, context);
}
}
// Compile, turn rolls into arrays, and reduce those arrays into single values
reduce(){
return this.compileAndRoll()
reduce(scope, context){
if (this.resolve){
return this.resolve('reduce', scope, context);
} else {
return this.roll(scope, context);
}
}
// If traverse isn't implemented, just apply it to the current node
traverse(fn){
fn(this);
}
}

View File

@@ -0,0 +1,22 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default class RollArrayNode extends ParseNode {
constructor({values}) {
super(...arguments);
this.values = values;
}
compile(){
return this;
}
toString(){
return `[${this.values.join(', ')}]`;
}
reduce(){
let total = this.values.reduce((a, b) => a + b);
return new ConstantNode({
value: total,
type: 'number',
});
}
}

View File

@@ -0,0 +1,67 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
import RollArrayNode from '/imports/parser/parseTree/RollArrayNode.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
import roll from '/imports/parser/roll.js';
export default class RollNode extends ParseNode {
constructor({left, right}) {
super(...arguments);
this.left = left;
this.right = right;
}
compile(scope, context){
let left = this.left.compile(scope, context);
let right = this.right.compile(scope, context);
return new RollNode({left, right, previousNodes: [this]});
}
toString(){
if (
this.left.isNumberNode && this.left.value === 1
){
return `d${this.right.toString()}`;
} else {
return `${this.left.toString()}d${this.right.toString()}`;
}
}
roll(scope, context){
let left = this.left.reduce(scope, context);
let right = this.right.reduce(scope, context);
if (!left.isInteger){
return new ErrorNode({
node: this,
error: 'Number of dice is not an integer',
previousNodes: [this, left, right],
});
}
if (!right.isInteger){
return new ErrorNode({
node: this,
error: 'Dice size is not an integer',
previousNodes: [this, left, right],
});
}
let number = left.value;
if (context.doubleRolls){
number *= 2;
}
if (number > 100) return new ErrorNode({
node: this,
error: 'Can\'t roll more than 100 dice at once',
context,
});
let diceSize = right.value;
let values = roll(number, diceSize);
if (context){
context.storeRoll({number, diceSize, values});
}
return new RollArrayNode({values});
}
reduce(scope, context){
return this.roll(scope, context).reduce(scope, context);
}
traverse(fn){
fn(this);
this.left.traverse(fn);
this.right.traverse(fn);
}
}

View File

@@ -3,22 +3,43 @@ import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default class SymbolNode extends ParseNode {
constructor({name}){
super();
super(...arguments);
this.name = name;
}
toString(){
return `${this.name}`
}
compile(scope){
let value = scope && scope[this.name];
let type = typeof value;
// For objects, get their value
if (type === 'object'){
value = value.value;
type = typeof value;
}
if (type === 'string' || type === 'number' || type === 'boolean'){
return new ConstantNode({value, type});
} else if (type === 'undefined'){
return new ConstantNode({
value: this.name,
type: 'uncompiledNode',
errors: [`${this.name} could not be resolved`]
return new SymbolNode({
name: this.name,
});
} else {
throw new Meteor.Error(`Unexpected case: ${this.name} resolved to ${value}`);
}
}
reduce(scope, context){
let result = this.compile(scope);
if (result instanceof SymbolNode){
if (context) context.storeError({
type: 'info',
message: `${result.toString()} not found, set to 0`
});
return new ConstantNode({
type: 'number',
value: 0,
});
} else {
return result;
}
}
}

View File

@@ -0,0 +1,37 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default class UnaryOperatorNode extends ParseNode {
constructor({operator, right}) {
super(...arguments);
this.operator = operator;
this.right = right;
}
resolve(fn, scope, context){
let rightNode = this.right[fn](scope, context);
if (rightNode.type !== 'number'){
return new UnaryOperatorNode({
operator: this.operator,
right: rightNode,
});
}
let right = rightNode.value;
let result;
switch(this.operator){
case '-': result = -right; break;
case '+': result = +right; break;
}
return new ConstantNode({
value: result,
type: typeof result,
});
}
toString(){
let {right, operator} = this;
return `${operator}${right.toString()}`;
}
traverse(fn){
fn(this);
this.right.traverse(fn);
}
}

View File

@@ -6,3 +6,32 @@ const nearleyGrammar = nearley.Grammar.fromCompiled(grammar);
export default function parser(){
return new nearley.Parser(nearleyGrammar);
}
export class CompilationContext {
constructor({doubleRolls} = {}){
this.errors = [];
this.rolls = [];
this.doubleRolls = doubleRolls;
}
storeError(e){
this.errors.push(e);
}
storeRoll(r){
this.rolls.push(r);
}
}
export function parse(string){
let parser = new nearley.Parser(nearleyGrammar);
parser.feed(string);
let results = parser.results;
if (results.length === 1){
return results[0];
} else if (results.length === 0){
// Valid parsing up until now, but need more. Unexpected end of input.
return null;
} else {
console.warn('Grammar is ambiguous!', {string, results});
return results[0];
}
}

View File

@@ -0,0 +1,9 @@
export default function roll(number, diceSize){
let values = [];
let randomSrc = DDP.randomStream('diceRoller');
for (let i = 0; i < number; i++){
let roll = ~~(randomSrc.fraction() * diceSize) + 1
values.push(roll);
}
return values;
}

View File

@@ -23,7 +23,7 @@ Meteor.startup(() => {
removedAt: {$lt: thirtyMinutesAgo} // dates *before* 30 minutes ago
}, function(error){
if (error){
console.error(error);
console.error(JSON.stringify(error, null, 2));
}
});
});
@@ -32,7 +32,7 @@ Meteor.startup(() => {
SyncedCron.add({
name: 'deleteSoftRemovedDocs',
schedule: function(parser) {
return parser.text('every 2 hours');
return parser.text('every 10 minutes');
},
job: deleteOldSoftRemovedDocs,
});

View File

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

View File

@@ -1,7 +0,0 @@
import Discord from 'discord.js'
export default function sendWebhook({webhook, message}){
// const hook = new Discord.WebhookClient(webhook.id, webhook.token);
const hook = new Discord.WebhookClient('420492135716880394', 'KHmRsf9QHd81C4LZOyQe_cUw5ua4ugSaIlpDMNWo3vcNHs0p0JBOHfeGWtHKqPXMYgkk');
// Send a message using the webhook
hook.send(message);
}

View File

@@ -5,7 +5,7 @@ Meteor.publish('characterList', function(){
this.autorun(function (){
var userId = this.userId;
if (!userId) {
return this.ready();
return [];
}
const user = Meteor.users.findOne(this.userId, {
fields: {subscribedCharacters: 1}

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/Creatures.js';
import Experiences from '/imports/api/creature/experience/Experiences.js';
import { assertViewPermission } from '/imports/api/creature/creaturePermissions.js';
let schema = new SimpleSchema({
creatureId: {
@@ -13,6 +14,9 @@ Meteor.publish('experiences', function(creatureId){
schema.validate({ creatureId });
this.autorun(function (){
let userId = this.userId;
if (!userId) {
return [];
}
let creatureCursor = Creatures.find({
_id: creatureId,
$or: [
@@ -22,7 +26,11 @@ Meteor.publish('experiences', function(creatureId){
{public: true},
],
});
if (!creatureCursor.count()) return this.ready();
try {
assertViewPermission(creatureCursor.fetch()[0], this.userId);
} catch (e){
return [];
}
return [
Experiences.find({
creatureId,

View File

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

View File

@@ -1,7 +1,7 @@
import SimpleSchema from 'simpl-schema';
import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import { assertViewPermission } from '/imports/api/sharing/sharingPermissions.js';
const standardLibraryIds = [
'SRDLibraryGA3XWsd',
];
@@ -12,10 +12,11 @@ Meteor.publish('standardLibraries', function(){
Meteor.publish('libraries', function(){
this.autorun(function (){
if (!this.userId) {
return this.ready();
let userId = this.userId;
if (!userId) {
return [];
}
const user = Meteor.users.findOne(this.userId, {
const user = Meteor.users.findOne(userId, {
fields: {subscribedLibraries: 1}
});
const subs = user && user.subscribedLibraries || [];
@@ -40,24 +41,13 @@ let libraryIdSchema = new SimpleSchema({
Meteor.publish('library', function(libraryId){
libraryIdSchema.validate({libraryId});
this.autorun(function (){
let libraryCursor
if (this.userId) {
libraryCursor = Libraries.find({
_id: libraryId,
$or: [
{owner: this.userId},
{writers: this.userId},
{readers: this.userId},
{public: true},
],
});
} else {
libraryCursor = Libraries.find({
_id: libraryId,
public: true,
});
}
if (!libraryCursor.count()) return this.ready();
let userId = this.userId;
let libraryCursor = Libraries.find({
_id: libraryId,
});
let library = libraryCursor.fetch()[0];
try { assertViewPermission(library, userId) }
catch(e){ return [] }
return [
libraryCursor,
LibraryNodes.find({

View File

@@ -1,4 +1,4 @@
// Limit all subscriptions to 1/s
DDPRateLimiter.addRule({
type: 'subscription',
}, 10, 10000);
}, 50, 10000);

View File

@@ -1,6 +1,10 @@
import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/Creatures.js';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
import { assertViewPermission } from '/imports/api/creature/creaturePermissions.js';
import { recomputeCreatureById } from '/imports/api/creature/computation/recomputeCreature.js';
import VERSION from '/imports/constants/VERSION.js';
let schema = new SimpleSchema({
creatureId: {
@@ -13,21 +17,28 @@ Meteor.publish('singleCharacter', function(creatureId){
schema.validate({ creatureId });
this.autorun(function (){
let userId = this.userId;
let creatureCursor = Creatures.find({
let creatureCursor
creatureCursor = Creatures.find({
_id: creatureId,
$or: [
{readers: userId},
{writers: userId},
{owner: userId},
{public: true},
],
});
if (!creatureCursor.count()) return this.ready();
let creature = creatureCursor.fetch()[0];
try { assertViewPermission(creature, userId) }
catch(e){ return [] }
if (creature.computeVersion !== VERSION){
try { recomputeCreatureById(creatureId) }
catch(e){ console.error(e) }
}
return [
creatureCursor,
CreatureProperties.find({
'ancestors.id': creatureId,
}),
CreatureLogs.find({
creatureId,
}, {
limit: 20,
sort: {date: -1},
}),
];
});
});

View File

@@ -0,0 +1,51 @@
import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
Meteor.publish('slotFillers', function(slotId){
this.autorun(function (){
let userId = this.userId;
if (!userId) {
return [];
}
// Get the slot
let slot = CreatureProperties.findOne(slotId);
if (!slot){
return [];
}
// Get all the ids of libraries the user can access
const user = Meteor.users.findOne(userId, {
fields: {subscribedLibraries: 1}
});
const subs = user && user.subscribedLibraries || [];
let libraryIds = Libraries.find({
$or: [
{owner: this.userId},
{writers: this.userId},
{readers: this.userId},
{_id: {$in: subs}},
]
}, {
fields: {_id: 1},
}).map(lib => lib._id);
// Build a filter for nodes in those libraries that match the slot
let filter = {
'ancestors.id': {$in: libraryIds},
removed: {$ne: true},
};
if (slot.slotTags && slot.slotTags.length){
filter.tags = {$all: slot.slotTags};
}
if (slot.slotType){
filter.$or = [{
type: slot.slotType
},{
type: 'slotFiller',
slotFillerType: slot.slotType,
}];
}
return LibraryNodes.find(filter);
});
});

View File

@@ -5,7 +5,7 @@ import Messages from '/imports/api/tabletop/Messages.js';
Meteor.publish('tabletops', function(){
var userId = this.userId;
if (!userId) {
return this.ready();
return [];
}
return Tabletops.find({
$or: [
@@ -18,7 +18,7 @@ Meteor.publish('tabletops', function(){
Meteor.publish('tabletop', function(tabletopId){
var userId = this.userId;
if (!userId) {
return this.ready();
return [];
}
this.autorun(function (){
let tabletopCursor = Tabletops.find({
@@ -30,7 +30,7 @@ Meteor.publish('tabletop', function(tabletopId){
});
let tabletop = tabletopCursor.fetch()[0];
if (!tabletop){
return this.ready();
return [];
}
// Warning, this leaks data to users of the same tabletop who may not have
// read permission of this specific creature, so publish as few fields as

View File

@@ -1,6 +1,7 @@
<template lang="html">
<v-card
:hover="hasClickListener"
:elevation="hovering ? 8 : undefined"
class="toolbar-card"
@click="$emit('click')"
>
@@ -11,6 +12,8 @@
:dark="isDark"
:light="!isDark"
@click="$emit('toolbarclick')"
@mouseover="hoverToolbar(true)"
@mouseleave="hoverToolbar(false)"
>
<slot name="toolbar" />
</v-toolbar>
@@ -31,17 +34,27 @@
},
},
},
data(){ return {
hovering: false,
}},
computed: {
isDark(){
return isDarkColor(this.color);
},
hasClickListener(){
return this.$listeners && !!this.$listeners.click;
return this.$listeners && !!this.$listeners.click;
},
hasToolbarClickListener(){
return this.$listeners && !!this.$listeners.toolbarclick;
return this.$listeners && !!this.$listeners.toolbarclick;
},
}
},
methods: {
hoverToolbar(val){
this.hovering = this.$listeners &&
!!this.$listeners.toolbarclick &&
val;
}
}
};
</script>

View File

@@ -0,0 +1,99 @@
<template lang="html">
<v-card>
<template v-if="!result">
<v-btn-toggle v-model="advantage">
<v-btn flat>
Advantage
</v-btn>
<v-btn flat>
Disadvantage
</v-btn>
</v-btn-toggle>
<v-card-text>
<div class="layout row justify-center align-center">
<v-btn
large
fab
outline
@click="makeRoll"
>
<div class="display-1">
{{ numberToSignedString(bonus) }}
</div>
</v-btn>
</div>
</v-card-text>
</template>
<template v-else>
<div>
<div class="title">
<span
v-for="(roll, index) of rolls"
:key="index"
class="roll"
:class="{strikethrough: index !== chosenRollIndex}"
>
{{ roll }}
</span>
<span class="ml-1">
{{ numberToSignedString(bonus) }}
</span>
</div>
<div class="display-1">
{{ result }}
</div>
</div>
</template>
</v-card>
</template>
<script>
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
export default {
props: {
attributeVarName: {
type: String,
default: '',
},
attributeName: {
type: String,
default: '',
},
creatureId: {
type: String,
default: '',
},
bonus: {
type: Number,
required: true,
},
},
data(){return {
advantage: undefined,
result: undefined,
rolls: undefined,
chosenRoll: undefined,
chosenRollIndex: undefined,
}},
methods: {
makeRoll(){
//let {rolls, bonus, chosenRoll, result} = doCheckWork.call();
this.rolls = [12, 8];
if (this.advantage === 1){
this.chosenRoll = 8;
} else {
this.chosenRoll = 12;
}
this.result = this.chosenRoll + this.bonus;
this.chosenRollIndex = this.rolls.indexOf(this.chosenRoll);
},
numberToSignedString,
}
}
</script>
<style lang="css" scoped>
.strikethrough {
text-decoration: line-through;
}
</style>

View File

@@ -0,0 +1,48 @@
<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

@@ -0,0 +1,47 @@
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

@@ -4,24 +4,18 @@
label="Name"
:value="model.name"
:error-messages="errors.name"
:debounce-time="debounceTime"
:disabled="disabled"
@change="(value, ack) => $emit('change', {path: ['name'], value, ack})"
/>
<text-field
label="Alignment"
:value="model.alignment"
:error-messages="errors.alignment"
:debounce-time="debounceTime"
:disabled="disabled"
@change="(value, ack) => $emit('change', {path: ['alignment'], value, ack})"
/>
<text-field
label="Gender"
:value="model.gender"
:error-messages="errors.gender"
:debounce-time="debounceTime"
:disabled="disabled"
@change="(value, ack) => $emit('change', {path: ['gender'], value, ack})"
/>
<text-field
@@ -29,8 +23,6 @@
hint="A link to a high resolution image"
:value="model.picture"
:error-messages="errors.picture"
:debounce-time="debounceTime"
:disabled="disabled"
@change="(value, ack) => $emit('change', {path: ['picture'], value, ack})"
/>
<text-field
@@ -38,8 +30,6 @@
hint="A link to a smaller, square image to use as an avatar"
:value="model.avatarPicture"
:error-messages="errors.avatarPicture"
:debounce-time="debounceTime"
:disabled="disabled"
@change="(value, ack) => $emit('change', {path: ['avatarPicture'], value, ack})"
/>
<form-sections>
@@ -47,7 +37,6 @@
<v-switch
label="Hide redundant stats"
:input-value="model.settings.hideUnusedStats"
:disabled="disabled"
@change="value => $emit('change', {path: ['settings','hideUnusedStats'], value: !!value})"
/>
<text-field
@@ -59,30 +48,32 @@
max="1"
step="0.1"
:value="model.settings.hitDiceResetMultiplier"
:debounce-time="debounceTime"
:disabled="disabled"
@change="(value, ack) => $emit('change', {path: ['settings','hitDiceResetMultiplier'], value, ack})"
/>
<text-field
label="Discord Webhook URL"
hint="This creature's logs will be posted to the discord channel"
placeholder="https://discordapp.com/api/webhooks/<id>/<token>"
:value="model.settings.discordWebhook"
@change="(value, ack) => $emit('change', {path: ['settings','discordWebhook'], value, ack})"
/>
<!--
<v-switch
label="Use variant encumbrance"
:input-value="model.settings.useVariantEncumbrance"
:error-messages="errors.useVariantEncumbrance"
:disabled="disabled"
@change="value => $emit('change', {path: ['settings','useVariantEncumbrance'], value})"
/>
<v-switch
label="Hide spells tab"
:input-value="model.settings.hideSpellcasting"
:error-messages="errors.hideSpellcasting"
:disabled="disabled"
@change="value => $emit('change', {path: ['settings','hideSpellcasting'], value})"
/>
<v-switch
label="Swap ability scores and modifiers"
:input-value="model.settings.swapStatAndModifier"
:error-messages="errors.swapStatAndModifier"
:disabled="disabled"
@change="value => $emit('change', {path: ['settings','swapStatAndModifier'], value})"
/>
-->
@@ -114,7 +105,6 @@ export default {
attackForm: {
type: Boolean,
},
debounceTime: Number,
disabled: Boolean,
},
};

View File

@@ -2,7 +2,7 @@
<dialog-base :color="model.color">
<template slot="toolbar">
<v-toolbar-title>
Creature Form Dialog
Character Details
</v-toolbar-title>
<v-spacer />
<color-picker

View File

@@ -1,6 +1,7 @@
<template lang="html">
<v-btn
:loading="loading"
:disabled="context.editPermission === false"
outline
style="width: 160px;"
@click="rest"
@@ -16,6 +17,9 @@
import restCreature from '/imports/api/creature/restCreature.js';
export default {
inject: {
context: { default: {} }
},
props:{
type: {
type: String,

View File

@@ -0,0 +1,127 @@
<template lang="html">
<div
style="height: 100%; overflow: hidden;"
class="character-log layout column justify-end"
>
<div
class="log flex layout column reverse align-end pa-3"
style="overflow: auto;"
>
<v-card
v-for="log in logs"
:key="log._id"
class="ma-2"
>
<v-card-text class="pa-2">
<markdown-text :markdown="log.text" />
</v-card-text>
</v-card>
</div>
<v-card>
<v-text-field
v-model="input"
class="mx-2 mb-2"
persistent-hint
style="flex-grow: 0"
append-outer-icon="send"
:hint="inputHint"
:error-messages="inputError"
:disabled="!editPermission"
@click:append-outer="submit"
@keyup.enter="submit"
/>
</v-card>
</div>
</template>
<script>
import CreatureLogs, { logRoll } from '/imports/api/creature/log/CreatureLogs.js';
import Creatures from '/imports/api/creature/Creatures.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { parse } from '/imports/parser/parser.js';
import MarkdownText from '/imports/ui/components/MarkdownText.vue';
export default {
components: {
MarkdownText,
},
props: {
creatureId: {
type: String,
required: true,
},
},
data(){return {
inputHint: undefined,
inputError: undefined,
input: undefined,
}},
watch: {
input(value){
this.input = value;
this.inputHint = this.inputError = undefined;
if (!this.input) return;
let result;
try {
result = parse(value);
} catch (e){
console.error(e);
this.inputError = 'Invalid syntax';
return;
}
if (result === null){
this.inputError = '...';
return;
}
try {
let compiled = result.compile(this.creature.variables);
this.inputHint = compiled.toString();
return;
} catch (e){
console.error(e);
this.inputError = 'Compilation error';
return;
}
},
},
methods: {
submit(){
if (this.inputError || !this.input) return;
logRoll.call({
roll: this.input,
creatureId: this.creatureId,
}, (error) => {
if (error) console.error(error);
});
this.input = '';
},
},
meteor: {
logs(){
return CreatureLogs.find({
creatureId: this.creatureId,
}, {
sort: {date: -1},
limit: 20
});
},
creature(){
return Creatures.findOne(this.creatureId) || {};
},
editPermission(){
try {
assertEditPermission(this.creature, Meteor.userId());
return true;
} catch (e) {
return false;
}
},
},
}
</script>
<style lang="css">
.log-tab p:last-child {
margin-bottom: 0;
}
</style>

View File

@@ -50,7 +50,7 @@
<spells-tab :creature-id="creatureId" />
</v-tab-item>
<v-tab-item>
<persona-tab :creature-id="creatureId" />
<character-tab :creature-id="creatureId" />
</v-tab-item>
<v-tab-item>
<tree-tab :creature-id="creatureId" />
@@ -69,9 +69,10 @@
import FeaturesTab from '/imports/ui/creature/character/characterSheetTabs/FeaturesTab.vue';
import InventoryTab from '/imports/ui/creature/character/characterSheetTabs/InventoryTab.vue';
import SpellsTab from '/imports/ui/creature/character/characterSheetTabs/SpellsTab.vue';
import PersonaTab from '/imports/ui/creature/character/characterSheetTabs/PersonaTab.vue';
import TreeTab from '/imports/ui/creature/character/characterSheetTabs/TreeTab.vue';
import CharacterTab from '/imports/ui/creature/character/characterSheetTabs/CharacterTab.vue';
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';
export default {
components: {
@@ -79,7 +80,7 @@
FeaturesTab,
InventoryTab,
SpellsTab,
PersonaTab,
CharacterTab,
TreeTab,
},
props: {
@@ -96,14 +97,6 @@
name: 'context',
include: ['creature', 'editPermission'],
},
onMounted(){
this.$store.commit('setPageTitle', this.creature && this.creature.name || 'Character Sheet');
},
watch: {
'creature.name'(value){
this.$store.commit('setPageTitle', value || 'Character Sheet');
},
},
computed: {
activeTab: {
get(){
@@ -113,6 +106,30 @@
this.$emit('update:tabs', newTab);
},
},
},
watch: {
'creature.name'(value){
this.$store.commit('setPageTitle', value || 'Character Sheet');
},
},
mounted(){
this.$store.commit('setPageTitle', this.creature && this.creature.name || 'Character Sheet');
let that = this;
this.logObserver = CreatureLogs.find({
creatureId: this.creatureId,
}).observe({
added(doc){
if (!that.$subReady.singleCharacter) return;
if (that.$store.state.rightDrawer) return;
that.$store.dispatch('snackbar', {
text: doc.text,
showCloseButton: true,
});
},
});
},
beforeDestroy(){
this.logObserver.stop();
},
meteor: {
$subscribe: {
@@ -131,6 +148,9 @@
return false;
}
},
snackbars(){
}
},
}
</script>

View File

@@ -0,0 +1,32 @@
<template lang="html">
<v-navigation-drawer
v-model="drawer"
app
right
clipped
>
<log-tab :creature-id="$route.params.id" />
</v-navigation-drawer>
</template>
<script>
import LogTab from '/imports/ui/creature/character/CharacterLog.vue';
export default {
components: {
LogTab,
},
computed: {
drawer: {
get () {
return this.$store.state.rightDrawer;
},
set (value) {
this.$store.commit('setRightDrawer', value);
},
},
},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -24,14 +24,6 @@
>
<div :key="$route.meta.title">
<v-toolbar-items v-if="creature">
<v-btn
v-if="editPermission"
flat
icon
@click="recompute(creature._id)"
>
<v-icon>refresh</v-icon>
</v-btn>
<v-menu
bottom
left
@@ -71,6 +63,7 @@
</v-list-tile>
</v-list>
</v-menu>
<v-toolbar-side-icon @click="toggleRightDrawer" />
</v-toolbar-items>
</div>
</v-fade-transition>
@@ -104,7 +97,7 @@
Spells
</v-tab>
<v-tab>
Persona
Character
</v-tab>
<v-tab>
Tree
@@ -120,7 +113,6 @@ import Creatures from '/imports/api/creature/Creatures.js';
import removeCreature from '/imports/api/creature/removeCreature.js';
import { mapMutations } from 'vuex';
import { theme } from '/imports/ui/theme.js';
import { recomputeCreature } from '/imports/api/creature/computation/recomputeCreature.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { updateUserSharePermissions } from '/imports/api/sharing/sharing.js';
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
@@ -153,10 +145,8 @@ export default {
methods: {
...mapMutations([
'toggleDrawer',
'toggleRightDrawer',
]),
recompute(charId){
recomputeCreature.call({charId});
},
showCharacterForm(){
this.$store.commit('pushDialogStack', {
component: 'creature-form-dialog',

View File

@@ -22,7 +22,7 @@
Spells
</v-tab>
<v-tab>
Persona
Character
</v-tab>
<v-tab>
Tree

View File

@@ -20,6 +20,19 @@
</v-card-text>
</v-card>
</div>
<div>
<toolbar-card
data-id="slot-card"
@toolbarclick="showSlotDialog"
>
<v-toolbar-title slot="toolbar">
Build
</v-toolbar-title>
<v-card-text style="background-color: inherit;">
<slots :creature-id="creatureId" />
</v-card-text>
</toolbar-card>
</div>
<div>
<v-card class="class-details">
<v-card-title
@@ -101,12 +114,16 @@ import Creatures from '/imports/api/creature/Creatures.js';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import NoteCard from '/imports/ui/properties/components/persona/NoteCard.vue';
import getActiveProperties from '/imports/api/creature/getActiveProperties.js'
import getActiveProperties from '/imports/api/creature/getActiveProperties.js';
import Slots from '/imports/ui/creature/slots/Slots.vue';
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
export default {
components: {
ColumnLayout,
NoteCard,
Slots,
ToolbarCard,
},
props: {
creatureId: {
@@ -185,6 +202,15 @@ export default {
!!this.creature.variables.milestoneLevels.value,
},
});
},
showSlotDialog(){
this.$store.commit('pushDialogStack', {
component: 'slot-details-dialog',
elementId: 'slot-card',
data: {
creatureId: this.creatureId,
},
});
},
},
};

View File

@@ -12,7 +12,7 @@
<item-list
equipment
:items="equippedItems"
:parent-ref="{id: creatureId, collection: 'creatures'}"
:parent-ref="equipmentParentRef"
/>
</v-card-text>
</toolbar-card>
@@ -27,7 +27,7 @@
<v-card-text class="px-0">
<item-list
:items="carriedItems"
:parent-ref="{id: creatureId, collection: 'creatures'}"
:parent-ref="carriedParentRef"
/>
</v-card-text>
</toolbar-card>
@@ -51,7 +51,8 @@ import ContainerCard from '/imports/ui/properties/components/inventory/Container
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
import ItemList from '/imports/ui/properties/components/inventory/ItemList.vue';
import { updateProperty } from '/imports/api/creature/CreatureProperties.js';
import getActiveProperties from '/imports/api/creature/getActiveProperties.js';
import { getParentRefByTag } from '/imports/api/creature/creatureProperties/manageEquipment.js';
import INVENTORY_TAGS from '/imports/constants/INVENTORY_TAGS.js';
export default {
components: {
@@ -78,6 +79,7 @@ export default {
'ancestors.id': this.creatureId,
type: 'container',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1},
});
@@ -90,29 +92,43 @@ export default {
},
type: 'container',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1},
});
},
carriedItems(){
return getActiveProperties({
ancestorId: this.creatureId,
includeUnequipped: true,
filter: {
type: 'item',
equipped: {$ne: true},
'parent.id': this.creatureId
},
});
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
$nin: this.containerIds
},
type: 'item',
equipped: {$ne: true},
removed: {$ne: true},
deactivatedByAncestor: {$ne: true},
}, {
sort: {order: 1},
});
},
equippedItems(){
return getActiveProperties({
ancestorId: this.creatureId,
filter: {
type: 'item',
equipped: true,
},
});
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
},
type: 'item',
equipped: true,
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1},
});
},
equipmentParentRef(){
return getParentRefByTag(this.creatureId, INVENTORY_TAGS.equipment);
},
carriedParentRef(){
return getParentRefByTag(this.creatureId, INVENTORY_TAGS.carried);
},
},
computed: {

View File

@@ -79,7 +79,7 @@
class="stat"
>
<attribute-card
v-bind="stat"
:model="stat"
:data-id="stat._id"
@click="clickProperty({_id: stat._id})"
/>
@@ -91,8 +91,7 @@
class="modifier"
>
<attribute-card
modifier
v-bind="modifier"
:model="modifier"
:data-id="modifier._id"
@click="clickProperty({_id: modifier._id})"
/>
@@ -105,7 +104,7 @@
>
<attribute-card
modifier
v-bind="check"
:model="check"
:data-id="check._id"
@click="clickProperty({_id: check._id})"
/>

View File

@@ -60,12 +60,13 @@
<script>
import CreatureProperties, {
updateProperty,
damageProperty,
updateProperty,
damageProperty,
duplicateProperty,
pushToProperty,
pullFromProperty,
softRemoveProperty,
pushToProperty,
pullFromProperty,
softRemoveProperty,
restoreProperty,
} from '/imports/api/creature/CreatureProperties.js';
import Creatures from '/imports/api/creature/Creatures.js';
import PropertyToolbar from '/imports/ui/components/propertyToolbar.vue';
@@ -75,40 +76,42 @@ import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormIndex.js';
import propertyViewerIndex from '/imports/ui/properties/viewers/shared/propertyViewerIndex.js';
import CreaturePropertiesTree from '/imports/ui/creature/creatureProperties/CreaturePropertiesTree.vue';
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
import { get, findLast } from 'lodash';
import { equipItem } from '/imports/api/creature/creatureProperties/manageEquipment.js';
let formIndex = {};
for (let key in propertyFormIndex){
formIndex[key + 'Form'] = propertyFormIndex[key];
formIndex[key + 'Form'] = propertyFormIndex[key];
}
let viewerIndex = {};
for (let key in propertyViewerIndex){
formIndex[key + 'Viewer'] = propertyViewerIndex[key];
formIndex[key + 'Viewer'] = propertyViewerIndex[key];
}
export default {
components: {
...formIndex,
...viewerIndex,
PropertyIcon,
DialogBase,
components: {
...formIndex,
...viewerIndex,
PropertyIcon,
DialogBase,
PropertyToolbar,
CreaturePropertiesTree,
},
props: {
_id: String,
CreaturePropertiesTree,
},
props: {
_id: String,
embedded: Boolean, // This dialog is embedded in a page
startInEditTab: Boolean,
},
data(){ return {
editing: !!this.startInEditTab,
}},
meteor: {
model(){
return CreatureProperties.findOne(this._id);
},
startInEditTab: Boolean,
},
data(){ return {
editing: !!this.startInEditTab,
}},
meteor: {
model(){
return CreatureProperties.findOne(this._id);
},
editPermission(){
try {
assertEditPermission(this.creature, Meteor.userId());
@@ -117,7 +120,7 @@ export default {
return false;
}
},
},
},
computed: {
creature(){
if (!this.model) return;
@@ -133,8 +136,8 @@ export default {
name: 'context',
include: ['creature', 'editPermission'],
},
methods: {
getPropertyName,
methods: {
getPropertyName,
duplicate(){
duplicateProperty.call({_id: this._id}, (error) => {
if (error) {
@@ -147,48 +150,63 @@ export default {
}
});
},
change({path, value, ack}){
updateProperty.call({_id: this._id, path, value}, (error) =>{
change({path, value, ack}){
if (path && path[0] === 'equipped'){
equipItem.call({_id: this._id, equipped: value}, (error) =>{
if (error) console.warn(error);
ack && ack(error && error.reason || error);
});
return;
}
updateProperty.call({_id: this._id, path, value}, (error) =>{
if (error) console.warn(error);
ack && ack(error && error.reason || error);
});
},
ack && ack(error && error.reason || error);
});
},
damage({operation, value, ack}){
damageProperty.call({_id: this._id, operation, value}, (error) =>{
damageProperty.call({_id: this._id, operation, value}, (error) =>{
if (error) console.warn(error);
ack && ack(error && error.reason || error);
});
},
push({path, value, ack}){
pushToProperty.call({_id: this._id, path, value}, (error) =>{
ack && ack(error && error.reason || error);
});
},
push({path, value, ack}){
pushToProperty.call({_id: this._id, path, value}, (error) =>{
if (error) console.warn(error);
ack && ack(error && error.reason || error);
});
},
pull({path, ack}){
let itemId = get(this.model, path)._id;
path.pop();
pullFromProperty.call({_id: this._id, path, itemId}, (error) =>{
ack && ack(error && error.reason || error);
});
},
pull({path, ack}){
let itemId = get(this.model, path)._id;
path.pop();
pullFromProperty.call({_id: this._id, path, itemId}, (error) =>{
if (error) console.warn(error);
ack && ack(error && error.reason || error);
});
},
remove(){
softRemoveProperty.call({_id: this._id});
ack && ack(error && error.reason || error);
});
},
remove(){
const _id = this._id;
softRemoveProperty.call({_id});
if (this.embedded){
this.$emit('removed');
} else {
this.$store.dispatch('popDialogStack');
}
},
selectSubProperty(_id){
this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog',
elementId: `tree-node-${_id}`,
data: {_id},
});
}
}
this.$store.dispatch('snackbar', {
text: `Deleted ${getPropertyTitle(this.model)}`,
callbackName: 'undo',
callback(){
restoreProperty.call({_id});
},
});
},
selectSubProperty(_id){
this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog',
elementId: `tree-node-${_id}`,
data: {_id},
});
}
}
};
</script>

View File

@@ -1,5 +1,8 @@
<template lang="html">
<dialog-base>
<v-toolbar-title slot="toolbar">
Add Experience
</v-toolbar-title>
<experience-form
:start-as-milestone="startAsMilestone"
:model="model"

View File

@@ -0,0 +1,42 @@
<template lang="html">
<dialog-base>
<v-toolbar-title slot="toolbar">
Build
</v-toolbar-title>
<slots
:creature-id="creatureId"
show-hidden-slots
/>
</dialog-base>
</template>
<script>
import Creatures from '/imports/api/creature/Creatures.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import Slots from '/imports/ui/creature/slots/Slots.vue'
export default {
components: {
DialogBase,
Slots,
},
props: {
creatureId: {
type: String,
required: true,
},
},
reactiveProvide: {
name: 'context',
include: ['creature'],
},
meteor: {
creature(){
return Creatures.findOne(this.creatureId);
},
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,214 @@
<template lang="html">
<dialog-base :color="model.color">
<template slot="toolbar">
<v-toolbar-title>
{{ model.name }}
</v-toolbar-title>
</template>
<div class="library-nodes">
<v-fade-transition mode="out-in">
<column-layout
v-if="$subReady.slotFillers && libraryNodes && libraryNodes.length"
wide-columns
>
<div
v-for="node in libraryNodes"
:key="node._id"
>
<v-card
style="max-width: 500px;"
hover
ripple
:class="{'primary': node._id === (selectedNode && selectedNode._id)}"
:dark="node._id === (selectedNode && selectedNode._id)"
@click="selectedNode = node"
>
<v-img
v-if="node.picture"
:src="node.picture"
:max-height="200"
position="top center"
/>
<v-card-title primary-title>
<div>
<h3 class="title mb-0">
<tree-node-view
class="mr-2"
:class="{'theme--dark': node._id === (selectedNode && selectedNode._id)}"
:hide-icon="node.picture"
:model="node"
:color="node.color"
/>
</h3>
<property-description
v-if="node.description"
:value="node.description"
/>
</div>
</v-card-title>
</v-card>
</div>
</column-layout>
<div
v-else-if="$subReady.slotFillers"
class="ma-4"
>
<h4>
Nothing suitable was found in your libraries
</h4>
<p>
This slot requires a {{ slotPropertyTypeName }}
<template v-if="model.slotTags.length">
with the following tags:
<span
v-for="(tag, index) in model.slotTags"
:key="index"
>
<code>{{ tag }}</code>,
</span>
</template>
<span v-if="model.spaceLeft">
that fills less than {{ model.spaceLeft }} slots
</span>
</p>
</div>
<div
v-else
key="character-loading"
class="fill-height layout justify-center align-center"
>
<v-progress-circular
indeterminate
color="primary"
size="64"
/>
</div>
</v-fade-transition>
</div>
<template slot="actions">
<v-spacer />
<v-btn
flat
@click="$store.dispatch('popDialogStack')"
>
Cancel
</v-btn>
<v-btn
flat
:disabled="!selectedNode"
@click="$store.dispatch('popDialogStack', selectedNode)"
>
Insert
</v-btn>
</template>
</dialog-base>
</template>
<script>
import Creatures from '/imports/api/creature/Creatures.js';
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import { parse, CompilationContext } from '/imports/parser/parser.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js';
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue'
export default {
components: {
DialogBase,
ColumnLayout,
TreeNodeView,
PropertyDescription,
},
props:{
slotId: {
type: String,
required: true,
},
creatureId: {
type: String,
required: true,
},
},
data(){return {
selectedNode: undefined,
}},
computed: {
slotPropertyTypeName(){
if (!this.model) return;
if (!this.model.slotType) return 'property';
let propName = getPropertyName(this.model.slotType);
return propName && propName.toLowerCase();
},
},
reactiveProvide: {
name: 'context',
include: ['creature'],
},
methods:{
getTitle(model){
if (!model) return;
if (model.name) return model.name;
let prop = PROPERTIES[model.type]
return prop && prop.name;
},
},
meteor: {
$subscribe: {
'slotFillers'(){
return [this.slotId]
},
},
model(){
return CreatureProperties.findOne(this.slotId);
},
creature(){
return Creatures.findOne(this.creatureId);
},
libraryNodes(){
let filter = {
removed: {$ne: true},
};
if (this.model.slotTags && this.model.slotTags.length){
filter.tags = {$all: this.model.slotTags};
}
if (this.model.slotType){
filter.$or = [{
type: this.model.slotType
},{
type: 'slotFiller',
slotFillerType: this.model.slotType,
}];
}
let nodes = LibraryNodes.find(filter).fetch();
// Filter out slotFillers whose condition isn't met or are too big to fit
// the quantity to fill
nodes = nodes.filter(node => {
if (node.slotFillerCondition){
let context = new CompilationContext();
let conditionResult = parse(node.slotFillerCondition)
.reduce(this.creature.variables, context);
if (conditionResult && !conditionResult.value) return false;
}
if (
node.type === 'slotFiller' &&
this.model.spaceLeft > 0 &&
node.slotQuantityFilled > this.model.spaceLeft
){
return false;
}
return true;
});
// Filter out slotFillers whose
if (nodes.length === 1) this.selectedNode = nodes[0];
return nodes;
},
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,162 @@
<template lang="html">
<div class="slots">
<div
v-for="slot in slots"
:key="slot._id"
class="slot"
>
<h3 class="layout row align-center">
{{ slot.name }}
<v-spacer />
<span v-if="slot.quantityExpected > 1">
{{ slot.totalFilled }} / {{ slot.quantityExpected }}
</span>
</h3>
<v-list v-if="slot.children.length">
<v-list-tile
v-for="child in slot.children"
:key="child._id"
:data-id="`slot-child-${child._id}`"
@click="clickSlotChild(child)"
>
<v-list-tile-content>
<tree-node-view
class="slotChild"
:model="child"
/>
</v-list-tile-content>
<v-list-tile-action>
<v-btn
icon
flat
small
@click.stop="remove(child)"
>
<v-icon>delete</v-icon>
</v-btn>
</v-list-tile-action>
</v-list-tile>
</v-list>
<v-btn
v-if="!slot.quantityExpected || slot.spaceLeft"
icon
:data-id="`slot-add-button-${slot._id}`"
style="background-color: inherit;"
@click="fillSlot(slot)"
>
<v-icon>add</v-icon>
</v-btn>
</div>
</div>
</template>
<script>
import CreatureProperties from '/imports/api/creature/CreatureProperties.js';
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
import {
insertPropertyFromLibraryNode,
softRemoveProperty,
restoreProperty
} from '/imports/api/creature/CreatureProperties.js';
import getActiveProperties from '/imports/api/creature/getActiveProperties.js';
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
export default {
components: {
TreeNodeView,
},
inject: {
context: { default: {} }
},
props: {
creatureId: {
type: String,
required: true,
},
showHiddenSlots: {
type: Boolean,
},
},
methods: {
clickSlotChild({_id}){
this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog',
elementId: `slot-child-${_id}`,
data: {_id},
});
},
fillSlot(slot){
let slotId = slot._id;
let creatureId = this.creatureId;
this.$store.commit('pushDialogStack', {
component: 'slot-fill-dialog',
elementId: `slot-add-button-${slotId}`,
data: {
slotId,
creatureId,
},
callback(node){
if(!node) return;
let newPropertyId = insertPropertyFromLibraryNode.call({
nodeId: node._id,
parentRef: {
'id': slotId,
'collection': 'creatureProperties',
},
});
return `slot-child-${newPropertyId}`;
}
});
},
remove(model){
softRemoveProperty.call({_id: model._id});
this.$store.dispatch('snackbar', {
text: `Deleted ${getPropertyTitle(model)}`,
callbackName: 'undo',
callback(){
restoreProperty.call({_id: model._id});
},
});
}
},
meteor: {
slots(){
return getActiveProperties({
ancestorId: this.creatureId,
filter: {
type: 'propertySlot',
$or: [
{slotConditionResult: true},
{slotConditionResult: {$exists: false}},
],
}
}).map(slot => {
if (
!this.showHiddenSlots &&
slot.quantityExpected === 0 &&
slot.hideWhenFull
){
slot.children = []
} else {
slot.children = CreatureProperties.find({
'parent.id': slot._id,
removed: {$ne: true},
}, {
sort: { order: 1 },
}).fetch();
}
return slot;
}).filter(slot => !( // Hide full and ignored slots
!this.showHiddenSlots &&
slot.hideWhenFull &&
slot.quantityExpected > 0 &&
slot.totalFilled >= slot.quantityExpected ||
slot.ignored
));
},
},
}
</script>
<style lang="css" scoped>
</style>

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