Merge branch 'version-2-dev' into version-2-tabletop

This commit is contained in:
Stefan Zermatten
2022-11-23 16:00:31 +02:00
421 changed files with 8529 additions and 3323 deletions

1
app/.gitignore vendored
View File

@@ -3,6 +3,7 @@
.demeteorized
.cache
.vscode
fileStorage
settings.json
public/components
public/_imports.html

View File

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

View File

@@ -1 +1 @@
METEOR@2.8.0
METEOR@2.8.1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
}],
}
});

View File

@@ -1,6 +1,6 @@
import { some, intersection, difference, remove, includes } from 'lodash';
import applyProperty from '../applyProperty.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import resolve, { Context, toString } from '/imports/parser/resolve.js';
import logErrors from './shared/logErrors.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
@@ -9,10 +9,11 @@ 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){
export default function applyDamage(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
const applyChildren = function(){
const applyChildren = function () {
applyNodeTriggers(node, 'after', actionContext);
node.children.forEach(child => applyProperty(child, actionContext));
};
@@ -28,10 +29,10 @@ export default function applyDamage(node, actionContext){
// Determine if the hit is critical
let criticalHit = scope['$criticalHit']?.value &&
prop.damageType !== 'healing' // Can't critically heal
;
;
// Double the damage rolls if the hit is critical
let context = new Context({
options: {doubleRolls: criticalHit},
options: { doubleRolls: criticalHit },
});
// Gather all the lines we need to log into an array
@@ -40,8 +41,8 @@ export default function applyDamage(node, actionContext){
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.amount, actionContext.log);
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
if (rolled.parseType !== 'constant'){
const { result: rolled } = resolve('roll', prop.amount.parseNode, scope, context);
if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled));
}
logErrors(context.errors, actionContext);
@@ -50,13 +51,13 @@ export default function applyDamage(node, actionContext){
context.errors = [];
// Resolve the roll to a final value
const {result: reduced} = resolve('reduce', rolled, scope, context);
const { result: reduced } = resolve('reduce', rolled, scope, context);
logErrors(context.errors, actionContext);
// Store the result
if (reduced.parseType === 'constant'){
if (reduced.parseType === 'constant') {
prop.amount.value = reduced.value;
} else if (reduced.parseType === 'error'){
} else if (reduced.parseType === 'error') {
prop.amount.value = null;
} else {
prop.amount.value = toString(reduced);
@@ -64,7 +65,7 @@ export default function applyDamage(node, actionContext){
let damage = +reduced.value;
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)) {
return applyChildren();
}
@@ -83,7 +84,7 @@ export default function applyDamage(node, actionContext){
// Memoise the damage suffix for the log
let suffix = (criticalHit ? ' critical ' : ' ') +
prop.damageType +
(prop.damageType !== 'healing' ? ' damage ': '');
(prop.damageType !== 'healing' ? ' damage ' : '');
if (damageTargets && damageTargets.length) {
// Iterate through all the targets
@@ -107,7 +108,7 @@ export default function applyDamage(node, actionContext){
});
// Log the damage done
if (target._id === actionContext.creature._id){
if (target._id === actionContext.creature._id) {
// Target is same as self, log damage as such
logValue.push(`**${damageDealt}** ${suffix} to self`);
} else {
@@ -135,33 +136,33 @@ export default function applyDamage(node, actionContext){
return applyChildren();
}
function applyDamageMultipliers({target, damage, damageProp, logValue}){
function applyDamageMultipliers({ target, damage, damageProp, logValue }) {
const damageType = damageProp?.damageType;
if (!damageType) return damage;
const multiplier = target?.variables?.[damageType];
if (!multiplier) return damage;
const damageTypeText = damageType == 'healing' ? 'healing': `${damageType} damage`;
const damageTypeText = damageType == 'healing' ? 'healing' : `${damageType} damage`;
if (
multiplier.immunity &&
some(multiplier.immunities, multiplierAppliesTo(damageProp, 'immunity'))
){
) {
logValue.push(`Immune to ${damageTypeText}`);
return 0;
} else {
if (
multiplier.resistance &&
some(multiplier.resistances, multiplierAppliesTo(damageProp, 'resistance'))
){
) {
logValue.push(`Resistant to ${damageTypeText}`);
damage = Math.floor(damage / 2);
}
if (
multiplier.vulnerability &&
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp, 'vulnerability'))
){
) {
logValue.push(`Vulnerable to ${damageTypeText}`);
damage = Math.floor(damage * 2);
}
@@ -169,24 +170,25 @@ function applyDamageMultipliers({target, damage, damageProp, logValue}){
return damage;
}
function multiplierAppliesTo(damageProp, multiplierType){
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;
}
}
function dealDamage({target, damageType, amount, actionContext}){
function dealDamage({ target, damageType, amount, actionContext }) {
// Get all the health bars and do damage to them
let healthBars = getPropertiesOfType(target._id, 'attribute');
@@ -238,6 +240,14 @@ function dealDamage({target, damageType, amount, actionContext}){
actionContext
});
damageLeft -= damageAdded;
// Prevent overflow
if (
damageType === 'healing' ?
healthBar.healthBarNoHealingOverflow :
healthBar.healthBarNoDamageOverflow
) {
damageLeft = 0;
}
});
return totalDamage;
}

View File

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

View File

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

View File

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

View File

@@ -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' });
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
import S3 from 'aws-sdk/clients/s3';
export default S3;

View File

@@ -0,0 +1,97 @@
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import SimpleSchema from 'simpl-schema';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import { RefSchema } from '/imports/api/parenting/ChildSchema.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import {
assertDocCopyPermission,
assertDocEditPermission
} from '/imports/api/sharing/sharingPermissions.js';
import {
setLineageOfDocs,
renewDocIds
} from '/imports/api/parenting/parenting.js';
import { reorderDocs } from '/imports/api/parenting/order.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
var snackbar;
if (Meteor.isClient) {
snackbar = require(
'/imports/client/ui/components/snackbars/SnackbarQueue.js'
).snackbar
}
const DUPLICATE_CHILDREN_LIMIT = 500;
const copyLibraryNodeTo = new ValidatedMethod({
name: 'libraryNodes.copyTo',
validate: new SimpleSchema({
_id: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
parent: {
type: RefSchema,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 1,
timeInterval: 10000,
},
run({ _id, parent }) {
if (parent.collection !== 'libraryNodes' && parent.collection !== 'libraries') {
throw new Meteor.Error('Invalid destination',
'Library documents can only be copied to destinations inside other libraries'
);
}
const libraryNode = LibraryNodes.findOne(_id);
const parentDoc = fetchDocByRef(parent);
assertDocCopyPermission(libraryNode, this.userId);
assertDocEditPermission(parentDoc, this.userId);
let decendants = LibraryNodes.find({
'ancestors.id': _id,
removed: { $ne: true },
}, {
limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: { order: 1 },
}).fetch();
if (decendants.length > DUPLICATE_CHILDREN_LIMIT) {
decendants.pop();
if (Meteor.isClient) {
snackbar({
text: `Only the first ${DUPLICATE_CHILDREN_LIMIT} children were duplicated`,
});
}
}
const nodes = [libraryNode, ...decendants];
const newAncestry = parentDoc.ancestors || [];
newAncestry.push(parent);
// re-map all the ancestors
setLineageOfDocs({
docArray: nodes,
newAncestry,
oldParent: libraryNode.parent,
});
// Give the docs new IDs without breaking internal references
renewDocIds({ docArray: nodes });
// Order the root node
libraryNode.order = (parentDoc.order || 0) + 0.5;
LibraryNodes.batchInsert(nodes);
// Tree structure changed by inserts, reorder the tree
reorderDocs({
collection: LibraryNodes,
ancestorId: parent.collection === 'libraries' ? parent.id : parentDoc.ancestors[0].id,
});
},
});
export default copyLibraryNodeTo;

View File

@@ -12,11 +12,11 @@ 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
}
const DUPLICATE_CHILDREN_LIMIT = 50;
const DUPLICATE_CHILDREN_LIMIT = 500;
const duplicateLibraryNode = new ValidatedMethod({
name: 'libraryNodes.duplicate',
@@ -28,7 +28,7 @@ const duplicateLibraryNode = new ValidatedMethod({
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
numRequests: 1,
timeInterval: 5000,
},
run({ _id }) {

View File

@@ -1,2 +1,3 @@
import '/imports/api/library/methods/copyLibraryNodeTo.js';
import '/imports/api/library/methods/duplicateLibraryNode.js';
import '/imports/api/library/methods/updateReferenceNode.js';

View File

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

View File

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

View File

@@ -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
@@ -69,6 +68,16 @@ let AttributeSchema = createPropertySchema({
type: Boolean,
optional: true,
},
// Control how the health bar handles overflow
healthBarNoDamageOverflow: {
type: Boolean,
optional: true,
},
healthBarNoHealingOverflow: {
type: Boolean,
optional: true,
},
// Control when the health bar takes damage or healing
healthBarDamageOrder: {
type: SimpleSchema.Integer,
optional: true,
@@ -107,11 +116,21 @@ let AttributeSchema = createPropertySchema({
type: Boolean,
optional: true,
},
hideWhenTotalZero: {
type: Boolean,
optional: true,
},
hideWhenValueZero: {
type: Boolean,
optional: true,
},
// Automatically zero the adjustment on these conditions
reset: {
type: String,
optional: true,
allowedValues: ['shortRest', 'longRest'],
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
},
});

View File

@@ -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'
],
},
});

View File

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

View File

@@ -33,6 +33,10 @@ let SharingSchema = new SimpleSchema({
defaultValue: false,
index: 1,
},
readersCanCopy: {
type: Boolean,
optional: true,
},
});
export default SharingSchema;

View File

@@ -27,6 +27,26 @@ const setPublic = new ValidatedMethod({
},
});
const setReadersCanCopy = new ValidatedMethod({
name: 'sharing.setReadersCanCopy',
validate: new SimpleSchema({
docRef: RefSchema,
readersCanCopy: { type: Boolean },
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ docRef, readersCanCopy }) {
let doc = fetchDocByRef(docRef);
assertOwnership(doc, this.userId);
return getCollectionByName(docRef.collection).update(docRef.id, {
$set: { readersCanCopy },
});
},
});
const updateUserSharePermissions = new ValidatedMethod({
name: 'sharing.updateUserSharePermissions',
validate: new SimpleSchema({
@@ -129,4 +149,4 @@ const transferOwnership = new ValidatedMethod({
},
});
export { setPublic, updateUserSharePermissions, transferOwnership };
export { setPublic, setReadersCanCopy, updateUserSharePermissions, transferOwnership };

View File

@@ -1,24 +1,25 @@
import { _ } from 'meteor/underscore';
import { includes } from 'lodash';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
function assertIdValid(userId){
if (!userId || typeof userId !== 'string'){
function assertIdValid(userId) {
if (!userId || typeof userId !== 'string') {
throw new Meteor.Error('Permission denied',
'No user ID. Are you logged in?');
}
}
function assertdocExists(doc){
if (!doc){
function assertdocExists(doc) {
if (!doc) {
throw new Meteor.Error('Permission denied',
'Permission denied: No such document exists');
}
}
export function assertOwnership(doc, userId){
export function assertOwnership(doc, userId) {
assertIdValid(userId);
assertdocExists(doc);
if (doc.owner === userId ){
if (doc.owner === userId) {
return true;
} else {
throw new Meteor.Error('Permission denied',
@@ -37,21 +38,20 @@ export function assertEditPermission(doc, userId) {
assertdocExists(doc);
const user = Meteor.users.findOne(userId, {
fields: {
'services.patreon': 1,
'roles': 1,
}
});
// Admin override
if (user.roles && user.roles.includes('admin')){
if (user.roles && user.roles.includes('admin')) {
return true;
}
// Ensure the user is authorized for this specific document
if (
doc.owner === userId ||
_.contains(doc.writers, userId)
){
includes(doc.writers, userId)
) {
return true;
} else {
throw new Meteor.Error('Edit permission denied',
@@ -59,9 +59,46 @@ export function assertEditPermission(doc, userId) {
}
}
function getRoot(doc){
/**
* Assert that the user can edit the root document which manages its own sharing
* permissions.
*
* Warning: the doc and userId must be set by a trusted source
*/
export function assertCopyPermission(doc, userId) {
assertIdValid(userId);
assertdocExists(doc);
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]){
const user = Meteor.users.findOne(userId, {
fields: {
'roles': 1,
}
});
// Admin override
if (user.roles && user.roles.includes('admin')) {
return true;
}
// Ensure the user is authorized for this specific document
if (
doc.owner === userId ||
includes(doc.writers, userId)
) {
return true;
} else if (
(includes(doc.readers, userId) || doc.public) &&
doc.readersCanCopy
) {
return true;
} else {
throw new Meteor.Error('Copy permission denied',
'You do not have permission to copy this document');
}
}
function getRoot(doc) {
assertdocExists(doc);
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]) {
return fetchDocByRef(doc.ancestors[0]);
} else {
return doc;
@@ -74,11 +111,22 @@ function getRoot(doc){
*
* Warning: the doc and userId must be set by a trusted source
*/
export function assertDocEditPermission(doc, userId){
export function assertDocEditPermission(doc, userId) {
let root = getRoot(doc);
assertEditPermission(root, userId);
}
/**
* Assert that the user can copy a descendant document whose root ancestor
* implements sharing permissions.
*
* Warning: the doc and userId must be set by a trusted source
*/
export function assertDocCopyPermission(doc, userId) {
let root = getRoot(doc);
assertCopyPermission(root, userId);
}
export function assertViewPermission(doc, userId) {
assertdocExists(doc);
if (doc.public) return true;
@@ -86,19 +134,19 @@ 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 {
// Admin override
const user = Meteor.users.findOne(userId, {
fields: {
'roles': 1,
}
});
if (user.roles && user.roles.includes('admin')){
if (user.roles && user.roles.includes('admin')) {
return true;
}
@@ -113,20 +161,20 @@ export function assertViewPermission(doc, userId) {
*
* Warning: the doc and userId must be set by a trusted source
*/
export function assertDocViewPermission(doc, userId){
export function assertDocViewPermission(doc, userId) {
let root = getRoot(doc);
assertViewPermission(root, userId);
}
export function assertAdmin(userId){
export function assertAdmin(userId) {
assertIdValid(userId);
let user = Meteor.users.findOne(userId, {fields: {roles: 1}});
if (!user){
let user = Meteor.users.findOne(userId, { fields: { roles: 1 } });
if (!user) {
throw new Meteor.Error('Permission denied',
'UserId does not match any existing user');
}
let isAdmin = user.roles && user.roles.includes('admin')
if (!isAdmin){
if (!isAdmin) {
throw new Meteor.Error('Permission denied',
'User does not have the admin role');
}

View File

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

View File

@@ -10,6 +10,7 @@
:outlined="!!label"
:icon="!label"
:min-width="label && 108"
:disabled="context.editPermission === false"
v-on="on"
>
{{ label }}
@@ -98,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;
@@ -124,6 +125,9 @@
}
export default {
inject: {
context: { default: {} }
},
props: {
//hex string
value: {

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -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() });
}
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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);

View File

@@ -69,6 +69,7 @@
</v-list-item>
<v-list-item
v-if="$listeners && $listeners.duplicate"
:disabled="context.editPermission === false"
@click="$emit('duplicate')"
>
<v-list-item-content>
@@ -80,8 +81,23 @@
<v-icon>mdi-content-copy</v-icon>
</v-list-item-action>
</v-list-item>
<v-list-item
v-if="$listeners && $listeners.copy"
:disabled="context.copyPermission === false"
@click="$emit('copy')"
>
<v-list-item-content>
<v-list-item-title>
Copy To
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-icon>mdi-content-duplicate</v-icon>
</v-list-item-action>
</v-list-item>
<v-list-item
v-if="$listeners && $listeners.move"
:disabled="context.editPermission === false"
@click="$emit('move')"
>
<v-list-item-content>
@@ -95,6 +111,7 @@
</v-list-item>
<v-list-item
v-if="$listeners && $listeners.remove"
:disabled="context.editPermission === false"
@click="$emit('remove')"
>
<v-list-item-content>
@@ -145,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 {
@@ -157,6 +174,9 @@ export default {
PropertyIcon,
ColorPicker,
},
inject: {
context: { default: {} }
},
props: {
model: {
type: Object,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = {};

View File

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

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