Compare commits
52 Commits
2.0-beta.4
...
2.0-beta.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
002c767d1a | ||
|
|
9aaf31d5cf | ||
|
|
d05cd2fa19 | ||
|
|
f13774df11 | ||
|
|
cc78ba948e | ||
|
|
c6bfb84bb0 | ||
|
|
7e49100d14 | ||
|
|
c3ac49a8c4 | ||
|
|
fd9d525ba9 | ||
|
|
d947b62ba4 | ||
|
|
046509224e | ||
|
|
63bcf023ee | ||
|
|
9741d1d56c | ||
|
|
0f12c98408 | ||
|
|
254390e1c1 | ||
|
|
ad2f43712d | ||
|
|
55f8dac0db | ||
|
|
9f8c3f0f3d | ||
|
|
56bd41f435 | ||
|
|
063d4288ef | ||
|
|
a3355dd988 | ||
|
|
d2649fd66e | ||
|
|
e619734ee1 | ||
|
|
5108b32624 | ||
|
|
a9b389023e | ||
|
|
e06d2befc4 | ||
|
|
cc7dc257fb | ||
|
|
f3deadb3f1 | ||
|
|
dcfb380e57 | ||
|
|
a568cdfb1e | ||
|
|
cea63e6a8e | ||
|
|
b6b0cfbb9b | ||
|
|
428aeef635 | ||
|
|
e3644eb9e8 | ||
|
|
060b5f93ca | ||
|
|
0f3a96da17 | ||
|
|
c55d572134 | ||
|
|
0a2b60990e | ||
|
|
a437ff5aef | ||
|
|
3d31d62860 | ||
|
|
8377231254 | ||
|
|
1ec29365cb | ||
|
|
60b21c1901 | ||
|
|
03f87b0afa | ||
|
|
48291d2c8f | ||
|
|
1cedf55fbf | ||
|
|
bed4d4b162 | ||
|
|
a1d992ec8d | ||
|
|
008ef62517 | ||
|
|
c436309ba8 | ||
|
|
0bfdb73b47 | ||
|
|
a462cc5ca2 |
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
.demeteorized
|
||||
.cache
|
||||
.vscode
|
||||
fileStorage
|
||||
settings.json
|
||||
public/components
|
||||
public/_imports.html
|
||||
|
||||
@@ -4,27 +4,27 @@
|
||||
# but you can also edit it by hand.
|
||||
|
||||
accounts-password@2.3.1
|
||||
random@1.2.0
|
||||
underscore@1.0.10
|
||||
random@1.2.1
|
||||
underscore@1.0.11
|
||||
dburles:mongo-collection-instances
|
||||
accounts-google@1.4.0
|
||||
email@2.2.1
|
||||
email@2.2.2
|
||||
meteor-base@1.5.1
|
||||
mobile-experience@1.1.0
|
||||
mongo@1.16.0
|
||||
session@1.2.0
|
||||
tracker@1.2.0
|
||||
mongo@1.16.1
|
||||
session@1.2.1
|
||||
tracker@1.2.1
|
||||
logging@1.3.1
|
||||
reload@1.3.1
|
||||
ejson@1.1.2
|
||||
check@1.3.1
|
||||
ejson@1.1.3
|
||||
check@1.3.2
|
||||
standard-minifier-js@2.8.1
|
||||
shell-server@0.5.0
|
||||
ecmascript@0.16.2
|
||||
ecmascript@0.16.3
|
||||
es5-shim@4.8.0
|
||||
service-configuration@1.3.0
|
||||
service-configuration@1.3.1
|
||||
dynamic-import@0.7.2
|
||||
ddp-rate-limiter@1.1.0
|
||||
ddp-rate-limiter@1.1.1
|
||||
rate-limit@1.0.9
|
||||
mdg:validated-method
|
||||
static-html@1.3.2
|
||||
@@ -37,7 +37,6 @@ simple:rest
|
||||
simple:rest-method-mixin
|
||||
mikowals:batch-insert
|
||||
peerlibrary:subscription-data
|
||||
seba:minifiers-autoprefixer
|
||||
zer0th:meteor-vuetify-loader
|
||||
akryum:vue-component
|
||||
akryum:vue-router2
|
||||
@@ -49,3 +48,4 @@ simple:rest-json-error-handler
|
||||
littledata:synced-cron
|
||||
mdg:meteor-apm-agent
|
||||
typescript@4.5.4
|
||||
seba:minifiers-autoprefixer
|
||||
|
||||
@@ -1 +1 @@
|
||||
METEOR@2.8.0
|
||||
METEOR@2.8.1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
accounts-base@2.2.4
|
||||
accounts-base@2.2.5
|
||||
accounts-google@1.4.0
|
||||
accounts-oauth@1.4.1
|
||||
accounts-password@2.3.1
|
||||
@@ -22,26 +22,26 @@ bozhao:link-accounts@2.6.1
|
||||
caching-compiler@1.2.2
|
||||
caching-html-compiler@1.2.1
|
||||
callback-hook@1.4.0
|
||||
check@1.3.1
|
||||
check@1.3.2
|
||||
coffeescript@2.4.1
|
||||
coffeescript-compiler@2.4.1
|
||||
dburles:mongo-collection-instances@0.3.6
|
||||
ddp@1.4.0
|
||||
ddp-client@2.6.0
|
||||
ddp@1.4.1
|
||||
ddp-client@2.6.1
|
||||
ddp-common@1.4.0
|
||||
ddp-rate-limiter@1.1.0
|
||||
ddp-rate-limiter@1.1.1
|
||||
ddp-server@2.6.0
|
||||
diff-sequence@1.1.1
|
||||
diff-sequence@1.1.2
|
||||
dynamic-import@0.7.2
|
||||
ecmascript@0.16.2
|
||||
ecmascript@0.16.3
|
||||
ecmascript-runtime@0.8.0
|
||||
ecmascript-runtime-client@0.12.1
|
||||
ecmascript-runtime-server@0.11.0
|
||||
ejson@1.1.2
|
||||
email@2.2.1
|
||||
ejson@1.1.3
|
||||
email@2.2.2
|
||||
es5-shim@4.8.0
|
||||
fetch@0.1.1
|
||||
geojson-utils@1.0.10
|
||||
fetch@0.1.2
|
||||
geojson-utils@1.0.11
|
||||
google-oauth@1.4.2
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.1.3
|
||||
@@ -57,7 +57,7 @@ localstorage@1.2.0
|
||||
logging@1.3.1
|
||||
mdg:meteor-apm-agent@3.5.1
|
||||
mdg:validated-method@1.2.0
|
||||
meteor@1.10.1
|
||||
meteor@1.10.2
|
||||
meteor-base@1.5.1
|
||||
meteortesting:browser-tests@1.3.5
|
||||
meteortesting:mocha@2.0.3
|
||||
@@ -68,20 +68,20 @@ minifier-js@2.7.5
|
||||
minimongo@1.9.0
|
||||
mobile-experience@1.1.0
|
||||
mobile-status-bar@1.1.0
|
||||
modern-browsers@0.1.8
|
||||
modern-browsers@0.1.9
|
||||
modules@0.19.0
|
||||
modules-runtime@0.13.0
|
||||
mongo@1.16.0
|
||||
modules-runtime@0.13.1
|
||||
mongo@1.16.1
|
||||
mongo-decimal@0.1.3
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.8
|
||||
mongo-livedata@1.0.12
|
||||
npm-mongo@4.9.0
|
||||
npm-mongo@4.11.0
|
||||
oauth@2.1.2
|
||||
oauth2@1.3.1
|
||||
ordered-dict@1.1.0
|
||||
ostrio:cookies@2.7.2
|
||||
ostrio:files@2.3.0
|
||||
ostrio:files@2.3.2
|
||||
patreon-oauth@0.1.0
|
||||
peerlibrary:assert@0.3.0
|
||||
peerlibrary:check-extension@0.7.0
|
||||
@@ -93,20 +93,20 @@ peerlibrary:reactive-mongo@0.4.1
|
||||
peerlibrary:reactive-publish@0.10.0
|
||||
peerlibrary:server-autorun@0.8.0
|
||||
peerlibrary:subscription-data@0.8.0
|
||||
percolate:migrations@1.0.3
|
||||
promise@0.12.0
|
||||
percolate:migrations@1.1.0
|
||||
promise@0.12.1
|
||||
raix:eventemitter@1.0.0
|
||||
random@1.2.0
|
||||
random@1.2.1
|
||||
rate-limit@1.0.9
|
||||
react-fast-refresh@0.2.3
|
||||
reactive-dict@1.3.0
|
||||
reactive-var@1.0.11
|
||||
reactive-dict@1.3.1
|
||||
reactive-var@1.0.12
|
||||
reload@1.3.1
|
||||
retry@1.1.0
|
||||
routepolicy@1.1.1
|
||||
seba:minifiers-autoprefixer@2.0.1
|
||||
service-configuration@1.3.0
|
||||
session@1.2.0
|
||||
service-configuration@1.3.1
|
||||
session@1.2.1
|
||||
sha@1.0.9
|
||||
shell-server@0.5.0
|
||||
simple:json-routes@2.3.1
|
||||
@@ -120,10 +120,10 @@ standard-minifier-js@2.8.1
|
||||
static-html@1.3.2
|
||||
templating-tools@1.2.2
|
||||
tmeasday:check-npm-versions@1.0.2
|
||||
tracker@1.2.0
|
||||
tracker@1.2.1
|
||||
typescript@4.5.4
|
||||
underscore@1.0.10
|
||||
underscore@1.0.11
|
||||
url@1.3.2
|
||||
webapp@1.13.1
|
||||
webapp-hashing@1.1.0
|
||||
webapp@1.13.2
|
||||
webapp-hashing@1.1.1
|
||||
zer0th:meteor-vuetify-loader@0.1.41
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import '/imports/api/simpleSchemaConfig.js';
|
||||
import '/imports/ui/vueSetup.js';
|
||||
import '/imports/ui/styles/stylesIndex.js';
|
||||
import '/imports/client/ui/vueSetup.js';
|
||||
import '/imports/client/ui/styles/stylesIndex.js';
|
||||
import '/imports/client/config.js';
|
||||
import '/imports/client/serviceWorker.js';
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ import { CreatureSchema } from '/imports/api/creature/creatures/Creatures.js';
|
||||
|
||||
const ArchiveCreatureFiles = createS3FilesCollection({
|
||||
collectionName: 'archiveCreatureFiles',
|
||||
storagePath: Meteor.isDevelopment ? '/DiceCloud/archiveCreatures/' : 'assets/app/archiveCreatures',
|
||||
storagePath: Meteor.isDevelopment ? '../../../../../fileStorage/archiveCreatures' : 'assets/app/archiveCreatures',
|
||||
onBeforeUpload(file) {
|
||||
// Allow upload files under 10MB, and only in json format
|
||||
if (file.size > 10485760) {
|
||||
return 'Please upload with size equal or less than 10MB';
|
||||
}
|
||||
if (!/json/i.test(file.extension)){
|
||||
if (!/json/i.test(file.extension)) {
|
||||
return 'Please upload only a JSON file';
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -59,7 +59,7 @@ const damageProperty = new ValidatedMethod({
|
||||
},
|
||||
});
|
||||
|
||||
export function damagePropertyWork({ prop, operation, value, actionContext }) {
|
||||
export function damagePropertyWork({ prop, operation, value, actionContext, logFunction }) {
|
||||
|
||||
// Save the value to the scope before applying the before triggers
|
||||
if (operation === 'increment') {
|
||||
@@ -105,6 +105,7 @@ export function damagePropertyWork({ prop, operation, value, actionContext }) {
|
||||
// Also write it straight to the prop so that it is updated in the actionContext
|
||||
prop.damage = damage;
|
||||
prop.value = newValue;
|
||||
logFunction?.(newValue);
|
||||
} else if (operation === 'increment') {
|
||||
let currentValue = prop.value || 0;
|
||||
let currentDamage = prop.damage || 0;
|
||||
@@ -125,6 +126,7 @@ export function damagePropertyWork({ prop, operation, value, actionContext }) {
|
||||
// Also write it straight to the prop so that it is updated in the actionContext
|
||||
prop.damage += increment;
|
||||
prop.value -= increment;
|
||||
logFunction?.(increment);
|
||||
}
|
||||
|
||||
applyTriggers(actionContext.triggers?.damageProperty?.after, prop, actionContext);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { reorderDocs } from '/imports/api/parenting/order.js';
|
||||
var snackbar;
|
||||
if (Meteor.isClient) {
|
||||
snackbar = require(
|
||||
'/imports/ui/components/snackbars/SnackbarQueue.js'
|
||||
'/imports/client/ui/components/snackbars/SnackbarQueue.js'
|
||||
).snackbar
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,11 @@ let CreatureSettingsSchema = new SimpleSchema({
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
//hide rest buttons
|
||||
hideRestButtons: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// Swap around the modifier and stat
|
||||
swapStatAndModifier: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { assertEditPermission } from '/imports/api/creature/creatures/creaturePe
|
||||
import { union } from 'lodash';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
|
||||
const restCreature = new ValidatedMethod({
|
||||
name: 'creature.methods.rest',
|
||||
@@ -49,7 +50,7 @@ const restCreature = new ValidatedMethod({
|
||||
applyTriggers(afterTriggers, null, actionContext);
|
||||
|
||||
// Insert log
|
||||
actionContext.writeLog();
|
||||
actionContext.writeLog();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -57,88 +58,113 @@ function doRestWork(restType, actionContext) {
|
||||
const creatureId = actionContext.creature._id;
|
||||
// Long rests reset short rest properties as well
|
||||
let resetFilter;
|
||||
if (restType === 'shortRest'){
|
||||
if (restType === 'shortRest') {
|
||||
resetFilter = 'shortRest'
|
||||
} else {
|
||||
resetFilter = {$in: ['shortRest', 'longRest']}
|
||||
resetFilter = { $in: ['shortRest', 'longRest'] }
|
||||
}
|
||||
resetProperties(creatureId, resetFilter, actionContext);
|
||||
|
||||
// Reset half hit dice on a long rest, starting with the highest dice
|
||||
if (restType === 'longRest') {
|
||||
resetHitDice(creatureId, actionContext);
|
||||
}
|
||||
}
|
||||
|
||||
export function resetProperties(creatureId, resetFilter, actionContext) {
|
||||
// Only apply to active properties
|
||||
let filter = {
|
||||
const filter = {
|
||||
'ancestors.id': creatureId,
|
||||
reset: resetFilter,
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
};
|
||||
// update all attribute's damage
|
||||
filter.type = 'attribute';
|
||||
CreatureProperties.update(filter, {
|
||||
$set: {
|
||||
damage: 0,
|
||||
dirty: true,
|
||||
}
|
||||
}, {
|
||||
selector: {type: 'attribute'},
|
||||
multi: true,
|
||||
const attributeFilter = {
|
||||
...filter,
|
||||
type: 'attribute',
|
||||
damage: { $ne: 0 },
|
||||
}
|
||||
CreatureProperties.find(attributeFilter).forEach(prop => {
|
||||
damagePropertyWork({
|
||||
prop,
|
||||
operation: 'increment',
|
||||
value: -prop.damage,
|
||||
actionContext,
|
||||
logFunction(increment) {
|
||||
actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: increment < 0 ? `Restored ${-increment}` : `Removed ${-increment}`
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
// Update all action-like properties' usesUsed
|
||||
filter.type = {$in: [
|
||||
'action',
|
||||
'attack',
|
||||
'spell'
|
||||
]};
|
||||
CreatureProperties.update(filter, {
|
||||
const actionFilter = {
|
||||
...filter,
|
||||
type: {
|
||||
$in: ['action', 'spell']
|
||||
},
|
||||
usesUsed: { $ne: 0 },
|
||||
};
|
||||
CreatureProperties.find(actionFilter, {
|
||||
fields: { name: 1, usesUsed: 1 }
|
||||
}).forEach(prop => {
|
||||
actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: prop.usesUsed >= 0 ? `Restored ${prop.usesUsed} uses` : `Removed ${-prop.usesUsed} uses`
|
||||
});
|
||||
});
|
||||
CreatureProperties.update(actionFilter, {
|
||||
$set: {
|
||||
usesUsed: 0,
|
||||
dirty: true,
|
||||
}
|
||||
}, {
|
||||
selector: {type: 'action'},
|
||||
selector: { type: 'action' },
|
||||
multi: true,
|
||||
});
|
||||
// Reset half hit dice on a long rest, starting with the highest dice
|
||||
if (restType === 'longRest'){
|
||||
let hitDice = CreatureProperties.find({
|
||||
'ancestors.id': creatureId,
|
||||
type: 'attribute',
|
||||
attributeType: 'hitDice',
|
||||
removed: {$ne: true},
|
||||
inactive: {$ne: true},
|
||||
}, {
|
||||
fields: {
|
||||
hitDiceSize: 1,
|
||||
damage: 1,
|
||||
total: 1,
|
||||
}
|
||||
|
||||
function resetHitDice(creatureId, actionContext) {
|
||||
let hitDice = CreatureProperties.find({
|
||||
'ancestors.id': creatureId,
|
||||
type: 'attribute',
|
||||
attributeType: 'hitDice',
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
}).fetch();
|
||||
// Use a collator to do sorting in natural order
|
||||
let collator = new Intl.Collator('en', {
|
||||
numeric: true, sensitivity: 'base'
|
||||
});
|
||||
// Get the hit dice in decending order of hitDiceSize
|
||||
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
|
||||
hitDice.sort(compare);
|
||||
// Get the total number of hit dice that can be recovered this rest
|
||||
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
|
||||
let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
|
||||
let recoverableHd = Math.max(Math.floor(totalHd * resetMultiplier), 1);
|
||||
// recover each hit dice in turn until the recoverable amount is used up
|
||||
let amountToRecover;
|
||||
hitDice.forEach(hd => {
|
||||
if (!recoverableHd) return;
|
||||
amountToRecover = Math.min(recoverableHd, hd.damage ?? 0);
|
||||
if (!amountToRecover) return;
|
||||
recoverableHd -= amountToRecover;
|
||||
damagePropertyWork({
|
||||
prop: hd,
|
||||
operation: 'increment',
|
||||
value: -amountToRecover,
|
||||
actionContext,
|
||||
logFunction(increment) {
|
||||
actionContext.addLog({
|
||||
name: hd.name,
|
||||
value: increment < 0 ? `Restored ${-increment} hit dice` : `Removed ${increment} hit dice`
|
||||
});
|
||||
}
|
||||
}).fetch();
|
||||
// Use a collator to do sorting in natural order
|
||||
let collator = new Intl.Collator('en', {
|
||||
numeric: true, sensitivity: 'base'
|
||||
});
|
||||
// Get the hit dice in decending order of hitDiceSize
|
||||
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
|
||||
hitDice.sort(compare);
|
||||
// Get the total number of hit dice that can be recovered this rest
|
||||
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
|
||||
let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
|
||||
let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1);
|
||||
// recover each hit dice in turn until the recoverable amount is used up
|
||||
let amountToRecover, resultingDamage;
|
||||
hitDice.forEach(hd => {
|
||||
if (!recoverableHd) return;
|
||||
amountToRecover = Math.min(recoverableHd, hd.damage || 0);
|
||||
if (!amountToRecover) return;
|
||||
recoverableHd -= amountToRecover;
|
||||
resultingDamage = hd.damage - amountToRecover;
|
||||
CreatureProperties.update(hd._id, {
|
||||
$set: {
|
||||
damage: resultingDamage,
|
||||
dirty: true,
|
||||
}
|
||||
}, {
|
||||
selector: {type: 'attribute'},
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default restCreature;
|
||||
|
||||
@@ -1,3 +1,324 @@
|
||||
if (Meteor.isServer) throw 'Client side only collection, don\'t import on server';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Mongo } from 'meteor/mongo';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import { softRemove } from '/imports/api/parenting/softRemove.js';
|
||||
import SoftRemovableSchema from '/imports/api/parenting/SoftRemovableSchema.js';
|
||||
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
|
||||
import '/imports/api/library/methods/index.js';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import { restore } from '/imports/api/parenting/softRemove.js';
|
||||
import { reorderDocs } from '/imports/api/parenting/order.js';
|
||||
import { getAncestry } from '/imports/api/parenting/parenting.js';
|
||||
|
||||
const Docs = new Mongo.Collection('docs');
|
||||
|
||||
const RefSchema = new SimpleSchema({
|
||||
id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
index: 1
|
||||
},
|
||||
collection: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.collectionName,
|
||||
},
|
||||
urlName: {
|
||||
type: String,
|
||||
regEx: /[a-z]+(?:[a-z]|-)+/,
|
||||
min: 2,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
optional: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.description,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
let ChildSchema = new SimpleSchema({
|
||||
order: {
|
||||
type: Number,
|
||||
},
|
||||
parent: {
|
||||
type: RefSchema,
|
||||
optional: true,
|
||||
},
|
||||
ancestors: {
|
||||
type: Array,
|
||||
defaultValue: [],
|
||||
maxCount: STORAGE_LIMITS.ancestorCount,
|
||||
},
|
||||
'ancestors.$': {
|
||||
type: RefSchema,
|
||||
},
|
||||
});
|
||||
|
||||
let DocSchema = new SimpleSchema({
|
||||
_id: {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.description,
|
||||
},
|
||||
urlName: {
|
||||
type: String,
|
||||
regEx: /[a-z]+(?:[a-z]|-)+/,
|
||||
min: 2,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
href: {
|
||||
type: String,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
published: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
icon: {
|
||||
type: storedIconsSchema,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.icon,
|
||||
},
|
||||
});
|
||||
|
||||
let schema = new SimpleSchema({});
|
||||
schema.extend(DocSchema);
|
||||
schema.extend(ChildSchema);
|
||||
schema.extend(SoftRemovableSchema);
|
||||
Docs.attachSchema(schema);
|
||||
|
||||
function assertDocsEditPermission(userId) {
|
||||
if (!userId || typeof userId !== 'string') throw new Meteor.Error('No user id provided');
|
||||
const user = Meteor.users.findOne(userId);
|
||||
if (!user) throw new Meteor.Error('User does not exist');
|
||||
if (!user?.roles?.includes?.('docsWriter')) throw ('Permission denied')
|
||||
}
|
||||
|
||||
function getDocLink(doc, urlName) {
|
||||
if (!urlName) urlName = doc.urlName;
|
||||
const address = ['/docs'];
|
||||
doc.ancestors?.forEach(a => {
|
||||
address.push(a.urlName);
|
||||
});
|
||||
address.push(urlName);
|
||||
return address.join('/');
|
||||
}
|
||||
|
||||
function rebuildDocAncestors(docId) {
|
||||
const newDoc = Docs.findOne(docId);
|
||||
Docs.find({ 'ancestors.id': docId }).forEach(doc => {
|
||||
doc.ancestors.forEach((a, i) => {
|
||||
if (a.id === docId) {
|
||||
Docs.update(doc._id, {
|
||||
$set: {
|
||||
[`ancestors.${i}`]: {
|
||||
id: newDoc._id,
|
||||
collection: 'docs',
|
||||
urlName: newDoc.urlName,
|
||||
name: newDoc.name,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
doc = Docs.findOne(doc._id);
|
||||
const newLink = getDocLink(doc);
|
||||
if (doc.href !== newLink) {
|
||||
Docs.update(doc._id, { $set: { href: newLink } })
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add a means of seeding new servers with documentation
|
||||
if (Meteor.isClient) {
|
||||
Docs.getJsonDocs = function () {
|
||||
return JSON.stringify(Docs.find({}).fetch(), null, 2);
|
||||
}
|
||||
} else if (Meteor.isServer) {
|
||||
Meteor.startup(() => {
|
||||
if (!Docs.findOne()) {
|
||||
Assets.getText('docs/defaultDocs.json', (error, string) => {
|
||||
const docs = JSON.parse(string)
|
||||
docs.forEach(doc => Docs.insert(doc));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const insertDoc = new ValidatedMethod({
|
||||
name: 'docs.insert',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ doc, parentRef }) {
|
||||
delete doc._id;
|
||||
assertDocsEditPermission(this.userId);
|
||||
// get the new ancestry for the properties
|
||||
if (parentRef) {
|
||||
var { ancestors } = getAncestry({
|
||||
parentRef,
|
||||
inheritedFields: { name: 1, urlName: 1 },
|
||||
});
|
||||
}
|
||||
doc.parent = parentRef;
|
||||
doc.ancestors = ancestors;
|
||||
|
||||
const lastOrder = Docs.find({}, { sort: { order: -1 } }).fetch()[0]?.order || 0;
|
||||
doc.order = lastOrder + 1;
|
||||
doc.urlName = 'new-doc-' + (lastOrder + 1);
|
||||
|
||||
doc.href = getDocLink(doc);
|
||||
if (Docs.findOne({ href: doc.href })) {
|
||||
throw new Meteor.Error('Link collision', 'A document with the same URL already exists');
|
||||
}
|
||||
|
||||
const docId = Docs.insert(doc);
|
||||
reorderDocs({
|
||||
collection: Docs,
|
||||
ancestorId: 'root',
|
||||
});
|
||||
return docId;
|
||||
},
|
||||
});
|
||||
|
||||
const updateDoc = new ValidatedMethod({
|
||||
name: 'docs.update',
|
||||
validate({ _id, path }) {
|
||||
if (!_id) return false;
|
||||
// We cannot change these fields with a simple update
|
||||
switch (path[0]) {
|
||||
case '_is':
|
||||
return false;
|
||||
}
|
||||
},
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ _id, path, value }) {
|
||||
assertDocsEditPermission(this.userId);
|
||||
let pathString = path.join('.');
|
||||
let modifier;
|
||||
// unset empty values
|
||||
if (value === null || value === undefined) {
|
||||
modifier = { $unset: { [pathString]: 1 } };
|
||||
} else {
|
||||
modifier = { $set: { [pathString]: value } };
|
||||
}
|
||||
if (pathString === 'urlName') {
|
||||
const doc = Docs.findOne(_id);
|
||||
const newLink = getDocLink(doc, value);
|
||||
if (Docs.findOne({ href: newLink })) {
|
||||
throw new Meteor.Error('Link collision', 'A document with the same URL already exists');
|
||||
}
|
||||
modifier.$set = modifier.$set || {};
|
||||
modifier.$set.href = newLink;
|
||||
rebuildDocAncestors(_id);
|
||||
}
|
||||
const updates = Docs.update(_id, modifier);
|
||||
if (pathString === 'name' || pathString === 'urlName') {
|
||||
rebuildDocAncestors(_id);
|
||||
}
|
||||
reorderDocs({
|
||||
collection: Docs,
|
||||
ancestorId: 'root',
|
||||
});
|
||||
return updates;
|
||||
},
|
||||
});
|
||||
|
||||
const pushToDoc = new ValidatedMethod({
|
||||
name: 'docs.push',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ _id, path, value }) {
|
||||
assertDocsEditPermission(this.userId);
|
||||
return Docs.update(_id, {
|
||||
$push: { [path.join('.')]: value },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const pullFromDoc = new ValidatedMethod({
|
||||
name: 'docs.pull',
|
||||
validate: null,
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ _id, path, itemId }) {
|
||||
assertDocsEditPermission(this.userId);
|
||||
return Docs.update(_id, {
|
||||
$pull: { [path.join('.')]: { _id: itemId } },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const softRemoveDoc = new ValidatedMethod({
|
||||
name: 'docs.softRemove',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ _id }) {
|
||||
assertDocsEditPermission(this.userId);
|
||||
softRemove({ _id, collection: Docs });
|
||||
reorderDocs({
|
||||
collection: Docs,
|
||||
ancestorId: 'root',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const restoreDoc = new ValidatedMethod({
|
||||
name: 'docs.restore',
|
||||
validate: new SimpleSchema({
|
||||
_id: SimpleSchema.RegEx.Id
|
||||
}).validator(),
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ _id }) {
|
||||
assertDocsEditPermission(this.userId);
|
||||
restore({ _id, collection: Docs });
|
||||
reorderDocs({
|
||||
collection: Docs,
|
||||
ancestorId: 'root',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export {
|
||||
DocSchema,
|
||||
insertDoc,
|
||||
updateDoc,
|
||||
pushToDoc,
|
||||
pullFromDoc,
|
||||
softRemoveDoc,
|
||||
restoreDoc,
|
||||
};
|
||||
|
||||
export default Docs;
|
||||
|
||||
@@ -5,8 +5,9 @@ import applyProperty from '../applyProperty.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js';
|
||||
|
||||
export default function applyAction(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
@@ -16,7 +17,7 @@ export default function applyAction(node, actionContext) {
|
||||
|
||||
// Log the name and summary
|
||||
let content = { name: prop.name };
|
||||
if (prop.summary?.text){
|
||||
if (prop.summary?.text) {
|
||||
recalculateInlineCalculations(prop.summary, actionContext);
|
||||
content.value = prop.summary.value;
|
||||
}
|
||||
@@ -29,24 +30,27 @@ export default function applyAction(node, actionContext) {
|
||||
const attack = prop.attackRoll || prop.attackRollBonus;
|
||||
|
||||
// Attack if there is an attack roll
|
||||
if (attack && attack.calculation){
|
||||
if (targets.length){
|
||||
if (attack && attack.calculation) {
|
||||
if (targets.length) {
|
||||
targets.forEach(target => {
|
||||
applyAttackToTarget({attack, target, actionContext});
|
||||
applyAttackToTarget({ attack, target, actionContext });
|
||||
// Apply the children, but only to the current target
|
||||
actionContext.targets = [target];
|
||||
applyChildren(node, actionContext);
|
||||
});
|
||||
} else {
|
||||
applyAttackWithoutTarget({attack, actionContext});
|
||||
applyAttackWithoutTarget({ attack, actionContext });
|
||||
applyChildren(node, actionContext);
|
||||
}
|
||||
} else {
|
||||
applyChildren(node, actionContext);
|
||||
}
|
||||
if (prop.actionType === 'event' && prop.variableName) {
|
||||
resetProperties(actionContext.creature._id, prop.variableName, actionContext);
|
||||
}
|
||||
}
|
||||
|
||||
function applyAttackWithoutTarget({attack, actionContext}){
|
||||
function applyAttackWithoutTarget({ attack, actionContext }) {
|
||||
delete actionContext.scope['$attackHit'];
|
||||
delete actionContext.scope['$attackMiss'];
|
||||
delete actionContext.scope['$criticalHit'];
|
||||
@@ -62,16 +66,16 @@ function applyAttackWithoutTarget({attack, actionContext}){
|
||||
criticalMiss,
|
||||
} = rollAttack(attack, scope);
|
||||
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
|
||||
if (scope['$attackAdvantage'] === 1){
|
||||
if (scope['$attackAdvantage'] === 1) {
|
||||
name += ' (Advantage)';
|
||||
} else if(scope['$attackAdvantage'] === -1){
|
||||
} else if (scope['$attackAdvantage'] === -1) {
|
||||
name += ' (Disadvantage)';
|
||||
}
|
||||
if (!criticalMiss){
|
||||
scope['$attackHit'] = {value: true}
|
||||
if (!criticalMiss) {
|
||||
scope['$attackHit'] = { value: true }
|
||||
}
|
||||
if (!criticalHit){
|
||||
scope['$attackMiss'] = {value: true};
|
||||
if (!criticalHit) {
|
||||
scope['$attackMiss'] = { value: true };
|
||||
}
|
||||
|
||||
actionContext.addLog({
|
||||
@@ -81,7 +85,7 @@ function applyAttackWithoutTarget({attack, actionContext}){
|
||||
});
|
||||
}
|
||||
|
||||
function applyAttackToTarget({attack, target, actionContext}){
|
||||
function applyAttackToTarget({ attack, target, actionContext }) {
|
||||
const scope = actionContext.scope;
|
||||
delete scope['$attackHit'];
|
||||
delete scope['$attackMiss'];
|
||||
@@ -99,15 +103,15 @@ function applyAttackToTarget({attack, target, actionContext}){
|
||||
criticalMiss,
|
||||
} = rollAttack(attack, scope);
|
||||
|
||||
if (target.variables.armor){
|
||||
if (target.variables.armor) {
|
||||
const armor = target.variables.armor.value;
|
||||
|
||||
let name = criticalHit ? 'Critical Hit!' :
|
||||
criticalMiss ? 'Critical Miss!' :
|
||||
result > armor ? 'Hit!' : 'Miss!';
|
||||
if (scope['$attackAdvantage'] === 1){
|
||||
result > armor ? 'Hit!' : 'Miss!';
|
||||
if (scope['$attackAdvantage'] === 1) {
|
||||
name += ' (Advantage)';
|
||||
} else if(scope['$attackAdvantage'] === -1){
|
||||
} else if (scope['$attackAdvantage'] === -1) {
|
||||
name += ' (Disadvantage)';
|
||||
}
|
||||
|
||||
@@ -116,15 +120,15 @@ function applyAttackToTarget({attack, target, actionContext}){
|
||||
value: `${resultPrefix}\n**${result}**`,
|
||||
inline: true,
|
||||
});
|
||||
if (criticalMiss || result < armor){
|
||||
scope['$attackMiss'] = {value: true};
|
||||
if (criticalMiss || result < armor) {
|
||||
scope['$attackMiss'] = { value: true };
|
||||
} else {
|
||||
scope['$attackHit'] = {value: true};
|
||||
scope['$attackHit'] = { value: true };
|
||||
}
|
||||
} else {
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value:'Target has no `armor`',
|
||||
value: 'Target has no `armor`',
|
||||
});
|
||||
actionContext.addLog({
|
||||
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
|
||||
@@ -134,10 +138,10 @@ function applyAttackToTarget({attack, target, actionContext}){
|
||||
}
|
||||
}
|
||||
|
||||
function rollAttack(attack, scope){
|
||||
function rollAttack(attack, scope) {
|
||||
const rollModifierText = numberToSignedString(attack.value, true);
|
||||
let value, resultPrefix;
|
||||
if (scope['$attackAdvantage'] === 1){
|
||||
if (scope['$attackAdvantage'] === 1) {
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a >= b) {
|
||||
value = a;
|
||||
@@ -146,7 +150,7 @@ function rollAttack(attack, scope){
|
||||
value = b;
|
||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
||||
}
|
||||
} else if (scope['$attackAdvantage'] === -1){
|
||||
} else if (scope['$attackAdvantage'] === -1) {
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a <= b) {
|
||||
value = a;
|
||||
@@ -159,25 +163,26 @@ function rollAttack(attack, scope){
|
||||
value = rollDice(1, 20)[0];
|
||||
resultPrefix = `1d20 [${value}] ${rollModifierText}`
|
||||
}
|
||||
scope['$attackRoll'] = {value};
|
||||
scope['$attackDiceRoll'] = { value };
|
||||
const result = value + attack.value;
|
||||
const {criticalHit, criticalMiss} = applyCrits(value, scope);
|
||||
return {resultPrefix, result, value, criticalHit, criticalMiss};
|
||||
scope['$attackRoll'] = { result };
|
||||
const { criticalHit, criticalMiss } = applyCrits(value, scope);
|
||||
return { resultPrefix, result, value, criticalHit, criticalMiss };
|
||||
}
|
||||
|
||||
function applyCrits(value, scope){
|
||||
function applyCrits(value, scope) {
|
||||
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
|
||||
let criticalHit = value >= criticalHitTarget;
|
||||
let criticalMiss;
|
||||
if (criticalHit){
|
||||
scope['$criticalHit'] = {value: true};
|
||||
if (criticalHit) {
|
||||
scope['$criticalHit'] = { value: true };
|
||||
} else {
|
||||
criticalMiss = value === 1;
|
||||
if (criticalMiss){
|
||||
scope['$criticalMiss'] = {value: true};
|
||||
if (criticalMiss) {
|
||||
scope['$criticalMiss'] = { value: true };
|
||||
}
|
||||
}
|
||||
return {criticalHit, criticalMiss};
|
||||
return { criticalHit, criticalMiss };
|
||||
}
|
||||
|
||||
function applyChildren(node, actionContext) {
|
||||
@@ -185,9 +190,9 @@ function applyChildren(node, actionContext) {
|
||||
node.children.forEach(child => applyProperty(child, actionContext));
|
||||
}
|
||||
|
||||
function spendResources(prop, actionContext){
|
||||
function spendResources(prop, actionContext) {
|
||||
// Check Uses
|
||||
if (prop.usesLeft <= 0){
|
||||
if (prop.usesLeft <= 0) {
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: `${prop.name || 'action'} does not have enough uses left`,
|
||||
@@ -195,7 +200,7 @@ function spendResources(prop, actionContext){
|
||||
return true;
|
||||
}
|
||||
// Resources
|
||||
if (prop.insufficientResources){
|
||||
if (prop.insufficientResources) {
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: 'This creature doesn\'t have sufficient resources to perform this action',
|
||||
@@ -209,14 +214,14 @@ function spendResources(prop, actionContext){
|
||||
try {
|
||||
prop.resources.itemsConsumed.forEach(itemConsumed => {
|
||||
recalculateCalculation(itemConsumed.quantity, actionContext);
|
||||
if (!itemConsumed.itemId){
|
||||
if (!itemConsumed.itemId) {
|
||||
throw 'No ammo was selected for this prop';
|
||||
}
|
||||
let item = CreatureProperties.findOne(itemConsumed.itemId);
|
||||
if (!item || item.ancestors[0].id !== prop.ancestors[0].id){
|
||||
if (!item || item.ancestors[0].id !== prop.ancestors[0].id) {
|
||||
throw 'The prop\'s ammo was not found on the creature';
|
||||
}
|
||||
if (!item.equipped){
|
||||
if (!item.equipped) {
|
||||
throw 'The selected ammo is not equipped';
|
||||
}
|
||||
if (
|
||||
@@ -229,16 +234,16 @@ function spendResources(prop, actionContext){
|
||||
value: itemConsumed.quantity.value,
|
||||
});
|
||||
let logName = item.name;
|
||||
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1){
|
||||
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1) {
|
||||
logName = item.plural || logName;
|
||||
}
|
||||
if (itemConsumed.quantity.value > 0){
|
||||
if (itemConsumed.quantity.value > 0) {
|
||||
spendLog.push(logName + ': ' + itemConsumed.quantity.value);
|
||||
} else if (itemConsumed.quantity.value < 0){
|
||||
} else if (itemConsumed.quantity.value < 0) {
|
||||
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
|
||||
}
|
||||
});
|
||||
} catch (e){
|
||||
} catch (e) {
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: e,
|
||||
@@ -251,9 +256,9 @@ function spendResources(prop, actionContext){
|
||||
itemQuantityAdjustments.forEach(adjustQuantityWork);
|
||||
|
||||
// Use uses
|
||||
if (prop.usesLeft){
|
||||
if (prop.usesLeft) {
|
||||
CreatureProperties.update(prop._id, {
|
||||
$inc: {usesUsed: 1}
|
||||
$inc: { usesUsed: 1 }
|
||||
}, {
|
||||
selector: prop
|
||||
});
|
||||
@@ -270,7 +275,7 @@ function spendResources(prop, actionContext){
|
||||
|
||||
if (!attConsumed.quantity?.value) return;
|
||||
let stat = actionContext.scope[attConsumed.variableName];
|
||||
if (!stat){
|
||||
if (!stat) {
|
||||
spendLog.push(stat.name + ': ' + ' not found');
|
||||
return;
|
||||
}
|
||||
@@ -280,9 +285,9 @@ function spendResources(prop, actionContext){
|
||||
value: attConsumed.quantity.value,
|
||||
actionContext,
|
||||
});
|
||||
if (attConsumed.quantity.value > 0){
|
||||
if (attConsumed.quantity.value > 0) {
|
||||
spendLog.push(stat.name + ': ' + attConsumed.quantity.value);
|
||||
} else if (attConsumed.quantity.value < 0){
|
||||
} else if (attConsumed.quantity.value < 0) {
|
||||
gainLog.push(stat.name + ': ' + -attConsumed.quantity.value);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
|
||||
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
|
||||
|
||||
export default function applyBuff(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
@@ -46,12 +47,17 @@ export default function applyBuff(node, actionContext) {
|
||||
copyNodeListToTarget(propList, target, oldParent);
|
||||
|
||||
//Log the buff
|
||||
let logValue = prop.description?.value
|
||||
if (prop.description?.text) {
|
||||
recalculateInlineCalculations(prop.description, actionContext);
|
||||
logValue = prop.description?.value;
|
||||
}
|
||||
if ((prop.name || prop.description?.value) && !prop.silent) {
|
||||
if (target._id === actionContext.creature._id) {
|
||||
// Targeting self
|
||||
actionContext.addLog({
|
||||
name: prop.name,
|
||||
value: prop.description?.value,
|
||||
value: logValue,
|
||||
});
|
||||
} else {
|
||||
// Targeting other
|
||||
@@ -60,7 +66,7 @@ export default function applyBuff(node, actionContext) {
|
||||
creatureId: target._id,
|
||||
content: [{
|
||||
name: prop.name,
|
||||
value: prop.description?.value,
|
||||
value: logValue,
|
||||
}],
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
getPropertiesOfType
|
||||
} from '/imports/api/engine/loadCreatures.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
|
||||
|
||||
export default function applyDamage(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
@@ -173,14 +174,15 @@ function applyDamageMultipliers({ target, damage, damageProp, logValue }) {
|
||||
function multiplierAppliesTo(damageProp, multiplierType) {
|
||||
return multiplier => {
|
||||
// Apply the default 'ignore x' tags
|
||||
if (includes(damageProp.tags, `ignore ${multiplierType}`)) return false;
|
||||
const effectiveTags = getEffectivePropTags(damageProp);
|
||||
if (includes(effectiveTags, `ignore ${multiplierType}`)) return false;
|
||||
|
||||
const hasRequiredTags = difference(
|
||||
multiplier.includeTags, damageProp.tags
|
||||
multiplier.includeTags, effectiveTags
|
||||
).length === 0;
|
||||
|
||||
const hasNoExcludedTags = intersection(
|
||||
multiplier.excludeTags, damageProp.tags
|
||||
multiplier.excludeTags, effectiveTags
|
||||
).length === 0;
|
||||
|
||||
return hasRequiredTags && hasNoExcludedTags;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import rollDice from '/imports/parser/rollDice.js';
|
||||
import recalculateCalculation from './shared/recalculateCalculation.js';
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import { applyUnresolvedEffects } from '/imports/api/engine/actions/doCheck.js';
|
||||
|
||||
export default function applySavingThrow(node, actionContext){
|
||||
export default function applySavingThrow(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
const prop = node.node;
|
||||
|
||||
@@ -13,7 +14,7 @@ export default function applySavingThrow(node, actionContext){
|
||||
recalculateCalculation(prop.dc, actionContext);
|
||||
|
||||
const dc = (prop.dc?.value);
|
||||
if (!isFinite(dc)){
|
||||
if (!isFinite(dc)) {
|
||||
actionContext.addLog({
|
||||
name: 'Error',
|
||||
value: 'Saving throw requires a DC',
|
||||
@@ -29,8 +30,8 @@ export default function applySavingThrow(node, actionContext){
|
||||
|
||||
// If there are no save targets, apply all children as if the save both
|
||||
// succeeeded and failed
|
||||
if (!saveTargets?.length){
|
||||
scope['$saveFailed'] = {value: true};
|
||||
if (!saveTargets?.length) {
|
||||
scope['$saveFailed'] = { value: true };
|
||||
scope['$saveSucceeded'] = { value: true };
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
return node.children.forEach(child => applyProperty(child, actionContext));
|
||||
@@ -51,7 +52,7 @@ export default function applySavingThrow(node, actionContext){
|
||||
|
||||
const save = target.variables[prop.stat];
|
||||
|
||||
if (!save){
|
||||
if (!save) {
|
||||
actionContext.addLog({
|
||||
name: 'Saving throw error',
|
||||
value: 'No saving throw found: ' + prop.stat,
|
||||
@@ -59,10 +60,14 @@ export default function applySavingThrow(node, actionContext){
|
||||
return applyChildren();
|
||||
}
|
||||
|
||||
const rollModifierText = numberToSignedString(save.value, true);
|
||||
let rollModifierText = numberToSignedString(save.value, true);
|
||||
let rollModifier = save.value
|
||||
const { effectBonus, effectString } = applyUnresolvedEffects(save, scope)
|
||||
rollModifierText += effectString;
|
||||
rollModifier += effectBonus;
|
||||
|
||||
let value, values, resultPrefix;
|
||||
if (save.advantage === 1){
|
||||
if (save.advantage === 1) {
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a >= b) {
|
||||
value = a;
|
||||
@@ -71,7 +76,7 @@ export default function applySavingThrow(node, actionContext){
|
||||
value = b;
|
||||
resultPrefix = `Advantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
||||
}
|
||||
} else if (save.advantage === -1){
|
||||
} else if (save.advantage === -1) {
|
||||
const [a, b] = rollDice(2, 20);
|
||||
if (a <= b) {
|
||||
value = a;
|
||||
@@ -85,14 +90,14 @@ export default function applySavingThrow(node, actionContext){
|
||||
value = values[0];
|
||||
resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
|
||||
}
|
||||
scope['$saveDiceRoll'] = {value};
|
||||
const result = value + save.value || 0;
|
||||
scope['$saveRoll'] = {value: result};
|
||||
scope['$saveDiceRoll'] = { value };
|
||||
const result = value + rollModifier || 0;
|
||||
scope['$saveRoll'] = { value: result };
|
||||
const saveSuccess = result >= dc;
|
||||
if (saveSuccess){
|
||||
scope['$saveSucceeded'] = {value: true};
|
||||
if (saveSuccess) {
|
||||
scope['$saveSucceeded'] = { value: true };
|
||||
} else {
|
||||
scope['$saveFailed'] = {value: true};
|
||||
scope['$saveFailed'] = { value: true };
|
||||
}
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: saveSuccess ? 'Successful save' : 'Failed save',
|
||||
|
||||
@@ -4,7 +4,7 @@ import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import rollDice from '/imports/parser/rollDice.js';
|
||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
|
||||
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import ActionContext from '/imports/api/engine/actions/ActionContext.js';
|
||||
import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation.js';
|
||||
@@ -28,6 +28,7 @@ const doCheck = new ValidatedMethod({
|
||||
const creatureId = prop.ancestors[0].id;
|
||||
const actionContext = new ActionContext(creatureId, [creatureId], this);
|
||||
Object.assign(actionContext.scope, scope);
|
||||
actionContext.scope[`#${prop.type}`] = prop;
|
||||
|
||||
// Check permissions
|
||||
assertEditPermission(actionContext.creature, this.userId);
|
||||
@@ -115,7 +116,7 @@ function rollCheck(prop, actionContext) {
|
||||
});
|
||||
}
|
||||
|
||||
function applyUnresolvedEffects(prop, scope) {
|
||||
export function applyUnresolvedEffects(prop, scope) {
|
||||
let effectBonus = 0;
|
||||
let effectString = '';
|
||||
if (!prop.effects) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import findAncestorByType from '/imports/api/engine/computation/utility/findAncestorByType.js';
|
||||
import { traverse } from '/imports/parser/resolve.js';
|
||||
|
||||
export default function linkCalculationDependencies(dependencyGraph, prop, {propsById}){
|
||||
export default function linkCalculationDependencies(dependencyGraph, prop, { propsById }) {
|
||||
prop._computationDetails.calculations.forEach(calcObj => {
|
||||
// Store resolved ancestors
|
||||
const memo = {
|
||||
@@ -16,12 +16,13 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop
|
||||
// Skip nodes that aren't symbols or accessors
|
||||
if (node.parseType !== 'symbol' && node.parseType !== 'accessor') return;
|
||||
// Link ancestor references as direct property dependencies
|
||||
if (node.name[0] === '#'){
|
||||
if (node.name[0] === '#') {
|
||||
let ancestorProp = getAncestorProp(
|
||||
node.name.slice(1), memo, prop, propsById
|
||||
);
|
||||
if (!ancestorProp) return;
|
||||
// Link the ancestor prop as a direct dependency
|
||||
// TODO: we might be referencing a calculation sub-field, depend on that instead
|
||||
dependencyGraph.addLink(
|
||||
calcNodeId, ancestorProp._id, 'ancestorReference'
|
||||
);
|
||||
@@ -34,16 +35,16 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop
|
||||
});
|
||||
// Store the resolved ancestors in this calculation's local scope
|
||||
if (memo.ancestors) {
|
||||
calcObj._localScope = { ...calcObj._localScope, ...memo.ancestors};
|
||||
calcObj._localScope = { ...calcObj._localScope, ...memo.ancestors };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getAncestorProp(type, memo, prop, propsById){
|
||||
if (memo.ancestors && memo.ancestors['#' + type]){
|
||||
function getAncestorProp(type, memo, prop, propsById) {
|
||||
if (memo.ancestors && memo.ancestors['#' + type]) {
|
||||
return memo.ancestors['#' + type];
|
||||
} else {
|
||||
var ancestorProp = findAncestorByType( prop, type, propsById );
|
||||
var ancestorProp = findAncestorByType(prop, type, propsById);
|
||||
if (!memo.ancestors) memo.ancestors = {};
|
||||
memo.ancestors['#' + type] = ancestorProp;
|
||||
return ancestorProp;
|
||||
|
||||
@@ -23,23 +23,26 @@ const linkDependenciesByType = {
|
||||
toggle: linkToggle,
|
||||
}
|
||||
|
||||
export default function linkTypeDependencies(dependencyGraph, prop, computation){
|
||||
export default function linkTypeDependencies(dependencyGraph, prop, computation) {
|
||||
linkDependenciesByType[prop.type]?.(dependencyGraph, prop, computation);
|
||||
}
|
||||
|
||||
function dependOnCalc({dependencyGraph, prop, key}){
|
||||
function dependOnCalc({ dependencyGraph, prop, key }) {
|
||||
let calc = get(prop, key);
|
||||
if (!calc) return;
|
||||
if (calc.type !== '_calculation'){
|
||||
if (calc.type !== '_calculation') {
|
||||
throw `Expected calculation got ${calc.type}`
|
||||
}
|
||||
dependencyGraph.addLink(prop._id, `${prop._id}.${key}`, 'calculation');
|
||||
}
|
||||
|
||||
function linkAction(dependencyGraph, prop, {propsById}){
|
||||
function linkAction(dependencyGraph, prop, { propsById }) {
|
||||
if (prop.variableName) {
|
||||
dependencyGraph.addLink(prop.variableName, prop._id, 'eventDefinition');
|
||||
}
|
||||
// The action depends on its attack roll and uses calculations
|
||||
dependOnCalc({dependencyGraph, prop, key: 'attackRoll'});
|
||||
dependOnCalc({dependencyGraph, prop, key: 'uses'});
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'attackRoll' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'uses' });
|
||||
|
||||
// Link the resources the action uses
|
||||
if (!prop.resources) return;
|
||||
@@ -47,7 +50,7 @@ function linkAction(dependencyGraph, prop, {propsById}){
|
||||
prop.resources.itemsConsumed.forEach((itemConsumed, index) => {
|
||||
if (!itemConsumed.itemId) return;
|
||||
const item = propsById[itemConsumed.itemId];
|
||||
if (!item || item.inactive){
|
||||
if (!item || item.inactive) {
|
||||
// Unlink if the item doesn't exist or is inactive
|
||||
itemConsumed.itemId = undefined;
|
||||
return;
|
||||
@@ -79,48 +82,48 @@ function linkAction(dependencyGraph, prop, {propsById}){
|
||||
});
|
||||
}
|
||||
|
||||
function linkAdjustment(dependencyGraph, prop){
|
||||
function linkAdjustment(dependencyGraph, prop) {
|
||||
// Adjustment depends on its amount
|
||||
dependOnCalc({dependencyGraph, prop, key: 'amount'});
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'amount' });
|
||||
}
|
||||
|
||||
function linkAttribute(dependencyGraph, prop){
|
||||
function linkAttribute(dependencyGraph, prop) {
|
||||
linkVariableName(dependencyGraph, prop);
|
||||
// Depends on spellSlotLevel
|
||||
dependOnCalc({dependencyGraph, prop, key: 'spellSlotLevel'});
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'spellSlotLevel' });
|
||||
|
||||
// Depends on base value
|
||||
dependOnCalc({dependencyGraph, prop, key: 'baseValue'});
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
|
||||
|
||||
// hit dice depend on constitution
|
||||
if (prop.attributeType === 'hitDice'){
|
||||
if (prop.attributeType === 'hitDice') {
|
||||
dependencyGraph.addLink(prop._id, 'constitution', 'hitDiceConMod');
|
||||
}
|
||||
}
|
||||
|
||||
function linkBranch(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'condition'});
|
||||
function linkBranch(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'condition' });
|
||||
}
|
||||
|
||||
function linkBuff(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'duration'});
|
||||
function linkBuff(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'duration' });
|
||||
}
|
||||
|
||||
function linkClassLevel(dependencyGraph, prop) {
|
||||
if (prop.inactive) return;
|
||||
// The variableName of the prop depends on the prop
|
||||
if (prop.variableName && prop.level){
|
||||
if (prop.variableName && prop.level) {
|
||||
dependencyGraph.addLink(prop.variableName, prop._id, 'classLevel');
|
||||
// The level variable depends on the class variableName variable
|
||||
let existingLevelLink = dependencyGraph.getLink('level', prop.variableName);
|
||||
if (!existingLevelLink){
|
||||
if (!existingLevelLink) {
|
||||
dependencyGraph.addLink('level', prop.variableName, 'level');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function linkDamage(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'amount'});
|
||||
function linkDamage(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'amount' });
|
||||
}
|
||||
|
||||
function linkEffects(dependencyGraph, prop, computation) {
|
||||
@@ -132,7 +135,7 @@ function linkEffects(dependencyGraph, prop, computation) {
|
||||
if (prop.inactive) {
|
||||
// Inactive effects apply to no stats
|
||||
return;
|
||||
} else if (prop.targetByTags){
|
||||
} else if (prop.targetByTags) {
|
||||
getEffectTagTargets(prop, computation).forEach(targetId => {
|
||||
const targetProp = computation.propsById[targetId];
|
||||
if (
|
||||
@@ -147,8 +150,8 @@ function linkEffects(dependencyGraph, prop, computation) {
|
||||
// Otherwise target a field on that property
|
||||
const key = prop.targetField || getDefaultCalculationField(targetProp);
|
||||
const calcObj = get(targetProp, key);
|
||||
if (calcObj && calcObj.calculation){
|
||||
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id , 'effect');
|
||||
if (calcObj && calcObj.calculation) {
|
||||
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'effect');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -161,14 +164,14 @@ function linkEffects(dependencyGraph, prop, computation) {
|
||||
}
|
||||
|
||||
// Returns an array of IDs of the properties the effect targets
|
||||
function getEffectTagTargets(effect, computation){
|
||||
function getEffectTagTargets(effect, computation) {
|
||||
let targets = getTargetListFromTags(effect.targetTags, computation);
|
||||
let notIds = [];
|
||||
if (effect.extraTags){
|
||||
if (effect.extraTags) {
|
||||
effect.extraTags.forEach(ex => {
|
||||
if (ex.operation === 'OR') {
|
||||
targets = union(targets, getTargetListFromTags(ex.tags, computation));
|
||||
} else if (ex.operation === 'NOT'){
|
||||
} else if (ex.operation === 'NOT') {
|
||||
ex.tags.forEach(tag => {
|
||||
const idList = computation.propsWithTag[tag];
|
||||
if (idList) {
|
||||
@@ -181,7 +184,7 @@ function getEffectTagTargets(effect, computation){
|
||||
return difference(targets, notIds);
|
||||
}
|
||||
|
||||
function getTargetListFromTags(tags, computation){
|
||||
function getTargetListFromTags(tags, computation) {
|
||||
const targetTagIdLists = [];
|
||||
if (!tags) return [];
|
||||
tags.forEach(tag => {
|
||||
@@ -192,8 +195,8 @@ function getTargetListFromTags(tags, computation){
|
||||
return targets;
|
||||
}
|
||||
|
||||
function getDefaultCalculationField(prop){
|
||||
switch (prop.type){
|
||||
function getDefaultCalculationField(prop) {
|
||||
switch (prop.type) {
|
||||
case 'action': return 'attackRoll';
|
||||
case 'adjustment': return 'amount';
|
||||
case 'attribute': return 'baseValue';
|
||||
@@ -223,13 +226,13 @@ function getDefaultCalculationField(prop){
|
||||
}
|
||||
}
|
||||
|
||||
function linkRoll(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'roll'});
|
||||
function linkRoll(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'roll' });
|
||||
}
|
||||
|
||||
function linkVariableName(dependencyGraph, prop){
|
||||
function linkVariableName(dependencyGraph, prop) {
|
||||
// The variableName of the prop depends on the prop if the prop is active
|
||||
if (prop.variableName && !prop.inactive){
|
||||
if (prop.variableName && !prop.inactive) {
|
||||
dependencyGraph.addLink(prop.variableName, prop._id, 'definition');
|
||||
}
|
||||
}
|
||||
@@ -243,7 +246,7 @@ function linkDamageMultiplier(dependencyGraph, prop) {
|
||||
});
|
||||
}
|
||||
|
||||
function linkPointBuy(dependencyGraph, prop){
|
||||
function linkPointBuy(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'min' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'max' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'cost' });
|
||||
@@ -265,7 +268,7 @@ function linkPointBuy(dependencyGraph, prop){
|
||||
if (prop.inactive) return;
|
||||
}
|
||||
|
||||
function linkProficiencies(dependencyGraph, prop){
|
||||
function linkProficiencies(dependencyGraph, prop) {
|
||||
// The stats depend on the proficiency
|
||||
if (prop.inactive) return;
|
||||
prop.stats.forEach(statName => {
|
||||
@@ -274,36 +277,36 @@ function linkProficiencies(dependencyGraph, prop){
|
||||
});
|
||||
}
|
||||
|
||||
function linkSavingThrow(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'dc'});
|
||||
function linkSavingThrow(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'dc' });
|
||||
}
|
||||
|
||||
function linkSkill(dependencyGraph, prop){
|
||||
function linkSkill(dependencyGraph, prop) {
|
||||
// Depends on base value
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
|
||||
// Link dependents
|
||||
if (prop.inactive) return;
|
||||
linkVariableName(dependencyGraph, prop);
|
||||
// The prop depends on the variable references as the ability
|
||||
if (prop.ability){
|
||||
if (prop.ability) {
|
||||
dependencyGraph.addLink(prop._id, prop.ability, 'skillAbilityScore');
|
||||
}
|
||||
// Skills depend on the creature's proficiencyBonus
|
||||
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
|
||||
}
|
||||
|
||||
function linkSlot(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'quantityExpected'});
|
||||
dependOnCalc({dependencyGraph, prop, key: 'slotCondition'});
|
||||
function linkSlot(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'quantityExpected' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'slotCondition' });
|
||||
}
|
||||
|
||||
function linkSpellList(dependencyGraph, prop){
|
||||
dependOnCalc({dependencyGraph, prop, key: 'maxPrepared'});
|
||||
dependOnCalc({dependencyGraph, prop, key: 'attackRollBonus'});
|
||||
dependOnCalc({dependencyGraph, prop, key: 'dc'});
|
||||
function linkSpellList(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'maxPrepared' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'attackRollBonus' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'dc' });
|
||||
}
|
||||
|
||||
function linkToggle(dependencyGraph, prop){
|
||||
function linkToggle(dependencyGraph, prop) {
|
||||
linkVariableName(dependencyGraph, prop);
|
||||
dependOnCalc({dependencyGraph, prop, key: 'condition'});
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'condition' });
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import skill from './computeByType/computeSkill.js';
|
||||
import pointBuy from './computeByType/computePointBuy.js';
|
||||
import propertySlot from './computeByType/computeSlot.js';
|
||||
import container from './computeByType/computeContainer.js';
|
||||
import spellList from './computeByType/computeSpellList.js';
|
||||
import _calculation from './computeByType/computeCalculation.js';
|
||||
|
||||
export default Object.freeze({
|
||||
@@ -17,4 +18,5 @@ export default Object.freeze({
|
||||
pointBuy,
|
||||
propertySlot,
|
||||
spell: action,
|
||||
spellList,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export default function computeAction(computation, node){
|
||||
export default function computeAction(computation, node) {
|
||||
const prop = node.data;
|
||||
if (prop.uses){
|
||||
if (prop.uses) {
|
||||
prop.usesLeft = prop.uses.value - (prop.usesUsed || 0);
|
||||
if (!prop.usesLeft){
|
||||
if (!prop.usesLeft) {
|
||||
prop.insufficientResources = true;
|
||||
}
|
||||
}
|
||||
@@ -10,19 +10,19 @@ export default function computeAction(computation, node){
|
||||
if (!prop.resources) return;
|
||||
prop.resources.itemsConsumed.forEach(itemConsumed => {
|
||||
if (!itemConsumed.itemId) return;
|
||||
if (itemConsumed.available < itemConsumed.quantity?.value){
|
||||
if (itemConsumed.available < itemConsumed.quantity?.value) {
|
||||
prop.insufficientResources = true;
|
||||
}
|
||||
});
|
||||
prop.resources.attributesConsumed.forEach(attConsumed => {
|
||||
if (!attConsumed.variableName) return;
|
||||
if (attConsumed.available < attConsumed.quantity?.value){
|
||||
if (attConsumed.available < attConsumed.quantity?.value) {
|
||||
prop.insufficientResources = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function computeResources(computation, node){
|
||||
function computeResources(computation, node) {
|
||||
const resources = node.data?.resources;
|
||||
if (!resources) return;
|
||||
resources.attributesConsumed.forEach(attConsumed => {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export default function computeSpelllist(computation, node) {
|
||||
const prop = node.data;
|
||||
|
||||
const ability = computation.scope[prop.ability];
|
||||
if (Number.isFinite(ability?.modifier)) {
|
||||
prop.abilityMod = ability.modifier;
|
||||
} else if (Number.isFinite(ability?.value)) {
|
||||
prop.abilityMod = ability.value;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import computeVariableAsToggle from './computeVariable/computeVariableAsToggle.j
|
||||
import computeImplicitVariable from './computeVariable/computeImplicitVariable.js';
|
||||
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
||||
|
||||
export default function computeVariable(computation, node){
|
||||
export default function computeVariable(computation, node) {
|
||||
const scope = computation.scope;
|
||||
if (!node.data) node.data = {};
|
||||
aggregateLinks(computation, node);
|
||||
@@ -15,7 +15,7 @@ export default function computeVariable(computation, node){
|
||||
// Don't add to the scope if the node id is not a legitimate variable name
|
||||
// Without this `some.thing` could break the entire sheet as a database key
|
||||
if (!VARIABLE_NAME_REGEX.test(node.id)) return;
|
||||
if (node.data.definingProp){
|
||||
if (node.data.definingProp) {
|
||||
// Add the defining variable to the scope
|
||||
scope[node.id] = node.data.definingProp
|
||||
} else {
|
||||
@@ -24,7 +24,7 @@ export default function computeVariable(computation, node){
|
||||
}
|
||||
}
|
||||
|
||||
function aggregateLinks(computation, node){
|
||||
function aggregateLinks(computation, node) {
|
||||
computation.dependencyGraph.forEachLinkedNode(
|
||||
node.id,
|
||||
(linkedNode, link) => {
|
||||
@@ -32,11 +32,12 @@ function aggregateLinks(computation, node){
|
||||
// Ignore inactive props
|
||||
if (linkedNode.data.inactive) return;
|
||||
// Apply all the aggregations
|
||||
let arg = {node, linkedNode, link, computation};
|
||||
let arg = { node, linkedNode, link, computation };
|
||||
aggregate.classLevel(arg);
|
||||
aggregate.damageMultiplier(arg);
|
||||
aggregate.definition(arg);
|
||||
aggregate.effect(arg);
|
||||
aggregate.eventDefinition(arg);
|
||||
aggregate.inventory(arg);
|
||||
aggregate.proficiency(arg);
|
||||
},
|
||||
@@ -44,7 +45,7 @@ function aggregateLinks(computation, node){
|
||||
);
|
||||
}
|
||||
|
||||
function combineAggregations(computation, node){
|
||||
function combineAggregations(computation, node) {
|
||||
combineMultiplierAggregator(node);
|
||||
node.data.overridenProps?.forEach(prop => {
|
||||
computeVariableProp(computation, node, prop);
|
||||
@@ -52,51 +53,51 @@ function combineAggregations(computation, node){
|
||||
computeVariableProp(computation, node, node.data.definingProp);
|
||||
}
|
||||
|
||||
function computeVariableProp(computation, node, prop){
|
||||
function computeVariableProp(computation, node, prop) {
|
||||
if (!prop) return;
|
||||
|
||||
// Combine damage multipliers in all props so that they can't be overridden
|
||||
if (node.data.immunity){
|
||||
if (node.data.immunity) {
|
||||
prop.immunity = node.data.immunity;
|
||||
prop.immunities = node.data.immunities;
|
||||
}
|
||||
if (node.data.resistance){
|
||||
if (node.data.resistance) {
|
||||
prop.resistance = node.data.resistance;
|
||||
prop.resistances = node.data.resistances;
|
||||
}
|
||||
if (node.data.vulnerability){
|
||||
if (node.data.vulnerability) {
|
||||
prop.vulnerability = node.data.vulnerability;
|
||||
prop.vulnerabilities = node.data.vulnerabilities;
|
||||
}
|
||||
|
||||
if (prop.type === 'attribute'){
|
||||
if (prop.type === 'attribute') {
|
||||
computeVariableAsAttribute(computation, node, prop);
|
||||
} else if (prop.type === 'skill'){
|
||||
} else if (prop.type === 'skill') {
|
||||
computeVariableAsSkill(computation, node, prop);
|
||||
} else if (prop.type === 'constant'){
|
||||
} else if (prop.type === 'constant') {
|
||||
computeVariableAsConstant(computation, node, prop);
|
||||
} else if (prop.type === 'class'){
|
||||
} else if (prop.type === 'class') {
|
||||
computeVariableAsClass(computation, node, prop);
|
||||
} else if (prop.type === 'toggle'){
|
||||
} else if (prop.type === 'toggle') {
|
||||
computeVariableAsToggle(computation, node, prop);
|
||||
}
|
||||
}
|
||||
|
||||
function combineMultiplierAggregator(node){
|
||||
function combineMultiplierAggregator(node) {
|
||||
// get a reference to the aggregator
|
||||
const aggregator = node.data.multiplierAggregator;
|
||||
if (!aggregator) return;
|
||||
|
||||
// Combine
|
||||
if (aggregator.immunities?.length){
|
||||
if (aggregator.immunities?.length) {
|
||||
node.data.immunity = true;
|
||||
node.data.immunities = aggregator.immunities;
|
||||
}
|
||||
if (aggregator.resistances?.length){
|
||||
if (aggregator.resistances?.length) {
|
||||
node.data.resistance = true;
|
||||
node.data.resistances = aggregator.resistances;
|
||||
}
|
||||
if (aggregator.vulnerabilities?.length){
|
||||
if (aggregator.vulnerabilities?.length) {
|
||||
node.data.vulnerability = true;
|
||||
node.data.vulnerabilities = aggregator.vulnerabilities;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { pick } from 'lodash';
|
||||
|
||||
export default function aggregateEffect({node, linkedNode, link}){
|
||||
export default function aggregateEffect({ node, linkedNode, link }) {
|
||||
if (link.data !== 'effect') return;
|
||||
// store the effect aggregator, its presence indicates that the variable is
|
||||
// targeted by effects
|
||||
@@ -38,21 +38,22 @@ export default function aggregateEffect({node, linkedNode, link}){
|
||||
operation: linkedNode.data.operation,
|
||||
amount: effectAmount,
|
||||
type: linkedNode.data.type,
|
||||
text: linkedNode.data.text,
|
||||
// ancestors: linkedNode.data.ancestors,
|
||||
});
|
||||
|
||||
// get a shorter reference to the aggregator document
|
||||
const aggregator = node.data.effectAggregator;
|
||||
// Get the result of the effect
|
||||
const result = linkedNode.data.amount?.value;
|
||||
// Skip aggregating if the result is not resolved completely
|
||||
if (typeof result === 'string' || result === undefined) return;
|
||||
let result = linkedNode.data.amount?.value;
|
||||
if (typeof result !== 'number') result = undefined;
|
||||
|
||||
// Aggregate the effect based on its operation
|
||||
switch(linkedNode.data.operation){
|
||||
switch (linkedNode.data.operation) {
|
||||
case 'base':
|
||||
// Take the largest base value
|
||||
if (Number.isFinite(result)){
|
||||
if(Number.isFinite(aggregator.base)){
|
||||
if (Number.isFinite(result)) {
|
||||
if (Number.isFinite(aggregator.base)) {
|
||||
aggregator.base = Math.max(aggregator.base, result);
|
||||
} else {
|
||||
aggregator.base = result;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
export default function aggregateEventDefinition({ node, linkedNode, link }) {
|
||||
// Look at all event definition links
|
||||
if (link.data !== 'eventDefinition') return;
|
||||
|
||||
// Store which property is THE defining event and which are overridden
|
||||
const prop = linkedNode.data;
|
||||
// get current defining event
|
||||
const definingEvent = node.data.definingEvent;
|
||||
// Find the last defining event
|
||||
if (
|
||||
!definingEvent ||
|
||||
prop.order > definingEvent.order
|
||||
) {
|
||||
// override the current defining prop
|
||||
if (definingEvent) definingEvent.overridden = true;
|
||||
// set this prop as the new defining prop
|
||||
node.data.definingEvent = prop;
|
||||
} else {
|
||||
prop.overridden = true;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import definition from './aggregateDefinition.js';
|
||||
import damageMultiplier from './aggregateDamageMultiplier.js';
|
||||
import effect from './aggregateEffect.js';
|
||||
import eventDefinition from './aggregateEventDefinition.js';
|
||||
import proficiency from './aggregateProficiency.js';
|
||||
import classLevel from './aggregateClassLevel.js';
|
||||
import inventory from './aggregateInventory.js';
|
||||
@@ -10,6 +11,7 @@ export default Object.freeze({
|
||||
damageMultiplier,
|
||||
definition,
|
||||
effect,
|
||||
eventDefinition,
|
||||
inventory,
|
||||
proficiency,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js';
|
||||
|
||||
const UserImages = createS3FilesCollection({
|
||||
collectionName: 'userImages',
|
||||
storagePath: Meteor.isDevelopment ? '/DiceCloud/userImages/' : 'assets/app/userImages',
|
||||
storagePath: Meteor.isDevelopment ? '../../../../../fileStorage/userImages' : 'assets/app/userImages',
|
||||
onBeforeUpload(file) {
|
||||
// Allow upload files under 10MB
|
||||
if (file.size > 10485760) {
|
||||
|
||||
@@ -4,7 +4,9 @@ import { each, clone } from 'lodash';
|
||||
import { Random } from 'meteor/random';
|
||||
import { FilesCollection } from 'meteor/ostrio:files';
|
||||
import stream from 'stream';
|
||||
import S3 from 'aws-sdk/clients/s3';
|
||||
if (Meteor.isServer) {
|
||||
import S3 from '/imports/api/files/server/s3.js';
|
||||
}
|
||||
|
||||
/* See fs-extra and graceful-fs NPM packages */
|
||||
/* For better i/o performance */
|
||||
@@ -21,7 +23,7 @@ Meteor.settings.useS3 = !!(
|
||||
s3Conf && s3Conf.key && s3Conf.secret && s3Conf.bucket && s3Conf.endpoint
|
||||
);
|
||||
|
||||
const bound = Meteor.bindEnvironment((callback) => {
|
||||
const bound = Meteor.bindEnvironment((callback) => {
|
||||
return callback();
|
||||
});
|
||||
|
||||
@@ -43,14 +45,14 @@ if (Meteor.isServer && Meteor.settings.useS3) {
|
||||
}
|
||||
});
|
||||
|
||||
createS3FilesCollection = function({
|
||||
createS3FilesCollection = function ({
|
||||
collectionName,
|
||||
storagePath,
|
||||
onBeforeUpload,
|
||||
onAfterUpload,
|
||||
debug = !Meteor.isProduction,
|
||||
debug,// = !Meteor.isProduction,
|
||||
allowClientCode = false,
|
||||
}){
|
||||
}) {
|
||||
const collection = new FilesCollection({
|
||||
collectionName,
|
||||
storagePath,
|
||||
@@ -58,7 +60,7 @@ if (Meteor.isServer && Meteor.settings.useS3) {
|
||||
onAfterUpload(fileRef) {
|
||||
// Call the provided afterUpload hook first
|
||||
onAfterUpload?.(fileRef);
|
||||
|
||||
|
||||
// Start moving files to AWS:S3
|
||||
// after fully received by the Meteor server
|
||||
|
||||
@@ -128,19 +130,19 @@ if (Meteor.isServer && Meteor.settings.useS3) {
|
||||
};
|
||||
|
||||
if (http.request.headers.range) {
|
||||
const vRef = fileRef.versions[version];
|
||||
let range = clone(http.request.headers.range);
|
||||
const vRef = fileRef.versions[version];
|
||||
let range = clone(http.request.headers.range);
|
||||
const array = range.split(/bytes=([0-9]*)-([0-9]*)/);
|
||||
const start = parseInt(array[1]);
|
||||
let end = parseInt(array[2]);
|
||||
let end = parseInt(array[2]);
|
||||
if (isNaN(end)) {
|
||||
// Request data from AWS:S3 by small chunks
|
||||
end = (start + this.chunkSize) - 1;
|
||||
end = (start + this.chunkSize) - 1;
|
||||
if (end >= vRef.size) {
|
||||
end = vRef.size - 1;
|
||||
end = vRef.size - 1;
|
||||
}
|
||||
}
|
||||
opts.Range = `bytes=${start}-${end}`;
|
||||
opts.Range = `bytes=${start}-${end}`;
|
||||
http.request.headers.range = `bytes=${start}-${end}`;
|
||||
}
|
||||
|
||||
@@ -198,9 +200,9 @@ if (Meteor.isServer && Meteor.settings.useS3) {
|
||||
_origRemove.call(this, search);
|
||||
};
|
||||
|
||||
collection.readJSONFile = async function(file){
|
||||
collection.readJSONFile = async function (file) {
|
||||
// If there is the pipepath, use s3 to get the file
|
||||
if (file?.versions?.original?.meta?.pipePath){
|
||||
if (file?.versions?.original?.meta?.pipePath) {
|
||||
const path = file.versions.original.meta.pipePath;
|
||||
const data = await s3.getObject({
|
||||
Bucket: s3Conf.bucket,
|
||||
@@ -217,14 +219,14 @@ if (Meteor.isServer && Meteor.settings.useS3) {
|
||||
return collection;
|
||||
}
|
||||
} else {
|
||||
createS3FilesCollection = function({
|
||||
createS3FilesCollection = function ({
|
||||
collectionName,
|
||||
storagePath,
|
||||
onBeforeUpload,
|
||||
onAfterUpload,
|
||||
debug = !Meteor.isProduction,
|
||||
debug,// = !Meteor.isProduction,
|
||||
allowClientCode = false,
|
||||
}){
|
||||
}) {
|
||||
const collection = new FilesCollection({
|
||||
collectionName,
|
||||
storagePath,
|
||||
@@ -236,7 +238,7 @@ if (Meteor.isServer && Meteor.settings.useS3) {
|
||||
|
||||
if (Meteor.isServer) {
|
||||
// Use the normal file system to read files
|
||||
collection.readJSONFile = async function(file){
|
||||
collection.readJSONFile = async function (file) {
|
||||
const fileString = await fsp.readFile(file.path, 'utf8');
|
||||
return JSON.parse(fileString);
|
||||
};
|
||||
|
||||
2
app/imports/api/files/server/s3.js
Normal file
2
app/imports/api/files/server/s3.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import S3 from 'aws-sdk/clients/s3';
|
||||
export default S3;
|
||||
@@ -17,7 +17,7 @@ import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||
var snackbar;
|
||||
if (Meteor.isClient) {
|
||||
snackbar = require(
|
||||
'/imports/ui/components/snackbars/SnackbarQueue.js'
|
||||
'/imports/client/ui/components/snackbars/SnackbarQueue.js'
|
||||
).snackbar
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { reorderDocs } from '/imports/api/parenting/order.js';
|
||||
var snackbar;
|
||||
if (Meteor.isClient) {
|
||||
snackbar = require(
|
||||
'/imports/ui/components/snackbars/SnackbarQueue.js'
|
||||
'/imports/client/ui/components/snackbars/SnackbarQueue.js'
|
||||
).snackbar
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { union, difference, sortBy, findLast } from 'lodash';
|
||||
|
||||
export function nodeArrayToTree(nodes){
|
||||
export function nodeArrayToTree(nodes) {
|
||||
// Store a dict and list of all the nodes
|
||||
let nodeIndex = {};
|
||||
let nodeList = [];
|
||||
nodes.forEach( node => {
|
||||
nodes.forEach(node => {
|
||||
let treeNode = {
|
||||
node: node,
|
||||
children: [],
|
||||
@@ -20,7 +20,7 @@ export function nodeArrayToTree(nodes){
|
||||
treeNode.node.ancestors,
|
||||
ancestor => !!nodeIndex[ancestor.id]
|
||||
);
|
||||
if (ancestorInForest){
|
||||
if (ancestorInForest) {
|
||||
nodeIndex[ancestorInForest.id].children.push(treeNode);
|
||||
} else {
|
||||
forest.push(treeNode);
|
||||
@@ -33,13 +33,13 @@ export function nodeArrayToTree(nodes){
|
||||
export default function nodesToTree({
|
||||
collection, ancestorId, filter, options = {},
|
||||
includeFilteredDocAncestors = false, includeFilteredDocDescendants = false
|
||||
}){
|
||||
}) {
|
||||
// Setup the filter
|
||||
let collectionFilter = {
|
||||
'ancestors.id': ancestorId,
|
||||
'removed': {$ne: true},
|
||||
'removed': { $ne: true },
|
||||
};
|
||||
if (filter){
|
||||
if (filter) {
|
||||
collectionFilter = {
|
||||
...collectionFilter,
|
||||
...filter,
|
||||
@@ -49,7 +49,7 @@ export default function nodesToTree({
|
||||
let collectionSort = {
|
||||
order: 1
|
||||
};
|
||||
if (options && options.sort){
|
||||
if (options && options.sort) {
|
||||
collectionSort = {
|
||||
...collectionSort,
|
||||
...options.sort,
|
||||
@@ -58,7 +58,7 @@ export default function nodesToTree({
|
||||
let collectionOptions = {
|
||||
sort: collectionSort,
|
||||
}
|
||||
if (options){
|
||||
if (options) {
|
||||
collectionOptions = {
|
||||
...collectionOptions,
|
||||
...options,
|
||||
@@ -74,10 +74,10 @@ export default function nodesToTree({
|
||||
let ancestors = [];
|
||||
let ancestorIds = [];
|
||||
let docIds = [];
|
||||
if (filter && (includeFilteredDocAncestors || includeFilteredDocDescendants)){
|
||||
if (filter && (includeFilteredDocAncestors || includeFilteredDocDescendants)) {
|
||||
docIds = docs.map(doc => doc._id)
|
||||
}
|
||||
if (filter && includeFilteredDocAncestors){
|
||||
if (filter && includeFilteredDocAncestors) {
|
||||
// Add all ancestor ids to an array
|
||||
docs.forEach(doc => {
|
||||
ancestorIds = union(ancestorIds, doc.ancestors.map(ref => ref.id));
|
||||
@@ -86,19 +86,19 @@ export default function nodesToTree({
|
||||
ancestorIds = difference(ancestorIds, docIds);
|
||||
// Get the docs from the collection, don't worry about `removed` docs,
|
||||
// if their descendant was not removed, neither are they
|
||||
ancestors = collection.find({_id: {$in: ancestorIds}}).map(doc => {
|
||||
ancestors = collection.find({ _id: { $in: ancestorIds } }).map(doc => {
|
||||
// Mark that the nodes are ancestors of the found nodes
|
||||
doc._ancestorOfMatchedDocument = true;
|
||||
return doc;
|
||||
});
|
||||
}
|
||||
let descendants = [];
|
||||
if (filter && includeFilteredDocDescendants){
|
||||
if (filter && includeFilteredDocDescendants) {
|
||||
let exludeIds = union(ancestorIds, docIds);
|
||||
descendants = collection.find({
|
||||
'_id': {$nin: exludeIds},
|
||||
'ancestors.id': {$in: docIds},
|
||||
'removed': {$ne: true},
|
||||
'_id': { $nin: exludeIds },
|
||||
'ancestors.id': { $in: docIds },
|
||||
'removed': { $ne: true },
|
||||
}).map(doc => {
|
||||
// Mark that the nodes are descendants of the found nodes
|
||||
doc._descendantOfMatchedDocument = true;
|
||||
|
||||
@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
|
||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
||||
|
||||
/*
|
||||
* Actions are things a character can do
|
||||
@@ -24,9 +25,17 @@ let ActionSchema = createPropertySchema({
|
||||
// long actions take longer than 1 round to cast
|
||||
actionType: {
|
||||
type: String,
|
||||
allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long'],
|
||||
allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long', 'event'],
|
||||
defaultValue: 'action',
|
||||
},
|
||||
// If the action type is an event, what is the variable name of that event?
|
||||
variableName: {
|
||||
type: String,
|
||||
optional: true,
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
min: 2,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
// Who is the action directed at
|
||||
target: {
|
||||
type: String,
|
||||
@@ -56,8 +65,10 @@ let ActionSchema = createPropertySchema({
|
||||
// How this action's uses are reset automatically
|
||||
reset: {
|
||||
type: String,
|
||||
allowedValues: ['longRest', 'shortRest'],
|
||||
optional: true,
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
min: 2,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
// Resources
|
||||
resources: {
|
||||
@@ -74,7 +85,7 @@ let ActionSchema = createPropertySchema({
|
||||
'resources.itemsConsumed.$._id': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
autoValue(){
|
||||
autoValue() {
|
||||
if (!this.isSet) return Random.id();
|
||||
}
|
||||
},
|
||||
@@ -101,7 +112,7 @@ let ActionSchema = createPropertySchema({
|
||||
'resources.attributesConsumed.$._id': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
autoValue(){
|
||||
autoValue() {
|
||||
if (!this.isSet) return Random.id();
|
||||
}
|
||||
},
|
||||
@@ -151,6 +162,12 @@ const ComputedOnlyActionSchema = createPropertySchema({
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
// Denormalised tag if event is overridden by one with the same variable name
|
||||
overridden: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
// Resources
|
||||
resources: {
|
||||
type: Object,
|
||||
@@ -218,4 +235,4 @@ const ComputedActionSchema = new SimpleSchema()
|
||||
.extend(ActionSchema)
|
||||
.extend(ComputedOnlyActionSchema);
|
||||
|
||||
export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema};
|
||||
export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema };
|
||||
|
||||
@@ -28,8 +28,7 @@ let AttributeSchema = createPropertySchema({
|
||||
'stat', // Speed, Armor Class
|
||||
'modifier', // Proficiency Bonus, displayed as +x
|
||||
'hitDice', // d12 hit dice
|
||||
'healthBar', // Hitpoints, Temporary Hitpoints, can take damage
|
||||
'bar', // Displayed as a health bar, can't take damage
|
||||
'healthBar', // Hitpoints, Temporary Hitpoints
|
||||
'resource', // Rages, sorcery points
|
||||
'spellSlot', // Level 1, 2, 3... spell slots
|
||||
'utility', // Aren't displayed, Jump height, Carry capacity
|
||||
@@ -129,7 +128,9 @@ let AttributeSchema = createPropertySchema({
|
||||
reset: {
|
||||
type: String,
|
||||
optional: true,
|
||||
allowedValues: ['shortRest', 'longRest'],
|
||||
regEx: VARIABLE_NAME_REGEX,
|
||||
min: 2,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,29 @@ let FolderSchema = new createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
max: STORAGE_LIMITS.name,
|
||||
optional: true,
|
||||
},
|
||||
groupStats: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
hideStatsGroup: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
tab: {
|
||||
type: String,
|
||||
optional: true,
|
||||
allowedValues: [
|
||||
'stats', 'features', 'actions', 'spells', 'inventory', 'journal', 'build'
|
||||
],
|
||||
},
|
||||
location: {
|
||||
type: String,
|
||||
optional: true,
|
||||
allowedValues: [
|
||||
'start', 'events', 'stats', 'skills', 'proficiencies', 'end'
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -17,6 +17,12 @@ let SpellListSchema = createPropertySchema({
|
||||
type: 'fieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
// The variable name of the ability this spell relies on
|
||||
ability: {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
// Calculation of The attack roll bonus used by spell attacks in this list
|
||||
attackRollBonus: {
|
||||
type: 'fieldToCompute',
|
||||
@@ -38,6 +44,12 @@ const ComputedOnlySpellListSchema = createPropertySchema({
|
||||
type: 'computedOnlyField',
|
||||
optional: true,
|
||||
},
|
||||
// Computed value determined by the ability
|
||||
abilityMod: {
|
||||
type: SimpleSchema.Integer,
|
||||
optional: true,
|
||||
removeBeforeCompute: true,
|
||||
},
|
||||
attackRollBonus: {
|
||||
type: 'computedOnlyField',
|
||||
optional: true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { includes } from 'lodash';
|
||||
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||
|
||||
function assertIdValid(userId) {
|
||||
@@ -50,7 +50,7 @@ export function assertEditPermission(doc, userId) {
|
||||
// Ensure the user is authorized for this specific document
|
||||
if (
|
||||
doc.owner === userId ||
|
||||
_.contains(doc.writers, userId)
|
||||
includes(doc.writers, userId)
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
@@ -82,11 +82,11 @@ export function assertCopyPermission(doc, userId) {
|
||||
// Ensure the user is authorized for this specific document
|
||||
if (
|
||||
doc.owner === userId ||
|
||||
_.contains(doc.writers, userId)
|
||||
includes(doc.writers, userId)
|
||||
) {
|
||||
return true;
|
||||
} else if (
|
||||
(_.contains(doc.readers, userId) || doc.public) &&
|
||||
(includes(doc.readers, userId) || doc.public) &&
|
||||
doc.readersCanCopy
|
||||
) {
|
||||
return true;
|
||||
@@ -134,8 +134,8 @@ export function assertViewPermission(doc, userId) {
|
||||
|
||||
if (
|
||||
doc.owner === userId ||
|
||||
_.contains(doc.readers, userId) ||
|
||||
_.contains(doc.writers, userId)
|
||||
includes(doc.readers, userId) ||
|
||||
includes(doc.writers, userId)
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import valueToCoins from '/imports/ui/utility/valueToCoins.js';
|
||||
import valueToCoins from '/imports/client/ui/utility/valueToCoins.js';
|
||||
|
||||
export default {
|
||||
props:{
|
||||
@@ -99,9 +99,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
|
||||
import isDarkColor from '/imports/client/ui/utility/isDarkColor.js';
|
||||
import vuetifyColors from 'vuetify/es5/util/colors';
|
||||
import { kebabToCamelCase, camelToKebabCase } from '/imports/ui/utility/swapCase.js';
|
||||
import { kebabToCamelCase, camelToKebabCase } from '/imports/client/ui/utility/swapCase.js';
|
||||
|
||||
function colorToHex(color, shade = 'base'){
|
||||
if (!color) return;
|
||||
@@ -32,7 +32,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import IncrementMenu from '/imports/ui/components/IncrementMenu.vue';
|
||||
import IncrementMenu from '/imports/client/ui/components/IncrementMenu.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
46
app/imports/client/ui/components/ResetSelector.vue
Normal file
46
app/imports/client/ui/components/ResetSelector.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<smart-select
|
||||
label="Reset"
|
||||
clearable
|
||||
style="flex-basis: 300px;"
|
||||
:hint="hint"
|
||||
:items="resetOptions"
|
||||
:value="value"
|
||||
:error-messages="errorMessages"
|
||||
:menu-props="{auto: true, lazy: true}"
|
||||
@change="(value, ack) => $emit('change', value, ack)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import createListOfProperties from '/imports/client/ui/properties/forms/shared/lists/createListOfProperties.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: [String, Number, Date, Array, Object, Boolean],
|
||||
errorMessages: [String, Array],
|
||||
hint: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
resetOptions() {
|
||||
const eventActions = createListOfProperties({
|
||||
type: 'action',
|
||||
actionType: 'event',
|
||||
}, true);
|
||||
const defaultEvents = [
|
||||
{
|
||||
text: 'Short rest',
|
||||
value: 'shortRest',
|
||||
}, {
|
||||
text: 'Long rest',
|
||||
value: 'longRest',
|
||||
}
|
||||
];
|
||||
return [...defaultEvents, ...eventActions];
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -64,7 +64,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import VerticalHex from '/imports/ui/components/VerticalHex.vue';
|
||||
import VerticalHex from '/imports/client/ui/components/VerticalHex.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -27,9 +27,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
|
||||
import getThemeColor from '/imports/ui/utility/getThemeColor.js';
|
||||
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
|
||||
import isDarkColor from '/imports/client/ui/utility/isDarkColor.js';
|
||||
import getThemeColor from '/imports/client/ui/utility/getThemeColor.js';
|
||||
import CardHighlight from '/imports/client/ui/components/CardHighlight.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -30,7 +30,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
|
||||
import SmartInput from '/imports/client/ui/components/global/SmartInputMixin.js';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
export default {
|
||||
45
app/imports/client/ui/components/global/DragHandle.vue
Normal file
45
app/imports/client/ui/components/global/DragHandle.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<v-icon
|
||||
class="handle"
|
||||
v-bind="$attrs"
|
||||
@click.stop="() => { }"
|
||||
@touchstart.native.stop="() => { }"
|
||||
@touchend.native="portalEvent"
|
||||
>
|
||||
mdi-drag
|
||||
</v-icon>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import { defer } from 'lodash'
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
portalEvent(e) {
|
||||
// Stop everything in the document listening for this touch event
|
||||
e.stopPropagation();
|
||||
// But also send it to straight to the root for draggable.js
|
||||
defer(() => {
|
||||
e.target.ownerDocument.dispatchEvent(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.handle {
|
||||
cursor: move !important;
|
||||
cursor: -webkit-grab !important;
|
||||
}
|
||||
.handle::after {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.sortable-drag.handle {
|
||||
cursor: move !important;
|
||||
cursor: -webkit-grabbing !important;
|
||||
}
|
||||
</style>
|
||||
@@ -78,8 +78,8 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import SvgIcon from '/imports/ui/components/global/SvgIcon.vue';
|
||||
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
|
||||
import SvgIcon from '/imports/client/ui/components/global/SvgIcon.vue';
|
||||
import SmartInput from '/imports/client/ui/components/global/SmartInputMixin.js';
|
||||
import { findIcons } from '/imports/api/icons/Icons.js';
|
||||
|
||||
export default {
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<script lang="js">
|
||||
import { debounce } from 'lodash';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
|
||||
|
||||
export default {
|
||||
inject: {
|
||||
@@ -23,6 +23,7 @@ export default {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
singleClick: Boolean,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -52,18 +53,23 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
click() {
|
||||
this.timesClicked += 1;
|
||||
this.debounceClicks();
|
||||
if (this.singleClick) {
|
||||
this.loading = true;
|
||||
} else {
|
||||
this.timesClicked += 1;
|
||||
this.debounceClicks();
|
||||
}
|
||||
this.$emit('click', this.acknowledgeChange);
|
||||
},
|
||||
clicks() {
|
||||
this.$emit('clicks', this.timesClicked, this.acknowledgeChange);
|
||||
this.loading = true;
|
||||
this.$emit('clicks', this.timesClicked, this.acknowledgeChange);
|
||||
this.timesClicked = 0;
|
||||
},
|
||||
acknowledgeChange(error){
|
||||
this.loading = false;
|
||||
if (error) {
|
||||
console.error(error)
|
||||
snackbar({ text: error.reason || error.message || error.toString() });
|
||||
}
|
||||
},
|
||||
@@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
|
||||
import SmartInput from '/imports/client/ui/components/global/SmartInputMixin.js';
|
||||
|
||||
export default {
|
||||
mixins: [SmartInput],
|
||||
@@ -21,7 +21,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
|
||||
import SmartInput from '/imports/client/ui/components/global/SmartInputMixin.js';
|
||||
|
||||
export default {
|
||||
mixins: [SmartInput],
|
||||
@@ -23,7 +23,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
|
||||
import SmartInput from '/imports/client/ui/components/global/SmartInputMixin.js';
|
||||
|
||||
export default {
|
||||
mixins: [SmartInput],
|
||||
@@ -10,6 +10,9 @@
|
||||
:disabled="isDisabled"
|
||||
:outlined="!regular"
|
||||
@change="change"
|
||||
@input="e => $emit('input', e)"
|
||||
@end="e => $emit('end', e)"
|
||||
@start="e => $emit('start', e)"
|
||||
@focus="focused = true"
|
||||
@blur="focused = false"
|
||||
>
|
||||
@@ -23,12 +26,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
|
||||
import SmartInput from '/imports/client/ui/components/global/SmartInputMixin.js';
|
||||
|
||||
export default {
|
||||
mixins: [SmartInput],
|
||||
props: {
|
||||
regular: Boolean,
|
||||
},
|
||||
};
|
||||
export default {
|
||||
mixins: [SmartInput],
|
||||
props: {
|
||||
regular: Boolean,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
|
||||
import SmartInput from '/imports/client/ui/components/global/SmartInputMixin.js';
|
||||
|
||||
export default {
|
||||
mixins: [SmartInput],
|
||||
@@ -14,7 +14,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
|
||||
import SmartInput from '/imports/client/ui/components/global/SmartInputMixin.js';
|
||||
|
||||
export default {
|
||||
mixins: [SmartInput],
|
||||
@@ -20,7 +20,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
|
||||
import SmartInput from '/imports/client/ui/components/global/SmartInputMixin.js';
|
||||
|
||||
export default {
|
||||
mixins: [SmartInput],
|
||||
27
app/imports/client/ui/components/global/globalIndex.js
Normal file
27
app/imports/client/ui/components/global/globalIndex.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import Vue from 'vue';
|
||||
// Global components
|
||||
import DatePicker from '/imports/client/ui/components/global/DatePicker.vue';
|
||||
import DragHandle from '/imports/client/ui/components/global/DragHandle.vue';
|
||||
import IconPicker from '/imports/client/ui/components/global/IconPicker.vue';
|
||||
import TextField from '/imports/client/ui/components/global/TextField.vue';
|
||||
import TextArea from '/imports/client/ui/components/global/TextArea.vue';
|
||||
import SmartSelect from '/imports/client/ui/components/global/SmartSelect.vue';
|
||||
import SmartBtn from '/imports/client/ui/components/global/SmartBtn.vue';
|
||||
import SmartCombobox from '/imports/client/ui/components/global/SmartCombobox.vue';
|
||||
import SmartCheckbox from '/imports/client/ui/components/global/SmartCheckbox.vue';
|
||||
import SmartSwitch from '/imports/client/ui/components/global/SmartSwitch.vue';
|
||||
import SvgIcon from '/imports/client/ui/components/global/SvgIcon.vue';
|
||||
import SmartSlider from '/imports/client/ui/components/global/SmartSlider.vue';
|
||||
|
||||
Vue.component('DatePicker', DatePicker);
|
||||
Vue.component('DragHandle', DragHandle);
|
||||
Vue.component('IconPicker', IconPicker);
|
||||
Vue.component('TextField', TextField);
|
||||
Vue.component('TextArea', TextArea);
|
||||
Vue.component('SmartSelect', SmartSelect);
|
||||
Vue.component('SmartBtn', SmartBtn);
|
||||
Vue.component('SmartCombobox', SmartCombobox);
|
||||
Vue.component('SmartCheckbox', SmartCheckbox);
|
||||
Vue.component('SmartSlider', SmartSlider);
|
||||
Vue.component('SmartSwitch', SmartSwitch);
|
||||
Vue.component('SvgIcon', SvgIcon);
|
||||
@@ -162,11 +162,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
|
||||
import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
|
||||
import isDarkColor from '/imports/client/ui/utility/isDarkColor.js';
|
||||
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
import ColorPicker from '/imports/ui/components/ColorPicker.vue';
|
||||
import getThemeColor from '/imports/ui/utility/getThemeColor.js';
|
||||
import ColorPicker from '/imports/client/ui/components/ColorPicker.vue';
|
||||
import getThemeColor from '/imports/client/ui/utility/getThemeColor.js';
|
||||
import PROPERTIES from '/imports/constants/PROPERTIES.js';
|
||||
|
||||
export default {
|
||||
@@ -48,7 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||
import numberToSignedString from '../../../../api/utility/numberToSignedString.js';
|
||||
export default {
|
||||
props: {
|
||||
attributeVarName: {
|
||||
@@ -40,8 +40,8 @@
|
||||
|
||||
<script lang="js">
|
||||
// Modified from https://gitlab.com/tozd/vue/snackbar-queue
|
||||
import { globalState } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import LogContent from '/imports/ui/log/LogContent.vue';
|
||||
import { globalState } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
|
||||
import LogContent from '/imports/client/ui/log/LogContent.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -30,14 +30,12 @@
|
||||
:class="{'ml-4': startExpanded}"
|
||||
style="flex-grow: 0;"
|
||||
>
|
||||
<v-icon
|
||||
<drag-handle
|
||||
v-if="organize"
|
||||
class="handle mr-2"
|
||||
class="mr-2"
|
||||
:class="selected && 'primary--text'"
|
||||
:disabled="expanded"
|
||||
>
|
||||
mdi-drag
|
||||
</v-icon>
|
||||
/>
|
||||
<!--{{node && node.order}}-->
|
||||
<tree-node-view
|
||||
:model="node"
|
||||
@@ -85,7 +83,7 @@
|
||||
**/
|
||||
import { canBeParent } from '/imports/api/parenting/parenting.js';
|
||||
import { getPropertyIcon } from '/imports/constants/PROPERTIES.js';
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import TreeNodeView from '/imports/client/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import { some } from 'lodash';
|
||||
|
||||
export default {
|
||||
@@ -177,10 +175,6 @@ export default {
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
.handle {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.empty .drag-area {
|
||||
box-shadow: -2px 0px 0px 0px rgb(128, 128, 128, 0.4);
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
v-model="displayedChildren"
|
||||
class="drag-area"
|
||||
:group="group"
|
||||
:move="move"
|
||||
:animation="200"
|
||||
ghost-class="ghost"
|
||||
draggable=".item"
|
||||
@@ -27,14 +26,13 @@
|
||||
@selected="e => $emit('selected', e)"
|
||||
@reordered="e => $emit('reordered', e)"
|
||||
@reorganized="e => $emit('reorganized', e)"
|
||||
@dragstart.native="e => e.dataTransfer.setData('cow', child.node && child.node.name)"
|
||||
/>
|
||||
</draggable>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import draggable from 'vuedraggable';
|
||||
import TreeNode from '/imports/ui/components/tree/TreeNode.vue';
|
||||
import TreeNode from '/imports/client/ui/components/tree/TreeNode.vue';
|
||||
import { isParentAllowed } from '/imports/api/parenting/parenting.js';
|
||||
|
||||
export default {
|
||||
@@ -39,6 +39,11 @@
|
||||
:input-value="model.settings.hideUnusedStats"
|
||||
@change="value => $emit('change', {path: ['settings','hideUnusedStats'], value: !!value})"
|
||||
/>
|
||||
<v-switch
|
||||
label="Hide rest buttons"
|
||||
:input-value="model.settings.hideRestButtons"
|
||||
@change="value => $emit('change', {path: ['settings','hideRestButtons'], value: !!value})"
|
||||
/>
|
||||
<v-switch
|
||||
label="Show spells tab"
|
||||
:input-value="!model.settings.hideSpellsTab"
|
||||
@@ -121,8 +126,8 @@
|
||||
|
||||
<script lang="js">
|
||||
import { union, without, debounce } from 'lodash';
|
||||
import FormSection, { FormSections } from '/imports/ui/properties/forms/shared/FormSection.vue';
|
||||
import LibraryList from '/imports/ui/library/LibraryList.vue';
|
||||
import FormSection, { FormSections } from '/imports/client/ui/properties/forms/shared/FormSection.vue';
|
||||
import LibraryList from '/imports/client/ui/library/LibraryList.vue';
|
||||
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
|
||||
import { changeAllowedLibraries, toggleAllUserLibraries } from '/imports/api/creature/creatures/methods/changeAllowedLibraries.js';
|
||||
|
||||
@@ -34,10 +34,10 @@
|
||||
<script lang="js">
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import updateCreature from '/imports/api/creature/creatures/methods/updateCreature.js';
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import CreatureForm from '/imports/ui/creature/CreatureForm.vue'
|
||||
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
|
||||
import CreatureForm from '/imports/client/ui/creature/CreatureForm.vue'
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import ColorPicker from '/imports/ui/components/ColorPicker.vue';
|
||||
import ColorPicker from '/imports/client/ui/components/ColorPicker.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -57,14 +57,14 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureFolders from '/imports/api/creature/creatureFolders/CreatureFolders.js';
|
||||
import CreatureFolderList from '/imports/ui/creature/creatureList/CreatureFolderList.vue';
|
||||
import CreatureFolderList from '/imports/client/ui/creature/creatureList/CreatureFolderList.vue';
|
||||
import ArchiveCreatureFiles from '/imports/api/creature/archive/ArchiveCreatureFiles.js';
|
||||
import archiveCreatureToFile from '/imports/api/creature/archive/methods/archiveCreatureToFile.js';
|
||||
import restoreCreatureFromFile from '/imports/api/creature/archive/methods/restoreCreatureFromFile.js';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
|
||||
import { uniq, flatten } from 'lodash';
|
||||
import { characterSlotsRemaining } from '/imports/api/creature/creatures/methods/assertHasCharacterSlots.js';
|
||||
|
||||
@@ -123,13 +123,13 @@
|
||||
* the tree view shows off the full character structure, and where each part of
|
||||
* character comes from.
|
||||
**/
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import FillSlotButton from '/imports/ui/creature/buildTree/FillSlotButton.vue';
|
||||
import TreeNodeView from '/imports/client/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import FillSlotButton from '/imports/client/ui/creature/buildTree/FillSlotButton.vue';
|
||||
import { some } from 'lodash';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
|
||||
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
|
||||
import restoreProperty from '/imports/api/creature/creatureProperties/methods/restoreProperty.js';
|
||||
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
|
||||
import getPropertyTitle from '/imports/client/ui/properties/shared/getPropertyTitle.js';
|
||||
|
||||
export default {
|
||||
name: 'BuildTreeNode',
|
||||
@@ -12,7 +12,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import BuildTreeNode from '/imports/ui/creature/buildTree/BuildTreeNode.vue';
|
||||
import BuildTreeNode from '/imports/client/ui/creature/buildTree/BuildTreeNode.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -110,11 +110,11 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
|
||||
import { defer, union, without } from 'lodash';
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
|
||||
import insertCreature from '/imports/api/creature/creatures/methods/insertCreature.js';
|
||||
import LibraryList from '/imports/ui/library/LibraryList.vue';
|
||||
import LibraryList from '/imports/client/ui/library/LibraryList.vue';
|
||||
import LibraryCollections from '/imports/api/library/LibraryCollections.js';
|
||||
|
||||
export default {
|
||||
@@ -32,9 +32,9 @@
|
||||
|
||||
<script lang="js">
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
|
||||
import removeCreature from '/imports/api/creature/creatures/methods/removeCreature.js';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -51,11 +51,14 @@
|
||||
<features-tab :creature-id="creatureId" />
|
||||
</v-tab-item>
|
||||
<v-tab-item>
|
||||
<inventory-tab :creature-id="creatureId" />
|
||||
<actions-tab :creature-id="creatureId" />
|
||||
</v-tab-item>
|
||||
<v-tab-item v-if="!creature.settings.hideSpellsTab">
|
||||
<spells-tab :creature-id="creatureId" />
|
||||
</v-tab-item>
|
||||
<v-tab-item>
|
||||
<inventory-tab :creature-id="creatureId" />
|
||||
</v-tab-item>
|
||||
<v-tab-item>
|
||||
<character-tab :creature-id="creatureId" />
|
||||
</v-tab-item>
|
||||
@@ -68,6 +71,60 @@
|
||||
</v-tabs-items>
|
||||
</div>
|
||||
</v-fade-transition>
|
||||
<character-sheet-fab
|
||||
v-if="$vuetify.breakpoint.xsOnly"
|
||||
direction="top"
|
||||
fixed
|
||||
bottom
|
||||
right
|
||||
class="character-sheet-bottom-fab"
|
||||
:edit-permission="editPermission"
|
||||
/>
|
||||
<v-bottom-navigation
|
||||
v-if="$vuetify.breakpoint.xsOnly && creature && creature.settings"
|
||||
app
|
||||
shift
|
||||
mandatory
|
||||
class="bottom-nav-btns"
|
||||
:value="$store.getters.tabById($route.params.id)"
|
||||
@change="e => $store.commit(
|
||||
'setTabForCharacterSheet',
|
||||
{id: $route.params.id, tab: e}
|
||||
)"
|
||||
>
|
||||
<v-btn>
|
||||
<span>Stats</span>
|
||||
<v-icon>mdi-chart-box</v-icon>
|
||||
</v-btn>
|
||||
<v-btn>
|
||||
<span>Features</span>
|
||||
<v-icon>mdi-text</v-icon>
|
||||
</v-btn>
|
||||
<v-btn>
|
||||
<span>Actions</span>
|
||||
<v-icon>mdi-lightning-bolt</v-icon>
|
||||
</v-btn>
|
||||
<v-btn>
|
||||
<span v-if="!creature.settings.hideSpellsTab">Spells</span>
|
||||
<v-icon>mdi-fire</v-icon>
|
||||
</v-btn>
|
||||
<v-btn>
|
||||
<span>Inventory</span>
|
||||
<v-icon>mdi-cube</v-icon>
|
||||
</v-btn>
|
||||
<v-btn>
|
||||
<span>Journal</span>
|
||||
<v-icon>mdi-book-open-variant</v-icon>
|
||||
</v-btn>
|
||||
<v-btn>
|
||||
<span>Build</span>
|
||||
<v-icon>mdi-wrench</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="creature.settings.showTreeTab">
|
||||
<span>Tree</span>
|
||||
<v-icon>mdi-file-tree</v-icon>
|
||||
</v-btn>
|
||||
</v-bottom-navigation>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -75,26 +132,30 @@
|
||||
//TODO add a "no character found" screen if shown on a false address
|
||||
// or on a character the user does not have permission to view
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import StatsTab from '/imports/ui/creature/character/characterSheetTabs/StatsTab.vue';
|
||||
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 CharacterTab from '/imports/ui/creature/character/characterSheetTabs/JournalTab.vue';
|
||||
import BuildTab from '/imports/ui/creature/character/characterSheetTabs/BuildTab.vue';
|
||||
import TreeTab from '/imports/ui/creature/character/characterSheetTabs/TreeTab.vue';
|
||||
import StatsTab from '/imports/client/ui/creature/character/characterSheetTabs/StatsTab.vue';
|
||||
import FeaturesTab from '/imports/client/ui/creature/character/characterSheetTabs/FeaturesTab.vue';
|
||||
import InventoryTab from '/imports/client/ui/creature/character/characterSheetTabs/InventoryTab.vue';
|
||||
import SpellsTab from '/imports/client/ui/creature/character/characterSheetTabs/SpellsTab.vue';
|
||||
import CharacterTab from '/imports/client/ui/creature/character/characterSheetTabs/JournalTab.vue';
|
||||
import BuildTab from '/imports/client/ui/creature/character/characterSheetTabs/BuildTab.vue';
|
||||
import TreeTab from '/imports/client/ui/creature/character/characterSheetTabs/TreeTab.vue';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import CreatureLogs from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
|
||||
import CharacterSheetFab from '/imports/client/ui/creature/character/CharacterSheetFab.vue';
|
||||
import ActionsTab from '/imports/client/ui/creature/character/characterSheetTabs/ActionsTab.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StatsTab,
|
||||
FeaturesTab,
|
||||
InventoryTab,
|
||||
ActionsTab,
|
||||
SpellsTab,
|
||||
InventoryTab,
|
||||
CharacterTab,
|
||||
BuildTab,
|
||||
TreeTab,
|
||||
CharacterSheetFab,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
@@ -171,6 +232,19 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bottom-nav-btns > .v-btn{
|
||||
min-width: 0 !important;
|
||||
padding: 0 !important;
|
||||
flex: 1 1 auto !important;
|
||||
font-size: 0.6rem !important;
|
||||
}
|
||||
.character-sheet-bottom-fab {
|
||||
z-index: 5;
|
||||
bottom: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.character-sheet .v-window-item {
|
||||
min-height: calc(100vh - 96px);
|
||||
@@ -1,7 +1,7 @@
|
||||
<template lang="html">
|
||||
<v-speed-dial
|
||||
v-model="fab"
|
||||
direction="bottom"
|
||||
v-bind="$attrs"
|
||||
:style="!speedDials ? 'visibility: hidden;' : ''"
|
||||
>
|
||||
<template #activator>
|
||||
@@ -39,7 +39,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import LabeledFab from '/imports/ui/components/LabeledFab.vue';
|
||||
import LabeledFab from '/imports/client/ui/components/LabeledFab.vue';
|
||||
import { getHighestOrder } from '/imports/api/parenting/order.js';
|
||||
import insertProperty from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
@@ -96,16 +96,6 @@
|
||||
}, 400);
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
'stats',
|
||||
'features',
|
||||
'inventory',
|
||||
'spells',
|
||||
'journal',
|
||||
'build',
|
||||
'tree',
|
||||
];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LabeledFab,
|
||||
@@ -120,21 +110,18 @@
|
||||
creatureId(){
|
||||
return this.$route.params.id;
|
||||
},
|
||||
tabNumber(){
|
||||
let tabNumber = this.$store.getters.tabById(this.creatureId);
|
||||
if (this.hideSpellsTab && tabNumber > 2){
|
||||
tabNumber += 1;
|
||||
}
|
||||
return tabNumber;
|
||||
tabName(){
|
||||
return this.$store.getters.tabNameById(this.creatureId);
|
||||
},
|
||||
speedDials(){
|
||||
return this.speedDialsByTab[tabs[this.tabNumber]];
|
||||
return this.speedDialsByTab[this.tabName];
|
||||
},
|
||||
speedDialsByTab() { return {
|
||||
'stats': ['attribute', 'skill', 'action', 'buff'],
|
||||
'stats': ['attribute', 'skill', 'buff'],
|
||||
'features': ['feature'],
|
||||
'inventory': ['item', 'container'],
|
||||
'spells': ['spellList', 'spell'],
|
||||
'actions': ['action'],
|
||||
'inventory': ['item', 'container'],
|
||||
'journal': ['note'],
|
||||
'tree': [null],
|
||||
};},
|
||||
@@ -10,7 +10,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CharacterLog from '/imports/ui/log/CharacterLog.vue';
|
||||
import CharacterLog from '/imports/client/ui/log/CharacterLog.vue';
|
||||
export default {
|
||||
components: {
|
||||
CharacterLog,
|
||||
@@ -6,8 +6,8 @@
|
||||
:dark="isDark"
|
||||
:light="!isDark"
|
||||
clipped-right
|
||||
extended
|
||||
tabs
|
||||
:extended="$vuetify.breakpoint.smAndUp"
|
||||
:tabs="$vuetify.breakpoint.smAndUp"
|
||||
dense
|
||||
>
|
||||
<v-app-bar-nav-icon @click="toggleDrawer" />
|
||||
@@ -64,11 +64,14 @@
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-app-bar-nav-icon @click="toggleRightDrawer" />
|
||||
<v-app-bar-nav-icon @click="toggleRightDrawer">
|
||||
<v-icon>mdi-forum</v-icon>
|
||||
</v-app-bar-nav-icon>
|
||||
</template>
|
||||
</v-layout>
|
||||
</v-fade-transition>
|
||||
<v-fade-transition
|
||||
v-if="$vuetify.breakpoint.smAndUp"
|
||||
slot="extension"
|
||||
mode="out-in"
|
||||
>
|
||||
@@ -102,11 +105,14 @@
|
||||
Features
|
||||
</v-tab>
|
||||
<v-tab>
|
||||
Inventory
|
||||
Actions
|
||||
</v-tab>
|
||||
<v-tab v-if="!creature.settings.hideSpellsTab">
|
||||
Spells
|
||||
</v-tab>
|
||||
<v-tab>
|
||||
Inventory
|
||||
</v-tab>
|
||||
<v-tab>
|
||||
Journal
|
||||
</v-tab>
|
||||
@@ -119,7 +125,8 @@
|
||||
</v-tabs>
|
||||
<v-spacer />
|
||||
<character-sheet-fab
|
||||
class="character-sheet-fab"
|
||||
direction="bottom"
|
||||
class="character-sheet-extension-fab"
|
||||
:edit-permission="editPermission"
|
||||
/>
|
||||
</div>
|
||||
@@ -133,10 +140,10 @@ import removeCreature from '/imports/api/creature/creatures/methods/removeCreatu
|
||||
import { mapMutations } from 'vuex';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import { updateUserSharePermissions } from '/imports/api/sharing/sharing.js';
|
||||
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
|
||||
import CharacterSheetFab from '/imports/ui/creature/character/CharacterSheetFab.vue';
|
||||
import getThemeColor from '/imports/ui/utility/getThemeColor.js';
|
||||
import SharedIcon from '/imports/ui/components/SharedIcon.vue';
|
||||
import isDarkColor from '/imports/client/ui/utility/isDarkColor.js';
|
||||
import CharacterSheetFab from '/imports/client/ui/creature/character/CharacterSheetFab.vue';
|
||||
import getThemeColor from '/imports/client/ui/utility/getThemeColor.js';
|
||||
import SharedIcon from '/imports/client/ui/components/SharedIcon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -250,7 +257,7 @@ export default {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.character-sheet-fab {
|
||||
.character-sheet-extension-fab {
|
||||
bottom: -24px;
|
||||
right: 8px;
|
||||
margin-left: 16px;
|
||||
@@ -65,12 +65,12 @@
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import PropertyToolbar from '/imports/ui/components/propertyToolbar.vue';
|
||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||
import PropertyToolbar from '/imports/client/ui/components/propertyToolbar.vue';
|
||||
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
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 propertyFormIndex from '/imports/client/ui/properties/forms/shared/propertyFormIndex.js';
|
||||
import propertyViewerIndex from '/imports/client/ui/properties/viewers/shared/propertyViewerIndex.js';
|
||||
import CreaturePropertiesTree from '/imports/client/ui/creature/creatureProperties/CreaturePropertiesTree.vue';
|
||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||
import { getHighestOrder } from '/imports/api/parenting/order.js';
|
||||
import insertProperty from '/imports/api/creature/creatureProperties/methods/insertProperty.js';
|
||||
@@ -22,7 +22,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
|
||||
import CardHighlight from '/imports/client/ui/components/CardHighlight.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -0,0 +1,73 @@
|
||||
<template lang="html">
|
||||
<div
|
||||
class="actions-tab ma-2"
|
||||
>
|
||||
<column-layout wide-columns>
|
||||
<folder-group-card
|
||||
v-for="folder in startFolders"
|
||||
:key="folder._id"
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
<div
|
||||
v-for="action in actions"
|
||||
:key="action._id"
|
||||
class="action"
|
||||
>
|
||||
<action-card
|
||||
:model="action"
|
||||
:data-id="action._id"
|
||||
@click="clickProperty({_id: action._id})"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
/>
|
||||
</div>
|
||||
<folder-group-card
|
||||
v-for="folder in endFolders"
|
||||
:key="folder._id"
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
</column-layout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import ColumnLayout from '/imports/client/ui/components/ColumnLayout.vue';
|
||||
import ActionCard from '/imports/client/ui/properties/components/actions/ActionCard.vue';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import tabFoldersMixin from '/imports/client/ui/properties/components/folders/tabFoldersMixin.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ColumnLayout,
|
||||
ActionCard,
|
||||
},
|
||||
mixins: [tabFoldersMixin],
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() { return {
|
||||
tabName: 'actions',
|
||||
}},
|
||||
meteor: {
|
||||
actions() {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
type: 'action',
|
||||
actionType: { $ne: 'event' },
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
}, {
|
||||
sort: { actionType: 1, order: 1 },
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -13,9 +13,19 @@
|
||||
</v-row>
|
||||
<v-row dense>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="8"
|
||||
lg="6"
|
||||
v-for="folder in startFolders"
|
||||
:key="folder._id"
|
||||
v-bind="cols"
|
||||
>
|
||||
<folder-group-card
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-bind="cols"
|
||||
>
|
||||
<v-card class="pb-4">
|
||||
<v-card-title style="height: 68px;">
|
||||
@@ -81,9 +91,7 @@
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="4"
|
||||
lg="6"
|
||||
v-bind="cols"
|
||||
>
|
||||
<v-card class="class-details mb-2">
|
||||
<v-card-title
|
||||
@@ -174,6 +182,18 @@
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-for="folder in endFolders"
|
||||
:key="folder._id"
|
||||
v-bind="cols"
|
||||
>
|
||||
<folder-group-card
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
@@ -182,14 +202,15 @@
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
|
||||
import BuildTreeNodeList from '/imports/ui/creature/buildTree/BuildTreeNodeList.vue';
|
||||
import SlotCardsToFill from '/imports/ui/creature/slots/SlotCardsToFill.vue';
|
||||
import CreatureVariables from '../../../../api/creature/creatures/CreatureVariables';
|
||||
import BuildTreeNodeList from '/imports/client/ui/creature/buildTree/BuildTreeNodeList.vue';
|
||||
import SlotCardsToFill from '/imports/client/ui/creature/slots/SlotCardsToFill.vue';
|
||||
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
|
||||
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
|
||||
import CharacterErrors from '/imports/ui/creature/character/errors/CharacterErrors.vue';
|
||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||
import CharacterErrors from '/imports/client/ui/creature/character/errors/CharacterErrors.vue';
|
||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
|
||||
import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js';
|
||||
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
|
||||
import getPropertyTitle from '/imports/client/ui/properties/shared/getPropertyTitle.js';
|
||||
import tabFoldersMixin from '/imports/client/ui/properties/components/folders/tabFoldersMixin.js';
|
||||
|
||||
function traverse(tree, callback, parents = []){
|
||||
tree.forEach(node => {
|
||||
@@ -204,12 +225,23 @@ export default {
|
||||
BuildTreeNodeList,
|
||||
SlotCardsToFill,
|
||||
},
|
||||
mixins: [tabFoldersMixin],
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabName: 'build',
|
||||
cols: {
|
||||
cols: '12',
|
||||
md: '6',
|
||||
xl: '4',
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
highestLevels(){
|
||||
let highestLevels = {};
|
||||
@@ -1,6 +1,14 @@
|
||||
<template lang="html">
|
||||
<div class="features">
|
||||
<column-layout wide-columns>
|
||||
<folder-group-card
|
||||
v-for="folder in startFolders"
|
||||
:key="folder._id"
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
<div
|
||||
v-for="feature in features"
|
||||
:key="feature._id"
|
||||
@@ -11,26 +19,41 @@
|
||||
@click="featureClicked(feature)"
|
||||
/>
|
||||
</div>
|
||||
<folder-group-card
|
||||
v-for="folder in endFolders"
|
||||
:key="folder._id"
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
</column-layout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
|
||||
import FeatureCard from '/imports/ui/properties/components/features/FeatureCard.vue';
|
||||
import ColumnLayout from '/imports/client/ui/components/ColumnLayout.vue';
|
||||
import FeatureCard from '/imports/client/ui/properties/components/features/FeatureCard.vue';
|
||||
import tabFoldersMixin from '/imports/client/ui/properties/components/folders/tabFoldersMixin.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ColumnLayout,
|
||||
FeatureCard,
|
||||
},
|
||||
mixins: [tabFoldersMixin],
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabName: 'features',
|
||||
};
|
||||
},
|
||||
meteor: {
|
||||
features() {
|
||||
return CreatureProperties.find({
|
||||
@@ -1,6 +1,14 @@
|
||||
<template lang="html">
|
||||
<div class="inventory">
|
||||
<column-layout wide-columns>
|
||||
<folder-group-card
|
||||
v-for="folder in startFolders"
|
||||
:key="folder._id"
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
<div>
|
||||
<v-card>
|
||||
<v-list>
|
||||
@@ -85,6 +93,14 @@
|
||||
>
|
||||
<container-card :model="container" />
|
||||
</div>
|
||||
<folder-group-card
|
||||
v-for="folder in endFolders"
|
||||
:key="folder._id"
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
</column-layout>
|
||||
</div>
|
||||
</template>
|
||||
@@ -92,15 +108,16 @@
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
|
||||
import ContainerCard from '/imports/ui/properties/components/inventory/ContainerCard.vue';
|
||||
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
|
||||
import ItemList from '/imports/ui/properties/components/inventory/ItemList.vue';
|
||||
import ColumnLayout from '/imports/client/ui/components/ColumnLayout.vue';
|
||||
import ContainerCard from '/imports/client/ui/properties/components/inventory/ContainerCard.vue';
|
||||
import ToolbarCard from '/imports/client/ui/components/ToolbarCard.vue';
|
||||
import ItemList from '/imports/client/ui/properties/components/inventory/ItemList.vue';
|
||||
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
|
||||
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
|
||||
import CoinValue from '/imports/ui/components/CoinValue.vue';
|
||||
import CoinValue from '/imports/client/ui/components/CoinValue.vue';
|
||||
import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js';
|
||||
import CreatureVariables from '../../../../api/creature/creatures/CreatureVariables';
|
||||
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
|
||||
import tabFoldersMixin from '/imports/client/ui/properties/components/folders/tabFoldersMixin.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -110,6 +127,7 @@ export default {
|
||||
ItemList,
|
||||
CoinValue,
|
||||
},
|
||||
mixins: [tabFoldersMixin],
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
@@ -119,7 +137,8 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
organize: false,
|
||||
}
|
||||
tabName: 'inventory',
|
||||
};
|
||||
},
|
||||
meteor: {
|
||||
containers() {
|
||||
@@ -216,15 +235,6 @@ export default {
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickProperty(_id) {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `tree-node-${_id}`,
|
||||
data: { _id },
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<template>
|
||||
<div class="build-tab">
|
||||
<column-layout wide-columns>
|
||||
<folder-group-card
|
||||
v-for="folder in startFolders"
|
||||
:key="folder._id"
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
<div>
|
||||
<creature-summary :creature="creature" />
|
||||
</div>
|
||||
@@ -12,16 +20,25 @@
|
||||
:model="note"
|
||||
/>
|
||||
</div>
|
||||
<folder-group-card
|
||||
v-for="folder in endFolders"
|
||||
:key="folder._id"
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
</column-layout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
|
||||
import ColumnLayout from '/imports/client/ui/components/ColumnLayout.vue';
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import NoteCard from '/imports/ui/properties/components/persona/NoteCard.vue';
|
||||
import CreatureSummary from '/imports/ui/creature/character/CreatureSummary.vue';
|
||||
import NoteCard from '/imports/client/ui/properties/components/persona/NoteCard.vue';
|
||||
import CreatureSummary from '/imports/client/ui/creature/character/CreatureSummary.vue';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import tabFoldersMixin from '/imports/client/ui/properties/components/folders/tabFoldersMixin.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -29,12 +46,18 @@ export default {
|
||||
CreatureSummary,
|
||||
NoteCard,
|
||||
},
|
||||
mixins: [tabFoldersMixin],
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabName: 'journal',
|
||||
};
|
||||
},
|
||||
meteor: {
|
||||
notes(){
|
||||
return CreatureProperties.find({
|
||||
@@ -0,0 +1,153 @@
|
||||
<template lang="html">
|
||||
<div class="spells">
|
||||
<column-layout wide-columns>
|
||||
<folder-group-card
|
||||
v-for="folder in startFolders"
|
||||
:key="folder._id"
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
<div
|
||||
v-if="spellSlots && spellSlots.length || hasSpells"
|
||||
class="spell-slots"
|
||||
>
|
||||
<spell-slot-card
|
||||
:creature-id="creatureId"
|
||||
:spell-slots="spellSlots"
|
||||
:has-spells="hasSpells"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="spellsWithoutList.length">
|
||||
<v-card>
|
||||
<spell-list
|
||||
:spells="spellsWithoutList"
|
||||
:parent-ref="{id: creatureId, collection: 'creatures'}"
|
||||
/>
|
||||
</v-card>
|
||||
</div>
|
||||
<div
|
||||
v-for="spellList in spellListsWithoutAncestorSpellLists"
|
||||
:key="spellList._id"
|
||||
>
|
||||
<spellList-card
|
||||
:model="spellList"
|
||||
:organize="organize"
|
||||
/>
|
||||
</div>
|
||||
<folder-group-card
|
||||
v-for="folder in endFolders"
|
||||
:key="folder._id"
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
</column-layout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import ColumnLayout from '/imports/client/ui/components/ColumnLayout.vue';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import SpellListCard from '/imports/client/ui/properties/components/spells/SpellListCard.vue';
|
||||
import SpellList from '/imports/client/ui/properties/components/spells/SpellList.vue';
|
||||
import tabFoldersMixin from '/imports/client/ui/properties/components/folders/tabFoldersMixin.js';
|
||||
import SpellSlotCard from '/imports/client/ui/properties/components/attributes/SpellSlotCard.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ColumnLayout,
|
||||
SpellList,
|
||||
SpellListCard,
|
||||
SpellSlotCard,
|
||||
},
|
||||
mixins: [tabFoldersMixin],
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
organize: false,
|
||||
tabName: 'spells',
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
spellSlots() {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
inactive: { $ne: true },
|
||||
removed: { $ne: true },
|
||||
overridden: { $ne: true },
|
||||
type: 'attribute',
|
||||
attributeType: 'spellSlot',
|
||||
$nor: [
|
||||
{ hideWhenTotalZero: true, total: 0 },
|
||||
{ hideWhenValueZero: true, value: 0 },
|
||||
],
|
||||
});
|
||||
},
|
||||
spellLists() {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': this.creatureId,
|
||||
type: 'spellList',
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
}, {
|
||||
sort: { order: 1 }
|
||||
});
|
||||
},
|
||||
hasSpells() {
|
||||
return !!CreatureProperties.findOne({
|
||||
'ancestors.id': this.creatureId,
|
||||
type: 'spell',
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
});
|
||||
},
|
||||
spellsWithoutList() {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': {
|
||||
$eq: this.creatureId,
|
||||
$nin: this.spellListIds,
|
||||
},
|
||||
type: 'spell',
|
||||
removed: { $ne: true },
|
||||
deactivatedByAncestor: { $ne: true },
|
||||
deactivatedByToggle: { $ne: true },
|
||||
}, {
|
||||
sort: {
|
||||
level: 1,
|
||||
order: 1,
|
||||
}
|
||||
});
|
||||
},
|
||||
spellListsWithoutAncestorSpellLists() {
|
||||
return CreatureProperties.find({
|
||||
'ancestors.id': {
|
||||
$eq: this.creatureId,
|
||||
$nin: this.spellListIds,
|
||||
},
|
||||
type: 'spellList',
|
||||
removed: { $ne: true },
|
||||
inactive: { $ne: true },
|
||||
}, {
|
||||
sort: { order: 1 }
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
spellListIds() {
|
||||
return this.spellLists.map(spellList => spellList._id);
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,629 @@
|
||||
<template lang="html">
|
||||
<div
|
||||
v-if="properties"
|
||||
class="stats-tab ma-2"
|
||||
>
|
||||
<div
|
||||
v-if="properties.attribute.healthBar && properties.attribute.healthBar.length"
|
||||
class="px-2 pt-2"
|
||||
>
|
||||
<v-card class="pa-2">
|
||||
<health-bar
|
||||
v-for="healthBar in properties.attribute.healthBar"
|
||||
:key="healthBar._id"
|
||||
:model="healthBar"
|
||||
@change="({ type, value }) => incrementChange(healthBar._id, { type, value: -value })"
|
||||
@click="clickProperty({_id: healthBar._id})"
|
||||
/>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<column-layout>
|
||||
<folder-group-card
|
||||
v-for="folder in properties.folder.start"
|
||||
:key="folder._id"
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
<div
|
||||
v-if="!creature.settings.hideRestButtons || (properties.action.event && properties.action.event.length)"
|
||||
class="character-buttons"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-text class="layout column align-center">
|
||||
<rest-button
|
||||
v-if="!creature.settings.hideRestButtons"
|
||||
:creature-id="creatureId"
|
||||
type="shortRest"
|
||||
class="ma-1"
|
||||
/>
|
||||
<rest-button
|
||||
v-if="!creature.settings.hideRestButtons"
|
||||
:creature-id="creatureId"
|
||||
type="longRest"
|
||||
class="ma-1"
|
||||
/>
|
||||
<event-button
|
||||
v-for="event in properties.event"
|
||||
:key="event._id"
|
||||
:model="event"
|
||||
class="ma-1"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<folder-group-card
|
||||
v-for="folder in properties.folder.events"
|
||||
:key="folder._id"
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
|
||||
<damage-multiplier-card
|
||||
v-if="properties.damageMultiplier && properties.damageMultiplier.length"
|
||||
:multipliers="properties.damageMultiplier"
|
||||
@click-multiplier="clickProperty"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="properties.buff && properties.buff.length"
|
||||
class="buffs"
|
||||
>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-subheader>Buffs and conditions</v-subheader>
|
||||
<buff-list-item
|
||||
v-for="buff in properties.buff"
|
||||
:key="buff._id"
|
||||
:data-id="buff._id"
|
||||
:model="buff"
|
||||
@click="clickProperty({_id: buff._id})"
|
||||
@remove="softRemove(buff._id)"
|
||||
/>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="properties.attribute.ability && properties.attribute.ability.length"
|
||||
class="ability-scores"
|
||||
>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<template v-for="(ability, index) in properties.attribute.ability">
|
||||
<v-divider
|
||||
v-if="index !== 0"
|
||||
:key="index"
|
||||
/>
|
||||
<ability-list-tile
|
||||
:key="ability._id"
|
||||
:model="ability"
|
||||
:data-id="ability._id"
|
||||
@click="clickProperty({_id: ability._id})"
|
||||
/>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="toggle in properties.toggle"
|
||||
:key="toggle._id"
|
||||
class="toggle"
|
||||
>
|
||||
<toggle-card
|
||||
:model="toggle"
|
||||
:data-id="toggle._id"
|
||||
@click="clickProperty({_id: toggle._id})"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="stat in properties.attribute.stat"
|
||||
:key="stat._id"
|
||||
class="stat"
|
||||
>
|
||||
<attribute-card
|
||||
:model="stat"
|
||||
:data-id="stat._id"
|
||||
@click="clickProperty({_id: stat._id})"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="modifier in properties.attribute.modifier"
|
||||
:key="modifier._id"
|
||||
class="modifier"
|
||||
>
|
||||
<attribute-card
|
||||
:model="modifier"
|
||||
:data-id="modifier._id"
|
||||
@click="clickProperty({_id: modifier._id})"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="check in properties.skill.check"
|
||||
:key="check._id"
|
||||
class="check"
|
||||
>
|
||||
<attribute-card
|
||||
modifier
|
||||
:model="check"
|
||||
:data-id="check._id"
|
||||
@click="clickProperty({_id: check._id})"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="properties.hitDice && properties.hitDice.length"
|
||||
class="hit-dice"
|
||||
>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-subheader>Hit Dice</v-subheader>
|
||||
<template v-for="(hitDie, index) in hitDice">
|
||||
<v-divider
|
||||
v-if="index !== 0"
|
||||
:key="hitDie._id + 'divider'"
|
||||
/>
|
||||
<hit-dice-list-tile
|
||||
:key="hitDie._id"
|
||||
:model="hitDie"
|
||||
:data-id="hitDie._id"
|
||||
@click="clickProperty({_id: hitDie._id})"
|
||||
@change="e => incrementChange(hitDie._id, e)"
|
||||
/>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="resource in properties.attribute.resource"
|
||||
:key="resource._id"
|
||||
class="resource"
|
||||
>
|
||||
<resource-card
|
||||
:model="resource"
|
||||
:data-id="resource._id"
|
||||
@click="clickProperty({_id: resource._id})"
|
||||
@change="e => incrementChange(resource._id, e)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="properties.attribute.spellSlot && properties.attribute.spellSlot.length"
|
||||
class="spell-slots"
|
||||
>
|
||||
<spell-slot-card
|
||||
:creature-id="creatureId"
|
||||
:spell-slots="properties.attribute.spellSlot"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<folder-group-card
|
||||
v-for="folder in properties.folder.stats"
|
||||
:key="folder._id"
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="properties.skill.save && properties.skill.save.length"
|
||||
class="saving-throws"
|
||||
>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-subheader>Saving Throws</v-subheader>
|
||||
<skill-list-tile
|
||||
v-for="save in properties.skill.save"
|
||||
:key="save._id"
|
||||
:model="save"
|
||||
:data-id="save._id"
|
||||
@click="clickProperty({_id: save._id})"
|
||||
/>
|
||||
<v-list-item
|
||||
v-for="(effect, index) in saveConditionals"
|
||||
:key="effect._id"
|
||||
:data-id="effect._id"
|
||||
:class="{'mt-2': !index}"
|
||||
@click="clickProperty({_id: effect._id})"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle style="white-space: unset;">
|
||||
{{ effect.text }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="properties.skill.skill && properties.skill.skill.length"
|
||||
class="skills"
|
||||
>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-subheader>Skills</v-subheader>
|
||||
<skill-list-tile
|
||||
v-for="skill in properties.skill.skill"
|
||||
:key="skill._id"
|
||||
:model="skill"
|
||||
:data-id="skill._id"
|
||||
@click="clickProperty({_id: skill._id})"
|
||||
/>
|
||||
<v-list-item
|
||||
v-for="(effect, index) in skillConditionals"
|
||||
:key="effect._id"
|
||||
:data-id="effect._id"
|
||||
:class="{'mt-2': !index}"
|
||||
@click="clickProperty({_id: effect._id})"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle style="white-space: unset;">
|
||||
{{ effect.text }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<folder-group-card
|
||||
v-for="folder in properties.folder.skills"
|
||||
:key="folder._id"
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="properties.skill.weapon && properties.skill.weapon.length"
|
||||
class="weapon-proficiencies"
|
||||
>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-subheader>
|
||||
Weapons
|
||||
</v-subheader>
|
||||
<skill-list-tile
|
||||
v-for="weapon in properties.skill.weapon"
|
||||
:key="weapon._id"
|
||||
hide-modifier
|
||||
:model="weapon"
|
||||
:data-id="weapon._id"
|
||||
@click="clickProperty({_id: weapon._id})"
|
||||
/>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</div>
|
||||
<div
|
||||
v-if="properties.skill.armor && properties.skill.armor.length"
|
||||
class="armor-proficiencies"
|
||||
>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-subheader>
|
||||
Armor
|
||||
</v-subheader>
|
||||
<skill-list-tile
|
||||
v-for="armor in properties.skill.armor"
|
||||
:key="armor._id"
|
||||
hide-modifier
|
||||
:model="armor"
|
||||
:data-id="armor._id"
|
||||
@click="clickProperty({_id: armor._id})"
|
||||
/>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</div>
|
||||
<div
|
||||
v-if="properties.skill.tool && properties.skill.tool.length"
|
||||
class="tool-proficiencies"
|
||||
>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-subheader>
|
||||
Tools
|
||||
</v-subheader>
|
||||
<skill-list-tile
|
||||
v-for="tool in properties.skill.tool"
|
||||
:key="tool._id"
|
||||
hide-modifier
|
||||
:model="tool"
|
||||
:data-id="tool._id"
|
||||
@click="clickProperty({_id: tool._id})"
|
||||
/>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</div>
|
||||
<div
|
||||
v-if="properties.skill.language && properties.skill.language.length"
|
||||
class="language-proficiencies"
|
||||
>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-subheader>
|
||||
Languages
|
||||
</v-subheader>
|
||||
<skill-list-tile
|
||||
v-for="language in properties.skill.language"
|
||||
:key="language._id"
|
||||
hide-modifier
|
||||
:model="language"
|
||||
:data-id="language._id"
|
||||
@click="clickProperty({_id: language._id})"
|
||||
/>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<folder-group-card
|
||||
v-for="folder in properties.folder.proficiencies"
|
||||
:key="folder._id"
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
|
||||
<folder-group-card
|
||||
v-for="folder in properties.folder.end"
|
||||
:key="folder._id"
|
||||
:model="folder"
|
||||
@click-property="clickProperty"
|
||||
@sub-click="_id => clickTreeProperty({_id})"
|
||||
@remove="softRemove"
|
||||
/>
|
||||
</column-layout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
|
||||
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
import HealthBar from '/imports/client/ui/properties/components/attributes/HealthBar.vue';
|
||||
import AttributeCard from '/imports/client/ui/properties/components/attributes/AttributeCard.vue';
|
||||
import AbilityListTile from '/imports/client/ui/properties/components/attributes/AbilityListTile.vue';
|
||||
import ColumnLayout from '/imports/client/ui/components/ColumnLayout.vue';
|
||||
import DamageMultiplierCard from '/imports/client/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue';
|
||||
import HitDiceListTile from '/imports/client/ui/properties/components/attributes/HitDiceListTile.vue';
|
||||
import SkillListTile from '/imports/client/ui/properties/components/skills/SkillListTile.vue';
|
||||
import ResourceCard from '/imports/client/ui/properties/components/attributes/ResourceCard.vue';
|
||||
import RestButton from '/imports/client/ui/creature/RestButton.vue';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import ToggleCard from '/imports/client/ui/properties/components/toggles/ToggleCard.vue';
|
||||
import BuffListItem from '/imports/client/ui/properties/components/buffs/BuffListItem.vue';
|
||||
import SpellSlotCard from '/imports/client/ui/properties/components/attributes/SpellSlotCard.vue';
|
||||
import EventButton from '/imports/client/ui/properties/components/actions/EventButton.vue';
|
||||
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
|
||||
import FolderGroupCard from '/imports/client/ui/properties/components/folders/FolderGroupCard.vue';
|
||||
import { get, set, uniqBy } from 'lodash';
|
||||
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'
|
||||
|
||||
function walkDown(forest, callback){
|
||||
let stack = [...forest];
|
||||
while(stack.length){
|
||||
let node = stack.pop();
|
||||
const { skipChildren } = callback(node) ?? { skipChildren: false };
|
||||
if (!skipChildren) {
|
||||
stack.push(...node.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const propertyHandlers = {
|
||||
folder(prop) {
|
||||
let skipChildren;
|
||||
let propPath = null;
|
||||
if (prop.hideStatsGroup) {
|
||||
return { skipChildren: true}
|
||||
}
|
||||
if (prop.tab === 'stats') {
|
||||
propPath = ['folder', prop.location]
|
||||
}
|
||||
return { skipChildren, propPath }
|
||||
},
|
||||
attribute(prop) {
|
||||
if (
|
||||
prop.attributeType === 'utility' ||
|
||||
prop.overridden ||
|
||||
(prop.hideWhenTotalZero && prop.total === 0) ||
|
||||
(prop.hideWhenValueZero && prop.value === 0)
|
||||
) return { propPath: null };
|
||||
return {
|
||||
propPath: ['attribute', prop.attributeType],
|
||||
}
|
||||
},
|
||||
skill(prop) {
|
||||
if (
|
||||
prop.skillType === 'utility'
|
||||
) return { propPath: null };
|
||||
return {
|
||||
propPath: ['skill', prop.skillType],
|
||||
}
|
||||
},
|
||||
toggle(prop) {
|
||||
if (
|
||||
prop.deactivatedByAncestor || !prop.showUI
|
||||
) return { propPath: null };
|
||||
return { propPath: 'toggle' };
|
||||
},
|
||||
action(prop) {
|
||||
if (prop.actionType === 'event' && !prop.overridden) {
|
||||
return { propPath: 'event' };
|
||||
}
|
||||
return { propPath: null };
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HealthBar,
|
||||
RestButton,
|
||||
BuffListItem,
|
||||
AbilityListTile,
|
||||
AttributeCard,
|
||||
ColumnLayout,
|
||||
DamageMultiplierCard,
|
||||
HitDiceListTile,
|
||||
SkillListTile,
|
||||
ResourceCard,
|
||||
SpellSlotCard,
|
||||
ToggleCard,
|
||||
EventButton,
|
||||
FolderGroupCard,
|
||||
},
|
||||
props: {
|
||||
creatureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
doCheckLoading: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
saveConditionals(){
|
||||
const conditionals = [];
|
||||
this.properties.skill?.save?.forEach(prop => {
|
||||
prop?.effects?.forEach(effect => {
|
||||
if (effect.operation === 'conditional') {
|
||||
conditionals.push(effect);
|
||||
}
|
||||
});
|
||||
});
|
||||
return uniqBy(conditionals, '_id');
|
||||
},
|
||||
skillConditionals(){
|
||||
const conditionals = [];
|
||||
this.properties.skill?.skill?.forEach(prop => {
|
||||
prop?.effects?.forEach(effect => {
|
||||
if (effect.operation === 'conditional') {
|
||||
conditionals.push(effect);
|
||||
}
|
||||
});
|
||||
});
|
||||
return uniqBy(conditionals, '_id');
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
properties() {
|
||||
const creature = this.creature;
|
||||
if (!creature) return;
|
||||
const filter = {
|
||||
'ancestors.id': this.creatureId,
|
||||
$or: [
|
||||
{ inactive: { $ne: true } },
|
||||
{ type: 'toggle' },
|
||||
],
|
||||
removed: { $ne: true },
|
||||
type: {
|
||||
$in: [
|
||||
'action',
|
||||
'attribute',
|
||||
'buff',
|
||||
'damageMultiplier',
|
||||
'folder',
|
||||
'skill',
|
||||
'toggle',
|
||||
]
|
||||
}
|
||||
};
|
||||
if (creature.settings.hideUnusedStats) {
|
||||
filter.hide = { $ne: true };
|
||||
}
|
||||
const allProps = CreatureProperties.find(filter, { sort: { order: 1 } });
|
||||
const forest = nodeArrayToTree(allProps);
|
||||
const properties = { folder: {}, attribute: {}, skill: {} };
|
||||
walkDown(forest, node => {
|
||||
const prop = node.node;
|
||||
const { propPath, skipChildren } = propertyHandlers[prop.type]?.(prop) ||
|
||||
{ propPath: prop.type };
|
||||
if (propPath) {
|
||||
let propArray = get(properties, propPath);
|
||||
if (!propArray) {
|
||||
propArray = [];
|
||||
set(properties, propPath, propArray);
|
||||
}
|
||||
if (!propArray?.push) {
|
||||
console.log({propArray});
|
||||
}
|
||||
propArray.push(prop);
|
||||
}
|
||||
return { skipChildren };
|
||||
});
|
||||
properties.damageMultiplier?.sort((a, b) => a.value - b.value);
|
||||
return properties;
|
||||
},
|
||||
creature() {
|
||||
return Creatures.findOne(this.creatureId, { fields: { settings: 1 } });
|
||||
},
|
||||
|
||||
toggles() {
|
||||
return CreatureProperties.find({
|
||||
type: 'toggle',
|
||||
'ancestors.id': this.creatureId,
|
||||
removed: { $ne: true },
|
||||
deactivatedByAncestor: { $ne: true },
|
||||
showUI: true,
|
||||
}, {
|
||||
sort: { order: 1 }
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clickProperty({ _id }) {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `${_id}`,
|
||||
data: { _id },
|
||||
});
|
||||
},
|
||||
clickTreeProperty({ _id }) {
|
||||
this.$store.commit('pushDialogStack', {
|
||||
component: 'creature-property-dialog',
|
||||
elementId: `tree-node-${_id}`,
|
||||
data: { _id },
|
||||
});
|
||||
},
|
||||
incrementChange(_id, { type, value }) {
|
||||
damageProperty.call({
|
||||
_id,
|
||||
operation: type,
|
||||
value: -value
|
||||
}, error => {
|
||||
if (error) {
|
||||
snackbar({ text: error.reason || error.message || error.toString() });
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
softRemove(_id) {
|
||||
softRemoveProperty.call({ _id }, error => {
|
||||
if (error) {
|
||||
snackbar({ text: error.reason || error.message || error.toString() });
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
</style>
|
||||
@@ -54,10 +54,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import TreeDetailLayout from '/imports/ui/components/TreeDetailLayout.vue';
|
||||
import CreaturePropertiesTree from '/imports/ui/creature/creatureProperties/CreaturePropertiesTree.vue';
|
||||
import CreaturePropertyDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue';
|
||||
import TreeSearchInput from '/imports/ui/components/tree/TreeSearchInput.vue';
|
||||
import TreeDetailLayout from '/imports/client/ui/components/TreeDetailLayout.vue';
|
||||
import CreaturePropertiesTree from '/imports/client/ui/creature/creatureProperties/CreaturePropertiesTree.vue';
|
||||
import CreaturePropertyDialog from '/imports/client/ui/creature/creatureProperties/CreaturePropertyDialog.vue';
|
||||
import TreeSearchInput from '/imports/client/ui/components/tree/TreeSearchInput.vue';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<script lang="js">
|
||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||
import DependencyLoopError from '/imports/ui/creature/character/errors/DependencyLoopError.vue';
|
||||
import DependencyLoopError from '/imports/client/ui/creature/character/errors/DependencyLoopError.vue';
|
||||
import updateCreature from '/imports/api/creature/creatures/methods/updateCreature.js';
|
||||
|
||||
export default {
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
<script lang="js">
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import TreeNodeView from '/imports/client/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||
import { reverse } from 'lodash';
|
||||
|
||||
export default {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user