Compare commits

...

54 Commits

Author SHA1 Message Date
Stefan Zermatten
c55d572134 Bumped version 2022-11-16 23:52:08 +02:00
Stefan Zermatten
0a2b60990e Merge branch 'version-2-dev' into version-2 2022-11-16 23:51:37 +02:00
Stefan Zermatten
a437ff5aef Fixed log of recovering HD not having names 2022-11-09 15:02:41 +02:00
Stefan Zermatten
3d31d62860 Completed folder stat grouping UI 2022-11-09 14:58:52 +02:00
Stefan Zermatten
8377231254 Began work on stat grouping by folder 2022-11-09 00:00:54 +02:00
Stefan Zermatten
1ec29365cb Added custom sheet events
Made rest buttons optional
2022-11-08 23:01:09 +02:00
Stefan Zermatten
60b21c1901 Fixed bugs with effects
they were not providing advantage or conditional benefits
2022-11-08 18:29:27 +02:00
Stefan Zermatten
03f87b0afa Added spellcasting ability to spell lists 2022-11-08 18:09:00 +02:00
Stefan Zermatten
48291d2c8f Added help to property creation forms 2022-11-08 17:17:26 +02:00
Stefan Zermatten
1cedf55fbf Merge branch 'version-2-print' into version-2-dev 2022-11-08 17:00:52 +02:00
Stefan Zermatten
bed4d4b162 Fixed logo not showing 2022-11-08 16:59:52 +02:00
Stefan Zermatten
a1d992ec8d Fixed blank multipliers box 2022-11-07 16:38:54 +02:00
Stefan Zermatten
008ef62517 Printing implemented, needs print button on sheet 2022-11-07 16:18:35 +02:00
Stefan Zermatten
c436309ba8 Work on column-based print layout 2022-11-07 00:07:42 +02:00
Stefan Zermatten
0bfdb73b47 Added a quick exit for migrations if the database is new 2022-11-04 12:34:37 +02:00
Stefan Zermatten
a462cc5ca2 Updated packages 2022-11-04 12:34:22 +02:00
Stefan Zermatten
5d57a74667 Merge branch 'version-2-dev' into version-2 2022-11-03 20:52:26 +02:00
Stefan Zermatten
21b0029df7 bumped version 2022-11-03 20:51:58 +02:00
Stefan Zermatten
c0ccafa787 Added overflow stops to health bars 2022-11-03 20:50:10 +02:00
Stefan Zermatten
d63ad9ea8f Added hide when total/value zero to attributes 2022-11-03 20:39:02 +02:00
Stefan Zermatten
8f56a60fb1 Added copy-to and related sharing permissions 2022-11-03 20:18:59 +02:00
Stefan Zermatten
358ae46627 Began work on copy to for library nodes 2022-11-03 19:08:44 +02:00
Stefan Zermatten
0b1db3c40c Updated meteor 2022-10-18 15:40:41 +02:00
Stefan Zermatten
0ad7e659d2 updated docs to include create a class guide 2022-10-18 15:40:17 +02:00
Stefan Zermatten
58c3875dc7 Hotifix: Casting cantrips without a spell slot 2022-10-12 07:36:42 +02:00
Stefan Zermatten
84f506f1fe Added $checkDiceRoll $checkRoll $checkModifier variables 2022-10-12 07:32:39 +02:00
Stefan Zermatten
d0a3ccc76a bumped version 2022-10-10 16:54:57 +02:00
Stefan Zermatten
93ac9215c2 Merge branch 'version-2-dev' into version-2 2022-10-10 16:53:10 +02:00
Stefan Zermatten
a6b501a62c Fixed error on missing group prop in tree node 2022-10-10 16:51:02 +02:00
Stefan Zermatten
e956bacf07 Added actionType to effective tags 2022-10-10 16:49:10 +02:00
Stefan Zermatten
60b6b283b1 Folders now get their children applied by actions 2022-10-10 16:45:53 +02:00
Stefan Zermatten
1c9b390551 Added ritual casting 2022-10-09 23:11:06 +02:00
Stefan Zermatten
21a487635d Removed unused code from action cards 2022-10-09 21:56:42 +02:00
Stefan Zermatten
c92a26d5e6 Action cards no longer display folder or the descendants of buffs 2022-10-09 21:56:01 +02:00
Stefan Zermatten
49b514b8f3 Load common dialogs more aggressively 2022-10-09 20:55:50 +02:00
Stefan Zermatten
5cb835c536 Got basic typescript tools working 2022-10-09 17:33:43 +02:00
Stefan Zermatten
aa8f2d230d Hunted the last of the \t's to extinction 2022-10-09 16:56:28 +02:00
Stefan Zermatten
2fa913b09a Applied style rules to genocide all \t characters 2022-10-09 16:01:36 +02:00
Stefan Zermatten
de598c70a7 Fixed rolled effects not applying to checks 2022-10-09 11:10:50 +02:00
Stefan Zermatten
baecdeff24 Fixed bug where items with zero quantity have active children 2022-10-09 10:10:21 +02:00
Stefan Zermatten
d4b7d22b5f Fixed toggled off spells showing in spell list 2022-09-26 09:43:00 +02:00
Stefan Zermatten
87f79737e8 Fixed empty calculated inline fields showing calc 2022-09-25 12:39:49 +02:00
Stefan Zermatten
9f0ffe13f8 Updated meteor to fix observer bugs 2022-09-13 17:34:46 +02:00
Stefan Zermatten
adaa31d76c damage tags to ignore multipliers 2022-09-13 17:34:30 +02:00
Stefan Zermatten
b051d764f8 Slot cards have slot color as outline 2022-09-13 15:47:31 +02:00
Stefan Zermatten
ffb5b4a4f3 Libraries show name in page title 2022-09-13 15:44:37 +02:00
Stefan Zermatten
fd87b7fb75 Added advantage popup to spell cast 2022-09-09 13:20:54 +02:00
Stefan Zermatten
f035902842 Removed unused file 2022-09-08 14:47:12 +02:00
Stefan Zermatten
dbc5f7253f Finished basic docs 2022-09-05 14:36:39 +02:00
Stefan Zermatten
f0e7253374 Updated docs 2022-09-01 13:33:28 +02:00
Stefan Zermatten
ffe37bf907 Added more property help docs 2022-09-01 12:18:29 +02:00
Stefan Zermatten
a63e2099d3 Added documentation UI and began documenting props 2022-08-31 14:43:38 +02:00
Stefan Zermatten
0308e4e7a7 Merge branch 'version-2' into version-2-dev 2022-08-29 11:30:55 +02:00
Stefan Zermatten
43f8df09f0 Fixed client crash when effects target calcs 2022-08-26 09:42:34 +02:00
329 changed files with 13085 additions and 7124 deletions

View File

@@ -11,14 +11,14 @@ accounts-google@1.4.0
email@2.2.1 email@2.2.1
meteor-base@1.5.1 meteor-base@1.5.1
mobile-experience@1.1.0 mobile-experience@1.1.0
mongo@1.15.0 mongo@1.16.0
session@1.2.0 session@1.2.0
tracker@1.2.0 tracker@1.2.0
logging@1.3.1 logging@1.3.1
reload@1.3.1 reload@1.3.1
ejson@1.1.2 ejson@1.1.2
check@1.3.1 check@1.3.1
standard-minifier-js@2.8.0 standard-minifier-js@2.8.1
shell-server@0.5.0 shell-server@0.5.0
ecmascript@0.16.2 ecmascript@0.16.2
es5-shim@4.8.0 es5-shim@4.8.0
@@ -48,3 +48,4 @@ simple:rest-bearer-token-parser
simple:rest-json-error-handler simple:rest-json-error-handler
littledata:synced-cron littledata:synced-cron
mdg:meteor-apm-agent mdg:meteor-apm-agent
typescript@4.5.4

View File

@@ -1 +1 @@
METEOR@2.7.3 METEOR@2.8.0

View File

@@ -1,4 +1,4 @@
accounts-base@2.2.3 accounts-base@2.2.4
accounts-google@1.4.0 accounts-google@1.4.0
accounts-oauth@1.4.1 accounts-oauth@1.4.1
accounts-password@2.3.1 accounts-password@2.3.1
@@ -12,7 +12,7 @@ aldeed:collection2@3.5.0
aldeed:schema-index@3.0.0 aldeed:schema-index@3.0.0
allow-deny@1.1.1 allow-deny@1.1.1
autoupdate@1.8.0 autoupdate@1.8.0
babel-compiler@7.9.0 babel-compiler@7.9.2
babel-runtime@1.5.1 babel-runtime@1.5.1
base64@1.0.12 base64@1.0.12
binary-heap@1.0.11 binary-heap@1.0.11
@@ -27,10 +27,10 @@ coffeescript@2.4.1
coffeescript-compiler@2.4.1 coffeescript-compiler@2.4.1
dburles:mongo-collection-instances@0.3.6 dburles:mongo-collection-instances@0.3.6
ddp@1.4.0 ddp@1.4.0
ddp-client@2.5.0 ddp-client@2.6.0
ddp-common@1.4.0 ddp-common@1.4.0
ddp-rate-limiter@1.1.0 ddp-rate-limiter@1.1.0
ddp-server@2.5.0 ddp-server@2.6.0
diff-sequence@1.1.1 diff-sequence@1.1.1
dynamic-import@0.7.2 dynamic-import@0.7.2
ecmascript@0.16.2 ecmascript@0.16.2
@@ -55,33 +55,33 @@ littledata:synced-cron@1.5.1
livedata@1.0.18 livedata@1.0.18
localstorage@1.2.0 localstorage@1.2.0
logging@1.3.1 logging@1.3.1
mdg:meteor-apm-agent@3.5.0 mdg:meteor-apm-agent@3.5.1
mdg:validated-method@1.2.0 mdg:validated-method@1.2.0
meteor@1.10.0 meteor@1.10.1
meteor-base@1.5.1 meteor-base@1.5.1
meteortesting:browser-tests@1.3.5 meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3 meteortesting:mocha@2.0.3
meteortesting:mocha-core@8.1.2 meteortesting:mocha-core@8.1.2
mikowals:batch-insert@1.3.0 mikowals:batch-insert@1.3.0
minifier-css@1.6.0 minifier-css@1.6.1
minifier-js@2.7.4 minifier-js@2.7.5
minimongo@1.8.0 minimongo@1.9.0
mobile-experience@1.1.0 mobile-experience@1.1.0
mobile-status-bar@1.1.0 mobile-status-bar@1.1.0
modern-browsers@0.1.8 modern-browsers@0.1.8
modules@0.18.0 modules@0.19.0
modules-runtime@0.13.0 modules-runtime@0.13.0
mongo@1.15.0 mongo@1.16.0
mongo-decimal@0.1.3 mongo-decimal@0.1.3
mongo-dev-server@1.1.0 mongo-dev-server@1.1.0
mongo-id@1.0.8 mongo-id@1.0.8
mongo-livedata@1.0.12 mongo-livedata@1.0.12
npm-mongo@4.3.1 npm-mongo@4.9.0
oauth@2.1.2 oauth@2.1.2
oauth2@1.3.1 oauth2@1.3.1
ordered-dict@1.1.0 ordered-dict@1.1.0
ostrio:cookies@2.7.2 ostrio:cookies@2.7.2
ostrio:files@2.0.1 ostrio:files@2.3.0
patreon-oauth@0.1.0 patreon-oauth@0.1.0
peerlibrary:assert@0.3.0 peerlibrary:assert@0.3.0
peerlibrary:check-extension@0.7.0 peerlibrary:check-extension@0.7.0
@@ -116,7 +116,7 @@ simple:rest-json-error-handler@1.1.1
simple:rest-method-mixin@1.1.0 simple:rest-method-mixin@1.1.0
socket-stream-client@0.5.0 socket-stream-client@0.5.0
spacebars-compiler@1.3.1 spacebars-compiler@1.3.1
standard-minifier-js@2.8.0 standard-minifier-js@2.8.1
static-html@1.3.2 static-html@1.3.2
templating-tools@1.2.2 templating-tools@1.2.2
tmeasday:check-npm-versions@1.0.2 tmeasday:check-npm-versions@1.0.2

View File

@@ -32,11 +32,13 @@ const flipToggle = new ValidatedMethod({
// Invert the current value, disabled is the canonical store of value // Invert the current value, disabled is the canonical store of value
const currentValue = !property.disabled; const currentValue = !property.disabled;
CreatureProperties.update(_id, {$set: { CreatureProperties.update(_id, {
$set: {
enabled: !currentValue, enabled: !currentValue,
disabled: currentValue, disabled: currentValue,
dirty: true, dirty: true,
}}, { }
}, {
selector: { type: 'toggle' }, selector: { type: 'toggle' },
}); });
}, },

View File

@@ -18,6 +18,11 @@ let CreatureSettingsSchema = new SimpleSchema({
type: Boolean, type: Boolean,
optional: true, optional: true,
}, },
//hide rest buttons
hideRestButtons: {
type: Boolean,
optional: true,
},
// Swap around the modifier and stat // Swap around the modifier and stat
swapStatAndModifier: { swapStatAndModifier: {
type: Boolean, type: Boolean,

View File

@@ -62,16 +62,37 @@ function doRestWork(restType, actionContext) {
} else { } 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 // Only apply to active properties
let filter = { const filter = {
'ancestors.id': creatureId, 'ancestors.id': creatureId,
reset: resetFilter, reset: resetFilter,
removed: { $ne: true }, removed: { $ne: true },
inactive: { $ne: true }, inactive: { $ne: true },
}; };
// update all attribute's damage // update all attribute's damage
filter.type = 'attribute'; const attributeFilter = {
CreatureProperties.update(filter, { ...filter,
type: 'attribute',
damage: { $ne: 0 },
}
CreatureProperties.find(attributeFilter, {
fields: { name: 1, damage: 1 }
}).forEach(prop => {
actionContext.addLog({
name: prop.name,
value: prop.damage >= 0 ? `Restored ${prop.damage}` : `Removed ${-prop.damage}`
});
});
CreatureProperties.update(attributeFilter, {
$set: { $set: {
damage: 0, damage: 0,
dirty: true, dirty: true,
@@ -81,12 +102,22 @@ function doRestWork(restType, actionContext) {
multi: true, multi: true,
}); });
// Update all action-like properties' usesUsed // Update all action-like properties' usesUsed
filter.type = {$in: [ const actionFilter = {
'action', ...filter,
'attack', type: {
'spell' $in: ['action', 'spell']
]}; },
CreatureProperties.update(filter, { 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: { $set: {
usesUsed: 0, usesUsed: 0,
dirty: true, dirty: true,
@@ -95,8 +126,9 @@ function doRestWork(restType, actionContext) {
selector: { type: 'action' }, selector: { type: 'action' },
multi: true, multi: true,
}); });
// Reset half hit dice on a long rest, starting with the highest dice }
if (restType === 'longRest'){
function resetHitDice(creatureId, actionContext) {
let hitDice = CreatureProperties.find({ let hitDice = CreatureProperties.find({
'ancestors.id': creatureId, 'ancestors.id': creatureId,
type: 'attribute', type: 'attribute',
@@ -105,6 +137,7 @@ function doRestWork(restType, actionContext) {
inactive: { $ne: true }, inactive: { $ne: true },
}, { }, {
fields: { fields: {
name: 1,
hitDiceSize: 1, hitDiceSize: 1,
damage: 1, damage: 1,
total: 1, total: 1,
@@ -129,6 +162,10 @@ function doRestWork(restType, actionContext) {
if (!amountToRecover) return; if (!amountToRecover) return;
recoverableHd -= amountToRecover; recoverableHd -= amountToRecover;
resultingDamage = hd.damage - amountToRecover; resultingDamage = hd.damage - amountToRecover;
actionContext.addLog({
name: hd.name,
value: amountToRecover >= 0 ? `Restored ${amountToRecover} hit dice` : `Removed ${-amountToRecover} hit dice`
});
CreatureProperties.update(hd._id, { CreatureProperties.update(hd._id, {
$set: { $set: {
damage: resultingDamage, damage: resultingDamage,
@@ -139,6 +176,5 @@ function doRestWork(restType, actionContext) {
}); });
}); });
} }
}
export default restCreature; export default restCreature;

View File

@@ -46,7 +46,7 @@ let ExperienceSchema = new SimpleSchema({
Experiences.attachSchema(ExperienceSchema); Experiences.attachSchema(ExperienceSchema);
const insertExperienceForCreature = function({experience, creatureId, userId}){ const insertExperienceForCreature = function ({ experience, creatureId }) {
if (experience.xp) { if (experience.xp) {
Creatures.update(creatureId, { Creatures.update(creatureId, {
$inc: { 'denormalizedStats.xp': experience.xp }, $inc: { 'denormalizedStats.xp': experience.xp },
@@ -172,11 +172,13 @@ const recomputeExperiences = new ValidatedMethod({
xp += experience.xp || 0; xp += experience.xp || 0;
milestoneLevels += experience.levels || 0; milestoneLevels += experience.levels || 0;
}); });
Creatures.update(creatureId, {$set: { Creatures.update(creatureId, {
$set: {
'denormalizedStats.xp': xp, 'denormalizedStats.xp': xp,
'denormalizedStats.milestoneLevels': milestoneLevels, 'denormalizedStats.milestoneLevels': milestoneLevels,
dirty: true, dirty: true,
}}); }
});
}, },
}); });

View File

@@ -99,14 +99,16 @@ const insertCreatureLog = new ValidatedMethod({
}).validator(), }).validator(),
run({ log }) { run({ log }) {
const creatureId = log.creatureId; const creatureId = log.creatureId;
const creature = Creatures.findOne(creatureId, {fields: { const creature = Creatures.findOne(creatureId, {
fields: {
readers: 1, readers: 1,
writers: 1, writers: 1,
owner: 1, owner: 1,
'settings.discordWebhook': 1, 'settings.discordWebhook': 1,
name: 1, name: 1,
avatarPicture: 1, avatarPicture: 1,
}}); }
});
assertEditPermission(creature, this.userId); assertEditPermission(creature, this.userId);
// Build the new log // Build the new log
let id = insertCreatureLogWork({ log, creature, method: this }) let id = insertCreatureLogWork({ log, creature, method: this })
@@ -154,14 +156,16 @@ const logRoll = new ValidatedMethod({
}, },
}).validator(), }).validator(),
run({ roll, creatureId }) { run({ roll, creatureId }) {
const creature = Creatures.findOne(creatureId, {fields: { const creature = Creatures.findOne(creatureId, {
fields: {
readers: 1, readers: 1,
writers: 1, writers: 1,
owner: 1, owner: 1,
'settings.discordWebhook': 1, 'settings.discordWebhook': 1,
name: 1, name: 1,
avatarPicture: 1, avatarPicture: 1,
}}); }
});
assertEditPermission(creature, this.userId); assertEditPermission(creature, this.userId);
const variables = CreatureVariables.findOne({ _creatureId: creatureId }); const variables = CreatureVariables.findOne({ _creatureId: creatureId });
let logContent = [] let logContent = []

View File

@@ -0,0 +1,3 @@
if (Meteor.isServer) throw 'Client side only collection, don\'t import on server';
const Docs = new Mongo.Collection('docs');
export default Docs;

View File

@@ -4,6 +4,7 @@ import branch from './applyPropertyByType/applyBranch.js';
import buff from './applyPropertyByType/applyBuff.js'; import buff from './applyPropertyByType/applyBuff.js';
import buffRemover from './applyPropertyByType/applyBuffRemover.js'; import buffRemover from './applyPropertyByType/applyBuffRemover.js';
import damage from './applyPropertyByType/applyDamage.js'; import damage from './applyPropertyByType/applyDamage.js';
import folder from './applyPropertyByType/applyFolder.js';
import note from './applyPropertyByType/applyNote.js'; import note from './applyPropertyByType/applyNote.js';
import roll from './applyPropertyByType/applyRoll.js'; import roll from './applyPropertyByType/applyRoll.js';
import savingThrow from './applyPropertyByType/applySavingThrow.js'; import savingThrow from './applyPropertyByType/applySavingThrow.js';
@@ -16,6 +17,7 @@ const applyPropertyByType = {
buff, buff,
buffRemover, buffRemover,
damage, damage,
folder,
note, note,
roll, roll,
savingThrow, savingThrow,

View File

@@ -7,6 +7,7 @@ import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/met
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js'; import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.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) { export default function applyAction(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext); applyNodeTriggers(node, 'before', actionContext);
@@ -44,6 +45,9 @@ export default function applyAction(node, actionContext) {
} else { } else {
applyChildren(node, actionContext); applyChildren(node, actionContext);
} }
if (prop.actionType === 'event' && prop.variableName) {
resetProperties(actionContext.creature._id, prop.variableName, actionContext);
}
} }
function applyAttackWithoutTarget({ attack, actionContext }) { function applyAttackWithoutTarget({ attack, actionContext }) {

View File

@@ -1,4 +1,4 @@
import { some, intersection, difference, remove } from 'lodash'; import { some, intersection, difference, remove, includes } from 'lodash';
import applyProperty from '../applyProperty.js'; 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 resolve, { Context, toString } from '/imports/parser/resolve.js';
@@ -147,21 +147,21 @@ function applyDamageMultipliers({target, damage, damageProp, logValue}){
if ( if (
multiplier.immunity && multiplier.immunity &&
some(multiplier.immunities, multiplierAppliesTo(damageProp)) some(multiplier.immunities, multiplierAppliesTo(damageProp, 'immunity'))
) { ) {
logValue.push(`Immune to ${damageTypeText}`); logValue.push(`Immune to ${damageTypeText}`);
return 0; return 0;
} else { } else {
if ( if (
multiplier.resistance && multiplier.resistance &&
some(multiplier.resistances, multiplierAppliesTo(damageProp)) some(multiplier.resistances, multiplierAppliesTo(damageProp, 'resistance'))
) { ) {
logValue.push(`Resistant to ${damageTypeText}`); logValue.push(`Resistant to ${damageTypeText}`);
damage = Math.floor(damage / 2); damage = Math.floor(damage / 2);
} }
if ( if (
multiplier.vulnerability && multiplier.vulnerability &&
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp)) some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp, 'vulnerability'))
) { ) {
logValue.push(`Vulnerable to ${damageTypeText}`); logValue.push(`Vulnerable to ${damageTypeText}`);
damage = Math.floor(damage * 2); damage = Math.floor(damage * 2);
@@ -170,8 +170,11 @@ function applyDamageMultipliers({target, damage, damageProp, logValue}){
return damage; return damage;
} }
function multiplierAppliesTo(damageProp){ function multiplierAppliesTo(damageProp, multiplierType) {
return multiplier => { return multiplier => {
// Apply the default 'ignore x' tags
if (includes(damageProp.tags, `ignore ${multiplierType}`)) return false;
const hasRequiredTags = difference( const hasRequiredTags = difference(
multiplier.includeTags, damageProp.tags multiplier.includeTags, damageProp.tags
).length === 0; ).length === 0;
@@ -236,6 +239,14 @@ function dealDamage({target, damageType, amount, actionContext}){
actionContext actionContext
}); });
damageLeft -= damageAdded; damageLeft -= damageAdded;
// Prevent overflow
if (
damageType === 'healing' ?
healthBar.healthBarNoHealingOverflow :
healthBar.healthBarNoDamageOverflow
) {
damageLeft = 0;
}
}); });
return totalDamage; return totalDamage;
} }

View File

@@ -0,0 +1,11 @@
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
import applyProperty from '../applyProperty.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
export default function applyFolder(node, actionContext) {
// Apply triggers
applyNodeTriggers(node, 'before', actionContext);
applyNodeTriggers(node, 'after', actionContext);
// Apply children
node.children.forEach(child => applyProperty(child, actionContext));
}

View File

@@ -5,7 +5,7 @@ import logErrors from './logErrors.js';
export default function recalculateCalculation(calc, actionContext, context){ export default function recalculateCalculation(calc, actionContext, context){
if (!calc?.parseNode) return; if (!calc?.parseNode) return;
calc._parseLevel = 'reduce'; calc._parseLevel = 'reduce';
applyEffectsToCalculationParseNode(calc, actionContext.log); applyEffectsToCalculationParseNode(calc, actionContext);
evaluateCalculation(calc, actionContext.scope, context); evaluateCalculation(calc, actionContext.scope, context);
logErrors(calc.errors, actionContext.log); logErrors(calc.errors, actionContext);
} }

View File

@@ -20,6 +20,10 @@ const doAction = new ValidatedMethod({
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,
optional: true, optional: true,
}, },
ritual: {
type: Boolean,
optional: true,
},
targetIds: { targetIds: {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
@@ -41,7 +45,7 @@ const doAction = new ValidatedMethod({
numRequests: 10, numRequests: 10,
timeInterval: 5000, timeInterval: 5000,
}, },
run({ spellId, slotId, targetIds = [], scope = {} }) { run({ spellId, slotId, ritual, targetIds = [], scope = {} }) {
// Get action context // Get action context
let spell = CreatureProperties.findOne(spellId); let spell = CreatureProperties.findOne(spellId);
const creatureId = spell.ancestors[0].id; const creatureId = spell.ancestors[0].id;
@@ -64,7 +68,8 @@ const doAction = new ValidatedMethod({
let slotLevel = spell.level || 0; let slotLevel = spell.level || 0;
let slot; let slot;
if (slotId && !spell.castWithoutSpellSlots){ // If a spell requires a slot, make sure a slot is spent
if (spell.level && !spell.castWithoutSpellSlots && !(ritual && spell.ritual)) {
slot = CreatureProperties.findOne(slotId); slot = CreatureProperties.findOne(slotId);
if (!slot) { if (!slot) {
throw new Meteor.Error('No slot', throw new Meteor.Error('No slot',
@@ -101,10 +106,16 @@ const doAction = new ValidatedMethod({
name: `Casting using a level ${slotLevel} spell slot` name: `Casting using a level ${slotLevel} spell slot`
}); });
} else if (slotLevel) { } else if (slotLevel) {
if (ritual) {
actionContext.addLog({
name: `Ritual casting at level ${slotLevel}`
});
} else {
actionContext.addLog({ actionContext.addLog({
name: `Casting at level ${slotLevel}` name: `Casting at level ${slotLevel}`
}); });
} }
}
actionContext.scope['slotLevel'] = slotLevel; actionContext.scope['slotLevel'] = slotLevel;

View File

@@ -7,6 +7,7 @@ import rollDice from '/imports/parser/rollDice.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js'; import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js'; import { applyTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js'; import ActionContext from '/imports/api/engine/actions/ActionContext.js';
import evaluateCalculation from '/imports/api/engine/computation/utility/evaluateCalculation.js';
const doCheck = new ValidatedMethod({ const doCheck = new ValidatedMethod({
name: 'creatureProperties.doCheck', name: 'creatureProperties.doCheck',
@@ -72,7 +73,11 @@ function rollCheck(prop, actionContext) {
throw (`${prop.type} not supported for checks`); throw (`${prop.type} not supported for checks`);
} }
const rollModifierText = numberToSignedString(rollModifier, true); let rollModifierText = numberToSignedString(rollModifier, true);
const { effectBonus, effectString } = applyUnresolvedEffects(prop, scope)
rollModifierText += effectString;
rollModifier += effectBonus;
let value, values, resultPrefix; let value, values, resultPrefix;
if (scope['$checkAdvantage'] === 1) { if (scope['$checkAdvantage'] === 1) {
@@ -101,8 +106,29 @@ function rollCheck(prop, actionContext) {
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = ` resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
} }
const result = (value + rollModifier) || 0; const result = (value + rollModifier) || 0;
scope['$checkDiceRoll'] = value;
scope['$checkRoll'] = result;
scope['$checkModifier'] = rollModifier;
actionContext.addLog({ actionContext.addLog({
name: logName, name: logName,
value: `${resultPrefix} **${result}**`, value: `${resultPrefix} **${result}**`,
}); });
} }
function applyUnresolvedEffects(prop, scope) {
let effectBonus = 0;
let effectString = '';
if (!prop.effects) {
return { effectBonus, effectString };
}
prop.effects.forEach(effect => {
if (!effect.amount?.parseNode) return;
if (effect.operation !== 'add') return;
effect.amount._parseLevel = 'reduce';
evaluateCalculation(effect.amount, scope);
if (typeof effect.amount?.value !== 'number') return;
effectBonus += effect.amount.value;
effectString += ` ${effect.amount.value < 0 ? '-' : '+'} [${effect.amount.calculation}] ${Math.abs(effect.amount.value)}`
});
return { effectBonus, effectString };
}

View File

@@ -1,9 +1,24 @@
import { EJSON } from 'meteor/ejson'; import { EJSON } from 'meteor/ejson';
import createGraph from 'ngraph.graph'; import createGraph, { Graph } from 'ngraph.graph';
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js'; import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
interface CreatureProperty {
_id: string;
type: string;
}
export default class CreatureComputation { export default class CreatureComputation {
constructor(properties, creature, variables){ originalPropsById: object;
propsById: object;
propsWithTag: object;
scope: object;
props: Array<CreatureProperty>;
dependencyGraph: Graph;
errors: Array<object>;
creature: object;
variables: object;
constructor(properties: Array<CreatureProperty>, creature: object, variables: object) {
// Set up fields // Set up fields
this.originalPropsById = {}; this.originalPropsById = {};
this.propsById = {}; this.propsById = {};

View File

@@ -29,8 +29,8 @@ function childrenActive(prop){
// Children of disabled properties are always inactive // Children of disabled properties are always inactive
if (prop.disabled) return false; if (prop.disabled) return false;
switch (prop.type){ switch (prop.type){
// Only equipped items have active children // Only equipped items with non-zero quantity have active children
case 'item': return !!prop.equipped; case 'item': return !!prop.equipped && prop.quantity !== 0;
// The children of actions, spells, and triggers are always inactive // The children of actions, spells, and triggers are always inactive
case 'action': return false; case 'action': return false;
case 'spell': return false; case 'spell': return false;

View File

@@ -10,8 +10,6 @@ export default function computeToggleDependencies(node, dependencyGraph){
prop.enabled prop.enabled
) return; ) return;
walkDown(node.children, child => { walkDown(node.children, child => {
// Only for children that aren't inactive
if (child.node.inactive) return;
// The child nodes depend on the toggle condition compuation // The child nodes depend on the toggle condition compuation
child.node._computationDetails.toggleAncestors.push(prop); child.node._computationDetails.toggleAncestors.push(prop);
dependencyGraph.addLink(child.node._id, prop._id, 'toggle'); dependencyGraph.addLink(child.node._id, prop._id, 'toggle');

View File

@@ -12,7 +12,7 @@ import computeToggleDependencies from './buildComputation/computeToggleDependenc
import linkCalculationDependencies from './buildComputation/linkCalculationDependencies.js'; import linkCalculationDependencies from './buildComputation/linkCalculationDependencies.js';
import linkTypeDependencies from './buildComputation/linkTypeDependencies.js'; import linkTypeDependencies from './buildComputation/linkTypeDependencies.js';
import computeSlotQuantityFilled from './buildComputation/computeSlotQuantityFilled.js'; import computeSlotQuantityFilled from './buildComputation/computeSlotQuantityFilled.js';
import CreatureComputation from './CreatureComputation.js'; import CreatureComputation from './CreatureComputation.ts';
import removeSchemaFields from './buildComputation/removeSchemaFields.js'; import removeSchemaFields from './buildComputation/removeSchemaFields.js';
/** /**

View File

@@ -5,6 +5,7 @@ import skill from './computeByType/computeSkill.js';
import pointBuy from './computeByType/computePointBuy.js'; import pointBuy from './computeByType/computePointBuy.js';
import propertySlot from './computeByType/computeSlot.js'; import propertySlot from './computeByType/computeSlot.js';
import container from './computeByType/computeContainer.js'; import container from './computeByType/computeContainer.js';
import spellList from './computeByType/computeSpellList.js';
import _calculation from './computeByType/computeCalculation.js'; import _calculation from './computeByType/computeCalculation.js';
export default Object.freeze({ export default Object.freeze({
@@ -17,4 +18,5 @@ export default Object.freeze({
pointBuy, pointBuy,
propertySlot, propertySlot,
spell: action, spell: action,
spellList,
}); });

View File

@@ -0,0 +1,6 @@
export default function computeSpelllist(computation, node) {
const prop = node.data;
const ability = computation.scope[prop.ability];
prop.abilityMod = ability?.modifier || 0;
}

View File

@@ -1,3 +1,5 @@
import { pick } from 'lodash';
export default function aggregateEffect({ node, linkedNode, link }) { export default function aggregateEffect({ node, linkedNode, link }) {
if (link.data !== 'effect') return; if (link.data !== 'effect') return;
// store the effect aggregator, its presence indicates that the variable is // store the effect aggregator, its presence indicates that the variable is
@@ -19,12 +21,24 @@ export default function aggregateEffect({node, linkedNode, link}){
// Store a summary of the effect itself // Store a summary of the effect itself
node.data.effects = node.data.effects || []; node.data.effects = node.data.effects || [];
// Store either just
let effectAmount;
if (!linkedNode.data.amount) {
effectAmount = undefined;
} else if (typeof linkedNode.data.amount.value === 'string') {
effectAmount = pick(linkedNode.data.amount, [
'calculation', 'parseNode', 'parseError', 'value'
]);
} else {
effectAmount = pick(linkedNode.data.amount, ['value']);
}
node.data.effects.push({ node.data.effects.push({
_id: linkedNode.data._id, _id: linkedNode.data._id,
name: linkedNode.data.name, name: linkedNode.data.name,
operation: linkedNode.data.operation, operation: linkedNode.data.operation,
amount: linkedNode.data.amount && {value: linkedNode.data.amount.value}, amount: effectAmount,
type: linkedNode.data.type, type: linkedNode.data.type,
text: linkedNode.data.text,
// ancestors: linkedNode.data.ancestors, // ancestors: linkedNode.data.ancestors,
}); });
@@ -32,8 +46,7 @@ export default function aggregateEffect({node, linkedNode, link}){
const aggregator = node.data.effectAggregator; const aggregator = node.data.effectAggregator;
// Get the result of the effect // Get the result of the effect
const result = linkedNode.data.amount?.value; const result = linkedNode.data.amount?.value;
// Skip aggregating if the result is not resolved completely
if (typeof result === 'string') return;
// Aggregate the effect based on its operation // Aggregate the effect based on its operation
switch (linkedNode.data.operation) { switch (linkedNode.data.operation) {
case 'base': case 'base':

View File

@@ -4,7 +4,7 @@ export default function evaluateToggles(computation, node){
let toggles = prop._computationDetails?.toggleAncestors; let toggles = prop._computationDetails?.toggleAncestors;
if (!toggles) return; if (!toggles) return;
toggles.forEach(toggle => { toggles.forEach(toggle => {
if (prop.inactive || !toggle.condition) return; if (!toggle.condition) return;
if (!toggle.condition.value){ if (!toggle.condition.value){
prop.inactive = true; prop.inactive = true;
prop.deactivatedByToggle = true; prop.deactivatedByToggle = true;

View File

@@ -12,6 +12,7 @@ export default function getEffectivePropTags(prop) {
if (prop.variableName) tags.push(prop.variableName); if (prop.variableName) tags.push(prop.variableName);
if (prop.damageType) tags.push(prop.damageType); if (prop.damageType) tags.push(prop.damageType);
if (prop.skillType) tags.push(prop.skillType); if (prop.skillType) tags.push(prop.skillType);
if (prop.actionType) tags.push(prop.actionType);
if (prop.attributeType) tags.push(prop.attributeType); if (prop.attributeType) tags.push(prop.attributeType);
if (prop.reset) tags.push(prop.reset); if (prop.reset) tags.push(prop.reset);
return tags; return tags;

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

@@ -16,7 +16,7 @@ if (Meteor.isClient){
).snackbar ).snackbar
} }
const DUPLICATE_CHILDREN_LIMIT = 50; const DUPLICATE_CHILDREN_LIMIT = 500;
const duplicateLibraryNode = new ValidatedMethod({ const duplicateLibraryNode = new ValidatedMethod({
name: 'libraryNodes.duplicate', name: 'libraryNodes.duplicate',
@@ -28,7 +28,7 @@ const duplicateLibraryNode = new ValidatedMethod({
}).validator(), }).validator(),
mixins: [RateLimiterMixin], mixins: [RateLimiterMixin],
rateLimit: { rateLimit: {
numRequests: 5, numRequests: 1,
timeInterval: 5000, timeInterval: 5000,
}, },
run({ _id }) { 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/duplicateLibraryNode.js';
import '/imports/api/library/methods/updateReferenceNode.js'; import '/imports/api/library/methods/updateReferenceNode.js';

View File

@@ -1,17 +1,17 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
let SoftRemovableSchema = new SimpleSchema({ let SoftRemovableSchema = new SimpleSchema({
"removed": { 'removed': {
type: Boolean, type: Boolean,
optional: true, optional: true,
index: 1, index: 1,
}, },
"removedAt": { 'removedAt': {
type: Date, type: Date,
optional: true, optional: true,
index: 1, index: 1,
}, },
"removedWith": { 'removedWith': {
optional: true, optional: true,
type: String, type: String,
regEx: SimpleSchema.RegEx.Id, regEx: SimpleSchema.RegEx.Id,

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js'; import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.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 * Actions are things a character can do
@@ -24,9 +25,17 @@ let ActionSchema = createPropertySchema({
// long actions take longer than 1 round to cast // long actions take longer than 1 round to cast
actionType: { actionType: {
type: String, type: String,
allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long'], allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long', 'event'],
defaultValue: 'action', 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 // Who is the action directed at
target: { target: {
type: String, type: String,
@@ -56,8 +65,10 @@ let ActionSchema = createPropertySchema({
// How this action's uses are reset automatically // How this action's uses are reset automatically
reset: { reset: {
type: String, type: String,
allowedValues: ['longRest', 'shortRest'],
optional: true, optional: true,
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
}, },
// Resources // Resources
resources: { resources: {

View File

@@ -28,8 +28,7 @@ let AttributeSchema = createPropertySchema({
'stat', // Speed, Armor Class 'stat', // Speed, Armor Class
'modifier', // Proficiency Bonus, displayed as +x 'modifier', // Proficiency Bonus, displayed as +x
'hitDice', // d12 hit dice 'hitDice', // d12 hit dice
'healthBar', // Hitpoints, Temporary Hitpoints, can take damage 'healthBar', // Hitpoints, Temporary Hitpoints
'bar', // Displayed as a health bar, can't take damage
'resource', // Rages, sorcery points 'resource', // Rages, sorcery points
'spellSlot', // Level 1, 2, 3... spell slots 'spellSlot', // Level 1, 2, 3... spell slots
'utility', // Aren't displayed, Jump height, Carry capacity 'utility', // Aren't displayed, Jump height, Carry capacity
@@ -69,6 +68,16 @@ let AttributeSchema = createPropertySchema({
type: Boolean, type: Boolean,
optional: true, 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: { healthBarDamageOrder: {
type: SimpleSchema.Integer, type: SimpleSchema.Integer,
optional: true, optional: true,
@@ -107,11 +116,21 @@ let AttributeSchema = createPropertySchema({
type: Boolean, type: Boolean,
optional: true, optional: true,
}, },
hideWhenTotalZero: {
type: Boolean,
optional: true,
},
hideWhenValueZero: {
type: Boolean,
optional: true,
},
// Automatically zero the adjustment on these conditions // Automatically zero the adjustment on these conditions
reset: { reset: {
type: String, type: String,
optional: true, optional: true,
allowedValues: ['shortRest', 'longRest'], regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
}, },
}); });

View File

@@ -7,6 +7,11 @@ let FolderSchema = new createPropertySchema({
name: { name: {
type: String, type: String,
max: STORAGE_LIMITS.name, max: STORAGE_LIMITS.name,
optional: true,
},
groupStats: {
type: Boolean,
optional: true,
}, },
}); });

View File

@@ -17,6 +17,12 @@ let SpellListSchema = createPropertySchema({
type: 'fieldToCompute', type: 'fieldToCompute',
optional: true, 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 // Calculation of The attack roll bonus used by spell attacks in this list
attackRollBonus: { attackRollBonus: {
type: 'fieldToCompute', type: 'fieldToCompute',
@@ -38,6 +44,12 @@ const ComputedOnlySpellListSchema = createPropertySchema({
type: 'computedOnlyField', type: 'computedOnlyField',
optional: true, optional: true,
}, },
// Computed value determined by the ability
abilityMod: {
type: SimpleSchema.Integer,
optional: true,
removeBeforeCompute: true,
},
attackRollBonus: { attackRollBonus: {
type: 'computedOnlyField', type: 'computedOnlyField',
optional: true, optional: true,

View File

@@ -33,6 +33,10 @@ let SharingSchema = new SimpleSchema({
defaultValue: false, defaultValue: false,
index: 1, index: 1,
}, },
readersCanCopy: {
type: Boolean,
optional: true,
},
}); });
export default SharingSchema; 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({ const updateUserSharePermissions = new ValidatedMethod({
name: 'sharing.updateUserSharePermissions', name: 'sharing.updateUserSharePermissions',
validate: new SimpleSchema({ validate: new SimpleSchema({
@@ -129,4 +149,4 @@ const transferOwnership = new ValidatedMethod({
}, },
}); });
export { setPublic, updateUserSharePermissions, transferOwnership }; export { setPublic, setReadersCanCopy, updateUserSharePermissions, transferOwnership };

View File

@@ -18,6 +18,7 @@ function assertdocExists(doc){
export function assertOwnership(doc, userId) { export function assertOwnership(doc, userId) {
assertIdValid(userId); assertIdValid(userId);
assertdocExists(doc); assertdocExists(doc);
if (doc.owner === userId) { if (doc.owner === userId) {
return true; return true;
} else { } else {
@@ -37,7 +38,6 @@ export function assertEditPermission(doc, userId) {
assertdocExists(doc); assertdocExists(doc);
const user = Meteor.users.findOne(userId, { const user = Meteor.users.findOne(userId, {
fields: { fields: {
'services.patreon': 1,
'roles': 1, 'roles': 1,
} }
}); });
@@ -59,6 +59,43 @@ export function assertEditPermission(doc, userId) {
} }
} }
/**
* 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);
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 ||
_.contains(doc.writers, userId)
) {
return true;
} else if (
(_.contains(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) { function getRoot(doc) {
assertdocExists(doc); assertdocExists(doc);
if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]) { if (doc.ancestors && doc.ancestors.length && doc.ancestors[0]) {
@@ -79,6 +116,17 @@ export function assertDocEditPermission(doc, userId){
assertEditPermission(root, userId); 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) { export function assertViewPermission(doc, userId) {
assertdocExists(doc); assertdocExists(doc);
if (doc.public) return true; if (doc.public) return true;

View File

@@ -4,6 +4,14 @@ import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
if (Meteor.isServer) { if (Meteor.isServer) {
Meteor.startup(() => { Meteor.startup(() => {
const dbVersion = Migrations.getVersion(); const dbVersion = Migrations.getVersion();
// If there are no users, this is a new DB, set the version to latest
const aUser = Meteor.users.findOne({});
const latestVersion = Migrations._list[Migrations._list.length - 1].version
if (!aUser && dbVersion !== latestVersion) {
Migrations._collection.update({ _id: 'control' }, { version: latestVersion });
return;
}
// Otherwise put the app in maintenance mode if it's not the right version
if ( if (
!Meteor.settings.public.maintenanceMode && !Meteor.settings.public.maintenanceMode &&
dbVersion !== undefined && dbVersion !== undefined &&

View File

@@ -2,12 +2,14 @@ const PROPERTIES = Object.freeze({
action: { action: {
icon: '$vuetify.icons.action', icon: '$vuetify.icons.action',
name: 'Action', name: 'Action',
docsPath: 'property/action',
helpText: 'Actions are things your character can do. When an action is taken, all the properties under it are activated.', helpText: 'Actions are things your character can do. When an action is taken, all the properties under it are activated.',
suggestedParents: ['classLevel', 'feature', 'item'], suggestedParents: ['classLevel', 'feature', 'item'],
}, },
attribute: { attribute: {
icon: '$vuetify.icons.attribute', icon: '$vuetify.icons.attribute',
name: 'Attribute', name: 'Attribute',
docsPath: 'property/attribute',
helpText: 'Attributes are the numbered statistics of your character, excluding rolls you might add proficiency bonus to, those are skills.', helpText: 'Attributes are the numbered statistics of your character, excluding rolls you might add proficiency bonus to, those are skills.',
examples: 'Ability scores, speed, hit points, ki', examples: 'Ability scores, speed, hit points, ki',
suggestedParents: ['classLevel', 'buff'], suggestedParents: ['classLevel', 'buff'],
@@ -15,48 +17,56 @@ const PROPERTIES = Object.freeze({
adjustment: { adjustment: {
icon: '$vuetify.icons.attribute_damage', icon: '$vuetify.icons.attribute_damage',
name: 'Attribute damage', name: 'Attribute damage',
docsPath: 'property/attribute-damage',
helpText: 'Attribute damage reduces the current value of an attribute when it is applied by an action. A negative value causes the attribute to increase instead, up to its normal maximum.', helpText: 'Attribute damage reduces the current value of an attribute when it is applied by an action. A negative value causes the attribute to increase instead, up to its normal maximum.',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'], suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
}, },
buff: { buff: {
icon: '$vuetify.icons.buff', icon: '$vuetify.icons.buff',
name: 'Buff', name: 'Buff',
docsPath: 'property/buff',
helpText: 'When a buff is activated as a child of an action, it will copy the properties under itself onto a target character.', helpText: 'When a buff is activated as a child of an action, it will copy the properties under itself onto a target character.',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'], suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
}, },
buffRemover: { buffRemover: {
icon: '$vuetify.icons.buffRemover', icon: '$vuetify.icons.buffRemover',
name: 'Remove Buff', name: 'Remove Buff',
docsPath: 'property/remove-buff',
helpText: 'Removes a buff from the target character', helpText: 'Removes a buff from the target character',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'], suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
}, },
branch: { branch: {
icon: 'mdi-file-tree', icon: 'mdi-file-tree',
name: 'Branch', name: 'Branch',
docsPath: 'property/branch',
helpText: 'When a branch is activated as a child of an action, it can control which of its children get activated.', helpText: 'When a branch is activated as a child of an action, it can control which of its children get activated.',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell'], suggestedParents: ['action', 'attack', 'savingThrow', 'spell'],
}, },
class: { class: {
icon: 'mdi-card-account-details', icon: 'mdi-card-account-details',
name: 'Class', name: 'Class',
docsPath: 'property/class',
helpText: 'Your character should ideally have one starting class. Classes hold class levels', helpText: 'Your character should ideally have one starting class. Classes hold class levels',
suggestedParents: [], suggestedParents: [],
}, },
classLevel: { classLevel: {
icon: '$vuetify.icons.class_level', icon: '$vuetify.icons.class_level',
name: 'Class level', name: 'Class level',
docsPath: 'property/class-level',
helpText: 'Class levels represent a single level gained in a class', helpText: 'Class levels represent a single level gained in a class',
suggestedParents: ['class'], suggestedParents: ['class'],
}, },
constant: { constant: {
icon: 'mdi-anchor', icon: 'mdi-anchor',
name: 'Constant', name: 'Constant',
docsPath: 'property/constant',
helpText: 'A constant can define a static value that can be used in calculations elsewhere in the sheet', helpText: 'A constant can define a static value that can be used in calculations elsewhere in the sheet',
suggestedParents: [], suggestedParents: [],
}, },
container: { container: {
icon: 'mdi-bag-personal-outline', icon: 'mdi-bag-personal-outline',
name: 'Container', name: 'Container',
docsPath: 'property/container',
helpText: 'A container holds items in the inventory', helpText: 'A container holds items in the inventory',
examples: 'Coin pouch, backpack', examples: 'Coin pouch, backpack',
suggestedParents: ['folder'], suggestedParents: ['folder'],
@@ -64,18 +74,21 @@ const PROPERTIES = Object.freeze({
damage: { damage: {
icon: '$vuetify.icons.damage', icon: '$vuetify.icons.damage',
name: 'Damage', name: 'Damage',
docsPath: 'property/damage',
helpText: 'When damage is activated by an action it reduces the hit points of the target creature by the calculated amount.', helpText: 'When damage is activated by an action it reduces the hit points of the target creature by the calculated amount.',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'], suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
}, },
damageMultiplier: { damageMultiplier: {
icon: '$vuetify.icons.damage_multiplier', icon: '$vuetify.icons.damage_multiplier',
name: 'Damage multiplier', name: 'Damage multiplier',
docsPath: 'property/damage-multiplier',
helpText: 'Resistance, vulnerability, and immunity.', helpText: 'Resistance, vulnerability, and immunity.',
suggestedParents: ['classLevel', 'feature', 'item'], suggestedParents: ['classLevel', 'feature', 'item'],
}, },
effect: { effect: {
icon: '$vuetify.icons.effect', icon: '$vuetify.icons.effect',
name: 'Effect', name: 'Effect',
docsPath: 'property/effect',
helpText: 'Effects change the value or state of attributes and skills.', helpText: 'Effects change the value or state of attributes and skills.',
examples: '+2 Strength, Advantage on dexterity saving throws', examples: '+2 Strength, Advantage on dexterity saving throws',
suggestedParents: ['buff', 'classLevel', 'feature', 'folder', 'item'], suggestedParents: ['buff', 'classLevel', 'feature', 'folder', 'item'],
@@ -83,42 +96,49 @@ const PROPERTIES = Object.freeze({
feature: { feature: {
icon: 'mdi-text-subject', icon: 'mdi-text-subject',
name: 'Feature', name: 'Feature',
docsPath: 'property/feature',
helpText: 'Descriptive or narrative features your character has access to', helpText: 'Descriptive or narrative features your character has access to',
suggestedParents: ['classLevel', 'folder'], suggestedParents: ['classLevel', 'folder'],
}, },
folder: { folder: {
icon: 'mdi-folder-outline', icon: 'mdi-folder-outline',
name: 'Folder', name: 'Folder',
docsPath: 'property/feature',
helpText: 'A way to organise other properties on the character', helpText: 'A way to organise other properties on the character',
suggestedParents: ['folder'], suggestedParents: ['action', 'folder'],
}, },
item: { item: {
icon: 'mdi-cube-outline', icon: 'mdi-cube-outline',
name: 'Item', name: 'Item',
docsPath: 'property/item',
helpText: 'Objects and equipment your charcter finds on their adventures', helpText: 'Objects and equipment your charcter finds on their adventures',
suggestedParents: ['container'], suggestedParents: ['container'],
}, },
note: { note: {
icon: 'mdi-note-outline', icon: 'mdi-note-outline',
name: 'Note', name: 'Note',
docsPath: 'property/note',
helpText: 'Notes about your character and their adventures', helpText: 'Notes about your character and their adventures',
suggestedParents: ['note', 'folder'], suggestedParents: ['note', 'folder'],
}, },
pointBuy: { pointBuy: {
icon: 'mdi-table', icon: 'mdi-table',
name: 'Point Buy', name: 'Point Buy',
docsPath: 'property/point-buy',
helpText: 'A point buy table that allows the user to select an array of values that match a given cost', helpText: 'A point buy table that allows the user to select an array of values that match a given cost',
suggestedParents: [], suggestedParents: [],
}, },
proficiency: { proficiency: {
icon: 'mdi-brightness-1', icon: 'mdi-brightness-1',
name: 'Proficiency', name: 'Proficiency',
docsPath: 'property/proficiency',
helpText: 'Proficiencies apply your proficiency bonus to skills already on your character sheet.', helpText: 'Proficiencies apply your proficiency bonus to skills already on your character sheet.',
suggestedParents: ['buff', 'classLevel', 'feature', 'folder'], suggestedParents: ['buff', 'classLevel', 'feature', 'folder'],
}, },
roll: { roll: {
icon: '$vuetify.icons.roll', icon: '$vuetify.icons.roll',
name: 'Roll', name: 'Roll',
docsPath: 'property/roll',
helpText: 'When activated by an action, rolls perform a calculation and temporarily store the result for other properties under the same action to use', helpText: 'When activated by an action, rolls perform a calculation and temporarily store the result for other properties under the same action to use',
suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'], suggestedParents: ['action', 'attack', 'savingThrow', 'spell', 'branch'],
}, },
@@ -132,48 +152,56 @@ const PROPERTIES = Object.freeze({
savingThrow: { savingThrow: {
icon: '$vuetify.icons.saving_throw', icon: '$vuetify.icons.saving_throw',
name: 'Saving throw', name: 'Saving throw',
docsPath: 'property/saving-throw',
helpText: 'When a saving throw is activated by an action, it causes the target to make a saving throw, if the saving throw fails, the children properties of the saving throw are activated.', helpText: 'When a saving throw is activated by an action, it causes the target to make a saving throw, if the saving throw fails, the children properties of the saving throw are activated.',
suggestedParents: ['action', 'attack', 'spell'], suggestedParents: ['action', 'attack', 'spell'],
}, },
skill: { skill: {
icon: '$vuetify.icons.skill', icon: '$vuetify.icons.skill',
name: 'Skill', name: 'Skill',
docsPath: 'property/skill',
helpText: 'Skills, saves, languages, and weapon and tool proficiencies are all skills. Skills can have a default proficiency set. Proficiencies and effects can change the value and state of skills.', helpText: 'Skills, saves, languages, and weapon and tool proficiencies are all skills. Skills can have a default proficiency set. Proficiencies and effects can change the value and state of skills.',
suggestedParents: ['classLevel', 'folder'], suggestedParents: ['classLevel', 'folder'],
}, },
propertySlot: { propertySlot: {
icon: 'mdi-power-socket-eu', icon: 'mdi-power-socket-eu',
name: 'Slot', name: 'Slot',
docsPath: 'property/slot',
helpText: 'A slot in the character sheet is used to specify that a property needs to be selected from a library to fill the slot. The slot can determine what tags it is looking for, and any subscribed library property with matching tags can fill the slot', helpText: 'A slot in the character sheet is used to specify that a property needs to be selected from a library to fill the slot. The slot can determine what tags it is looking for, and any subscribed library property with matching tags can fill the slot',
suggestedParents: [], suggestedParents: [],
}, },
slotFiller: { slotFiller: {
icon: 'mdi-power-plug-outline', icon: 'mdi-power-plug-outline',
name: 'Slot filler', name: 'Slot filler',
docsPath: 'property/slot-filler',
helpText: 'A slot filler allows for more advanced logic when it attempts to fill a slot. It can masquarade as any property type, and calculate whether it should fill a slot or not.', helpText: 'A slot filler allows for more advanced logic when it attempts to fill a slot. It can masquarade as any property type, and calculate whether it should fill a slot or not.',
suggestedParents: ['propertySlot'], suggestedParents: ['propertySlot'],
}, },
spellList: { spellList: {
icon: '$vuetify.icons.spell_list', icon: '$vuetify.icons.spell_list',
name: 'Spell list', name: 'Spell list',
docsPath: 'property/spell-list',
helpText: 'A list of spells on your character sheet. It can provide a DC and spell attack bonus to the spells within', helpText: 'A list of spells on your character sheet. It can provide a DC and spell attack bonus to the spells within',
suggestedParents: [], suggestedParents: [],
}, },
spell: { spell: {
icon: '$vuetify.icons.spell', icon: '$vuetify.icons.spell',
name: 'Spell', name: 'Spell',
docsPath: 'property/spell',
helpText: 'A spell your character can potentially cast', helpText: 'A spell your character can potentially cast',
suggestedParents: ['spellList'], suggestedParents: ['spellList'],
}, },
toggle: { toggle: {
icon: '$vuetify.icons.toggle', icon: '$vuetify.icons.toggle',
name: 'Toggle', name: 'Toggle',
docsPath: 'property/toggle',
helpText: 'Togggles allow parts of the character sheet to be turned on and off, either manually or as the result of a calculation.', helpText: 'Togggles allow parts of the character sheet to be turned on and off, either manually or as the result of a calculation.',
suggestedParents: [], suggestedParents: [],
}, },
trigger: { trigger: {
icon: 'mdi-electric-switch', icon: 'mdi-electric-switch',
name: 'Trigger', name: 'Trigger',
docsPath: 'property/trigger',
helpText: 'Triggers apply their children in response to events on the character sheet, such as taking an action or receiving damage', helpText: 'Triggers apply their children in response to events on the character sheet, such as taking an action or receiving damage',
suggestedParents: [], suggestedParents: [],
}, },
@@ -188,3 +216,17 @@ export function getPropertyName(type){
export function getPropertyIcon(type){ export function getPropertyIcon(type){
return type && PROPERTIES[type] && PROPERTIES[type].icon; return type && PROPERTIES[type] && PROPERTIES[type].icon;
} }
const propsByDocsPath = new Map();
for (const key in PROPERTIES) {
const prop = PROPERTIES[key];
if (prop.docsPath) {
propsByDocsPath.set(prop.docsPath, {
...prop,
type: key,
});
}
}
export { propsByDocsPath };

View File

@@ -112,10 +112,10 @@ export default {
} }
}, },
'resolve': { 'resolve': {
comment: 'Forces the given calcultion to resolve into a number', comment: 'Forces the given calcultion to resolve into a number, even in calculations where it would usually keep the unknown values as is',
examples: [ examples: [
{input: 'resolve(someUndefinedVariable + 3 + 4)', result: '7'}, {input: 'resolve(someUndefinedVariable + 3 + 4)', result: '7'},
{input: 'resolve(3d6)', result: '2'}, {input: 'resolve(1d6)', result: '4'},
], ],
arguments: ['parseNode'], arguments: ['parseNode'],
fn: function resolveFn(node){ fn: function resolveFn(node){

View File

View File

@@ -0,0 +1,35 @@
import { propsByDocsPath } from '/imports/constants/PROPERTIES.js';
// Manual doc paths
const docPaths = [
'computed-fields',
'inline-calculations',
'dependency-loops',
'docs',
'tags',
'walkthroughs/create-a-class',
];
const docs = new Map();
docPaths.forEach(path => {
docs.set(path, Assets.getText(`docs/${path}.md`))
});
// Doc paths for properties
propsByDocsPath.forEach(prop => {
docs.set(prop.docsPath, Assets.getText(`docs/${prop.docsPath}.md`));
});
Meteor.publish('docs', function (path) {
if (!path) {
docs.forEach((text, path) => {
this.added('docs', path, { text });
});
} else {
const text = docs.get(path);
if (text) {
this.added('docs', path, { text });
}
}
this.ready();
});

View File

@@ -11,3 +11,4 @@ import '/imports/server/publications/ownedDocuments.js';
import '/imports/server/publications/searchLibraryNodes.js'; import '/imports/server/publications/searchLibraryNodes.js';
import '/imports/server/publications/archiveFiles.js'; import '/imports/server/publications/archiveFiles.js';
import '/imports/server/publications/userImages.js'; import '/imports/server/publications/userImages.js';
import '/imports/server/publications/docs.js';

View File

@@ -4,7 +4,8 @@ import Invites from '/imports/api/users/Invites.js';
Meteor.publish('user', function () { Meteor.publish('user', function () {
return [ return [
Meteor.users.find(this.userId, {fields: { Meteor.users.find(this.userId, {
fields: {
roles: 1, roles: 1,
username: 1, username: 1,
apiKey: 1, apiKey: 1,
@@ -22,7 +23,8 @@ Meteor.publish('user', function(){
'services.google.name': 1, 'services.google.name': 1,
'services.google.email': 1, 'services.google.email': 1,
'services.google.locale': 1, 'services.google.locale': 1,
}}), }
}),
Invites.find({ Invites.find({
$or: [ $or: [
{ inviter: this.userId }, { inviter: this.userId },

View File

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

View File

@@ -27,6 +27,7 @@ export default {
transform: translateZ(0); transform: translateZ(0);
padding: 4px; padding: 4px;
} }
.column-layout.wide-columns { .column-layout.wide-columns {
column-count: 12; column-count: 12;
column-fill: balance; column-fill: balance;
@@ -35,14 +36,18 @@ export default {
transform: translateZ(0); transform: translateZ(0);
padding: 4px; padding: 4px;
} }
.column-layout > div, .column-layout > span > div {
.column-layout>div,
.column-layout>span>div {
/* /*
Table and width set because firefox does not support break-inside: avoid Table and width set because firefox does not support break-inside: avoid
*/ */
display: table; display: table;
table-layout: fixed; table-layout: fixed;
width: 100%; width: 100%;
backface-visibility: hidden;
-webkit-backface-visibility: hidden; -webkit-backface-visibility: hidden;
transform: translateX(0);
-webkit-transform: translateX(0); -webkit-transform: translateX(0);
-webkit-column-break-inside: avoid; -webkit-column-break-inside: avoid;
page-break-inside: avoid; page-break-inside: avoid;

View File

@@ -160,6 +160,7 @@
.filled.theme--light { .filled.theme--light {
background: #fff !important; background: #fff !important;
} }
.filled.theme--dark { .filled.theme--dark {
background: #424242 !important; background: #424242 !important;
} }

View File

@@ -1,7 +1,8 @@
<template lang="html"> <template lang="html">
<!-- eslint-disable-next-line vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<div <div
class="markdown" class="markdown"
@click="e => $emit('click', e)"
v-html="compiledMarkdown" v-html="compiledMarkdown"
/> />
</template> </template>

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

@@ -44,9 +44,11 @@
}, },
transparentToolbar: Boolean, transparentToolbar: Boolean,
}, },
data(){ return { data() {
return {
hovering: false, hovering: false,
}}, }
},
computed: { computed: {
isDark() { isDark() {
return isDarkColor(this.color); return isDarkColor(this.color);
@@ -72,9 +74,11 @@
.toolbar-card .v-toolbar__title { .toolbar-card .v-toolbar__title {
font-size: 15px; font-size: 15px;
} }
.toolbar-card { .toolbar-card {
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1); transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1);
} }
.toolbar-card.transparent-toolbar .theme--dark.v-toolbar.v-sheet { .toolbar-card.transparent-toolbar .theme--dark.v-toolbar.v-sheet {
background-color: #303030; background-color: #303030;
} }

View File

@@ -35,9 +35,11 @@ import { format } from 'date-fns';
export default { export default {
mixins: [SmartInput], mixins: [SmartInput],
data(){return { data() {
return {
menu: false, menu: false,
};}, };
},
computed: { computed: {
formattedSafeValue() { formattedSafeValue() {
return format(this.safeValue, 'YYYY-MM-DD') return format(this.safeValue, 'YYYY-MM-DD')
@@ -53,4 +55,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -97,11 +97,13 @@ export default {
default: undefined, default: undefined,
}, },
}, },
data(){return { data() {
return {
menu: false, menu: false,
searchString: '', searchString: '',
icons: [], icons: [],
};}, };
},
watch: { watch: {
menu(value) { menu(value) {
if (value) { if (value) {
@@ -131,4 +133,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -28,9 +28,11 @@
props: { props: {
multiple: Boolean, multiple: Boolean,
}, },
data(){ return { data() {
return {
searchInput: '', searchInput: '',
}}, }
},
computed: { computed: {
// Multiple combobox gets a long default debounce time while single // Multiple combobox gets a long default debounce time while single
// value gets a shorter one // value gets a shorter one

View File

@@ -13,7 +13,8 @@ export default {
context: { default: {} } context: { default: {} }
}, },
inheritAttrs: false, inheritAttrs: false,
data(){ return { data() {
return {
error: false, error: false,
ackErrors: null, ackErrors: null,
rulesErrors: null, rulesErrors: null,
@@ -22,7 +23,8 @@ export default {
dirty: false, dirty: false,
safeValue: this.value, safeValue: this.value,
inputValue: this.value, inputValue: this.value,
};}, };
},
props: { props: {
value: [String, Number, Date, Array, Object, Boolean], value: [String, Number, Date, Array, Object, Boolean],
errorMessages: [String, Array], errorMessages: [String, Array],

View File

@@ -54,8 +54,22 @@
</v-btn> </v-btn>
</template> </template>
<v-list> <v-list>
<v-list-item
v-if="docsPath"
@click="helpDialog"
>
<v-list-item-content>
<v-list-item-title>
Help
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-icon>mdi-help</v-icon>
</v-list-item-action>
</v-list-item>
<v-list-item <v-list-item
v-if="$listeners && $listeners.duplicate" v-if="$listeners && $listeners.duplicate"
:disabled="context.editPermission === false"
@click="$emit('duplicate')" @click="$emit('duplicate')"
> >
<v-list-item-content> <v-list-item-content>
@@ -67,8 +81,23 @@
<v-icon>mdi-content-copy</v-icon> <v-icon>mdi-content-copy</v-icon>
</v-list-item-action> </v-list-item-action>
</v-list-item> </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-list-item
v-if="$listeners && $listeners.move" v-if="$listeners && $listeners.move"
:disabled="context.editPermission === false"
@click="$emit('move')" @click="$emit('move')"
> >
<v-list-item-content> <v-list-item-content>
@@ -82,6 +111,7 @@
</v-list-item> </v-list-item>
<v-list-item <v-list-item
v-if="$listeners && $listeners.remove" v-if="$listeners && $listeners.remove"
:disabled="context.editPermission === false"
@click="$emit('remove')" @click="$emit('remove')"
> >
<v-list-item-content> <v-list-item-content>
@@ -137,12 +167,16 @@ import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js'; import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import ColorPicker from '/imports/ui/components/ColorPicker.vue'; import ColorPicker from '/imports/ui/components/ColorPicker.vue';
import getThemeColor from '/imports/ui/utility/getThemeColor.js'; import getThemeColor from '/imports/ui/utility/getThemeColor.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js';
export default { export default {
components: { components: {
PropertyIcon, PropertyIcon,
ColorPicker, ColorPicker,
}, },
inject: {
context: { default: {} }
},
props: { props: {
model: { model: {
type: Object, type: Object,
@@ -171,7 +205,11 @@ export default {
} }
} }
return model.name || getPropertyName(model.type); return model.name || getPropertyName(model.type);
} },
docsPath() {
const propDef = PROPERTIES[this.model.type];
return propDef && propDef.docsPath;
},
}, },
methods: { methods: {
colorChanged(value){ colorChanged(value){
@@ -180,6 +218,15 @@ export default {
back(){ back(){
this.$store.dispatch('popDialogStack'); this.$store.dispatch('popDialogStack');
}, },
helpDialog() {
this.$store.commit('pushDialogStack', {
component: 'help-dialog',
elementId: 'property-toolbar-menu-button',
data: {
path: this.docsPath,
},
});
},
} }
} }
</script> </script>

View File

@@ -100,7 +100,7 @@
}, },
group: { group: {
type: String, type: String,
required: true, default: undefined,
}, },
organize: Boolean, organize: Boolean,
children: { children: {
@@ -118,11 +118,13 @@
selected: Boolean, selected: Boolean,
startExpanded: Boolean, startExpanded: Boolean,
}, },
data(){return { data() {
return {
expanded: this.startExpanded || this.node._ancestorOfMatchedDocument || expanded: this.startExpanded || this.node._ancestorOfMatchedDocument ||
some(this.selectedNode?.ancestors, ref => ref.id === this.node._id) || some(this.selectedNode?.ancestors, ref => ref.id === this.node._id) ||
false, false,
}}, }
},
computed: { computed: {
hasChildren() { hasChildren() {
return this.children && !!this.children.length || this.lazy && !this.expanded; return this.children && !!this.children.length || this.lazy && !this.expanded;
@@ -168,43 +170,56 @@
.rotate-90 { .rotate-90 {
transform: rotate(90deg) translateZ(0); transform: rotate(90deg) translateZ(0);
} }
.drag-area { .drag-area {
box-shadow: -2px 0px 0px 0px #808080; box-shadow: -2px 0px 0px 0px #808080;
margin-left: 0; margin-left: 0;
min-height: 32px; min-height: 32px;
} }
.handle { .handle {
cursor: move; cursor: move;
} }
.empty .drag-area { .empty .drag-area {
box-shadow: -2px 0px 0px 0px rgb(128, 128, 128, 0.4); box-shadow: -2px 0px 0px 0px rgb(128, 128, 128, 0.4);
} }
.empty .v-btn { .empty .v-btn {
opacity: 0.4; opacity: 0.4;
} }
.found { .found {
background: rgba(200, 0, 0, 0.1) !important; background: rgba(200, 0, 0, 0.1) !important;
} }
.ghost { .ghost {
opacity: 0.5; opacity: 0.5;
background: rgba(251, 0, 0, 0.3); background: rgba(251, 0, 0, 0.3);
} }
.v-icon.v-icon--disabled { .v-icon.v-icon--disabled {
opacity: 0; opacity: 0;
} }
.v-icon { .v-icon {
transition: none !important; transition: none !important;
} }
.theme--light .tree-node-title:hover { .theme--light .tree-node-title:hover {
background-color: rgba(0, 0, 0, .04); background-color: rgba(0, 0, 0, .04);
} }
.theme--dark .tree-node-title:hover { .theme--dark .tree-node-title:hover {
background-color: rgba(255, 255, 255, .04); background-color: rgba(255, 255, 255, .04);
} }
.tree-node-title { .tree-node-title {
transition: background ease 0.3s, color ease 0.15s; transition: background ease 0.3s, color ease 0.15s;
} }
.tree-node-title, .dummy-node {
.tree-node-title,
.dummy-node {
height: 40px; height: 40px;
} }
</style> </style>

View File

@@ -43,8 +43,14 @@
TreeNode, TreeNode,
}, },
props: { props: {
node: Object, node: {
group: String, type: Object,
default: undefined,
},
group: {
type: String,
default: undefined,
},
organize: Boolean, organize: Boolean,
lazy: Boolean, lazy: Boolean,
children: { children: {
@@ -61,10 +67,12 @@
}, },
startExpanded: Boolean, startExpanded: Boolean,
}, },
data(){ return { data() {
return {
expanded: this.startExpanded || false, expanded: this.startExpanded || false,
displayedChildren: [], displayedChildren: [],
}}, }
},
computed: { computed: {
hasChildren() { hasChildren() {
return this.children && this.children.length; return this.children && this.children.length;
@@ -125,9 +133,11 @@
.flip-list-leave-active { .flip-list-leave-active {
display: none; display: none;
} }
.flip-list-move { .flip-list-move {
transition: transform 0.5s; transition: transform 0.5s;
} }
.no-move { .no-move {
transition: transform 0s; transition: transform 0s;
} }

View File

@@ -39,6 +39,11 @@
:input-value="model.settings.hideUnusedStats" :input-value="model.settings.hideUnusedStats"
@change="value => $emit('change', {path: ['settings','hideUnusedStats'], value: !!value})" @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 <v-switch
label="Show spells tab" label="Show spells tab"
:input-value="!model.settings.hideSpellsTab" :input-value="!model.settings.hideSpellsTab"
@@ -149,7 +154,8 @@ export default {
}, },
disabled: Boolean, disabled: Boolean,
}, },
data() { return { data() {
return {
libraryCollections: this.model.allowedLibraryCollections, libraryCollections: this.model.allowedLibraryCollections,
libraries: this.model.allowedLibraries, libraries: this.model.allowedLibraries,
libraryWriteLoading: false, libraryWriteLoading: false,
@@ -266,4 +272,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -1,5 +1,8 @@
<template lang="html"> <template lang="html">
<dialog-base v-if="model" :color="model.color"> <dialog-base
v-if="model"
:color="model.color"
>
<template slot="toolbar"> <template slot="toolbar">
<v-toolbar-title> <v-toolbar-title>
Character Details Character Details
@@ -81,4 +84,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -28,8 +28,10 @@
default: undefined, default: undefined,
}, },
}, },
data(){ return { data() {
return {
expanded: false, expanded: false,
}}, }
},
}; };
</script> </script>

View File

@@ -43,9 +43,11 @@ export default {
props: { props: {
id: String, id: String,
}, },
data(){return { data() {
return {
inputName: undefined, inputName: undefined,
}}, }
},
computed: { computed: {
nameMatch() { nameMatch() {
if (!this.name) return true; if (!this.name) return true;
@@ -76,4 +78,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -12,9 +12,7 @@
size="64" size="64"
/> />
</div> </div>
<div <div v-else-if="!creature">
v-else-if="!creature"
>
<v-layout <v-layout
column column
align-center align-center
@@ -55,9 +53,7 @@
<v-tab-item> <v-tab-item>
<inventory-tab :creature-id="creatureId" /> <inventory-tab :creature-id="creatureId" />
</v-tab-item> </v-tab-item>
<v-tab-item <v-tab-item v-if="!creature.settings.hideSpellsTab">
v-if="!creature.settings.hideSpellsTab"
>
<spells-tab :creature-id="creatureId" /> <spells-tab :creature-id="creatureId" />
</v-tab-item> </v-tab-item>
<v-tab-item> <v-tab-item>
@@ -66,9 +62,7 @@
<v-tab-item> <v-tab-item>
<build-tab :creature-id="creatureId" /> <build-tab :creature-id="creatureId" />
</v-tab-item> </v-tab-item>
<v-tab-item <v-tab-item v-if="creature.settings.showTreeTab">
v-if="creature.settings.showTreeTab"
>
<tree-tab :creature-id="creatureId" /> <tree-tab :creature-id="creatureId" />
</v-tab-item> </v-tab-item>
</v-tabs-items> </v-tabs-items>

View File

@@ -11,17 +11,13 @@
dense dense
> >
<v-app-bar-nav-icon @click="toggleDrawer" /> <v-app-bar-nav-icon @click="toggleDrawer" />
<v-fade-transition <v-fade-transition mode="out-in">
mode="out-in"
>
<v-toolbar-title :key="$store.state.pageTitle"> <v-toolbar-title :key="$store.state.pageTitle">
{{ $store.state.pageTitle }} {{ $store.state.pageTitle }}
</v-toolbar-title> </v-toolbar-title>
</v-fade-transition> </v-fade-transition>
<v-spacer /> <v-spacer />
<v-fade-transition <v-fade-transition mode="out-in">
mode="out-in"
>
<v-layout <v-layout
:key="$route.meta.title" :key="$route.meta.title"
class="flex-shrink-0 flex-grow-0" class="flex-shrink-0 flex-grow-0"
@@ -249,9 +245,11 @@ export default {
.character-sheet-toolbar .v-tabs__container--grow .v-tabs__div { .character-sheet-toolbar .v-tabs__container--grow .v-tabs__div {
max-width: 120px !important; max-width: 120px !important;
} }
.character-sheet-toolbar .v-tabs__bar { .character-sheet-toolbar .v-tabs__bar {
background: none !important; background: none !important;
} }
.character-sheet-fab { .character-sheet-fab {
bottom: -24px; bottom: -24px;
right: 8px; right: 8px;

View File

@@ -56,4 +56,5 @@
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -30,9 +30,7 @@
</v-list-item-content> </v-list-item-content>
<v-list-item-action> <v-list-item-action>
<v-list-item-title> <v-list-item-title>
<coin-value <coin-value :value="variables && variables.valueTotal && variables.valueTotal.value|| 0" />
:value="variables && variables.valueTotal && variables.valueTotal.value|| 0"
/>
</v-list-item-title> </v-list-item-title>
</v-list-item-action> </v-list-item-action>
</v-list-item> </v-list-item>
@@ -85,9 +83,7 @@
v-for="container in containersWithoutAncestorContainers" v-for="container in containersWithoutAncestorContainers"
:key="container._id" :key="container._id"
> >
<container-card <container-card :model="container" />
:model="container"
/>
</div> </div>
</column-layout> </column-layout>
</div> </div>
@@ -120,9 +116,11 @@ export default {
required: true, required: true,
}, },
}, },
data(){ return { data() {
return {
organize: false, organize: false,
}}, }
},
meteor: { meteor: {
containers() { containers() {
return CreatureProperties.find({ return CreatureProperties.find({
@@ -135,10 +133,12 @@ export default {
}); });
}, },
creature() { creature() {
return Creatures.findOne(this.creatureId, {fields: { return Creatures.findOne(this.creatureId, {
fields: {
color: 1, color: 1,
variables: 1, variables: 1,
}}); }
});
}, },
variables() { variables() {
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {}; return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
@@ -229,4 +229,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -40,9 +40,11 @@ export default {
required: true, required: true,
} }
}, },
data(){ return { data() {
return {
organize: false, organize: false,
}}, }
},
meteor: { meteor: {
spellLists() { spellLists() {
return CreatureProperties.find({ return CreatureProperties.find({
@@ -103,4 +105,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -1,23 +1,53 @@
<template lang="html"> <template lang="html">
<div class="stats-tab ma-2">
<div <div
class="stats-tab ma-2" v-if="healthBars.length"
class="px-2 pt-2"
> >
<health-bar-card-container :creature-id="creatureId" /> <v-card class="pa-2">
<health-bar
v-for="healthBar in healthBars"
:key="healthBar._id"
:model="healthBar"
@change="({ type, value }) => incrementChange(healthBar._id, { type, value: -value })"
@click="clickProperty({_id: healthBar._id})"
/>
</v-card>
</div>
<column-layout> <column-layout>
<div class="character-buttons"> <folder-group-card
v-for="folder in folders"
:key="folder._id"
:model="folder"
@click-property="clickProperty"
@sub-click="_id => clickTreeProperty({_id})"
@remove="softRemove"
/>
<div
v-if="!creature.settings.hideRestButtons || (events && events.length)"
class="character-buttons"
>
<v-card> <v-card>
<v-card-text class="layout column align-center"> <v-card-text class="layout column align-center">
<rest-button <rest-button
v-if="!creature.settings.hideRestButtons"
:creature-id="creatureId" :creature-id="creatureId"
type="shortRest" type="shortRest"
class="ma-1" class="ma-1"
/> />
<rest-button <rest-button
v-if="!creature.settings.hideRestButtons"
:creature-id="creatureId" :creature-id="creatureId"
type="longRest" type="longRest"
class="ma-1" class="ma-1"
/> />
<event-button
v-for="event in events"
:key="event._id"
:model="event"
class="ma-1"
/>
</v-card-text> </v-card-text>
</v-card> </v-card>
</div> </div>
@@ -35,26 +65,14 @@
<v-card> <v-card>
<v-list> <v-list>
<v-subheader>Buffs and conditions</v-subheader> <v-subheader>Buffs and conditions</v-subheader>
<v-list-item <buff-list-item
v-for="buff in appliedBuffs" v-for="buff in appliedBuffs"
:key="buff._id" :key="buff._id"
:data-id="buff._id" :data-id="buff._id"
:model="buff"
@click="clickProperty({_id: buff._id})" @click="clickProperty({_id: buff._id})"
> @remove="softRemove(buff._id)"
<v-list-item-content> />
<v-list-item-title>
{{ buff.name }}
</v-list-item-title>
</v-list-item-content>
<v-list-item-action v-if="!buff.hideRemoveButton">
<v-btn
icon
@click.stop="softRemove(buff._id)"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
</v-list> </v-list>
</v-card> </v-card>
</div> </div>
@@ -171,9 +189,7 @@
v-if="spellSlots && spellSlots.length || hasSpells" v-if="spellSlots && spellSlots.length || hasSpells"
class="spell-slots" class="spell-slots"
> >
<v-card <v-card data-id="spell-slot-card">
data-id="spell-slot-card"
>
<v-list <v-list
v-if="spellSlots && spellSlots.length" v-if="spellSlots && spellSlots.length"
two-line two-line
@@ -186,7 +202,6 @@
:model="spellSlot" :model="spellSlot"
:data-id="spellSlot._id" :data-id="spellSlot._id"
@click="clickProperty({_id: spellSlot._id})" @click="clickProperty({_id: spellSlot._id})"
@cast="castSpellWithSlot(spellSlot._id)"
/> />
</v-list> </v-list>
<div <div
@@ -253,18 +268,6 @@
@sub-click="_id => clickTreeProperty({_id})" @sub-click="_id => clickTreeProperty({_id})"
/> />
</div> </div>
<div
v-for="attack in attacks"
:key="attack._id"
class="attack"
>
<action-card
attack
:model="attack"
:data-id="attack._id"
@click="clickProperty({_id: attack._id})"
/>
</div>
<div <div
v-if="weapons && weapons.length" v-if="weapons && weapons.length"
@@ -354,11 +357,11 @@
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js'; import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import HealthBar from '/imports/ui/properties/components/attributes/HealthBar.vue';
import AttributeCard from '/imports/ui/properties/components/attributes/AttributeCard.vue'; import AttributeCard from '/imports/ui/properties/components/attributes/AttributeCard.vue';
import AbilityListTile from '/imports/ui/properties/components/attributes/AbilityListTile.vue'; import AbilityListTile from '/imports/ui/properties/components/attributes/AbilityListTile.vue';
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue'; import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import DamageMultiplierCard from '/imports/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue'; import DamageMultiplierCard from '/imports/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue';
import HealthBarCardContainer from '/imports/ui/properties/components/attributes/HealthBarCardContainer.vue';
import HitDiceListTile from '/imports/ui/properties/components/attributes/HitDiceListTile.vue'; import HitDiceListTile from '/imports/ui/properties/components/attributes/HitDiceListTile.vue';
import SkillListTile from '/imports/ui/properties/components/skills/SkillListTile.vue'; import SkillListTile from '/imports/ui/properties/components/skills/SkillListTile.vue';
import ResourceCard from '/imports/ui/properties/components/attributes/ResourceCard.vue'; import ResourceCard from '/imports/ui/properties/components/attributes/ResourceCard.vue';
@@ -367,10 +370,14 @@
import RestButton from '/imports/ui/creature/RestButton.vue'; import RestButton from '/imports/ui/creature/RestButton.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import ToggleCard from '/imports/ui/properties/components/toggles/ToggleCard.vue'; import ToggleCard from '/imports/ui/properties/components/toggles/ToggleCard.vue';
import BuffListItem from '/imports/ui/properties/components/buffs/BuffListItem.vue';
import doCastSpell from '/imports/api/engine/actions/doCastSpell.js'; import doCastSpell from '/imports/api/engine/actions/doCastSpell.js';
import EventButton from '/imports/ui/properties/components/actions/EventButton.vue';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js'; import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
import FolderGroupCard from '/imports/ui/properties/components/folders/FolderGroupCard.vue';
import { uniqBy } from 'lodash';
const getProperties = function(creature, filter, options = { const getProperties = function (creature, folderIds, filter, options = {
sort: { order: 1 } sort: { order: 1 }
}) { }) {
if (!creature) return; if (!creature) return;
@@ -378,22 +385,27 @@
filter.hide = { $ne: true }; filter.hide = { $ne: true };
} }
filter['ancestors.id'] = creature._id; filter['ancestors.id'] = creature._id;
filter['parent.id'] = {$nin: folderIds},
filter.removed = { $ne: true }; filter.removed = { $ne: true };
filter.inactive = { $ne: true }; filter.inactive = { $ne: true };
filter.overridden = { $ne: true }; filter.overridden = { $ne: true };
filter.$nor = [
{ hideWhenTotalZero: true, total: 0 },
{ hideWhenValueZero: true, value: 0 },
];
return CreatureProperties.find(filter, options); return CreatureProperties.find(filter, options);
}; };
const getAttributeOfType = function(creature, type){ const getAttributeOfType = function (creature, folderIds, type) {
return getProperties(creature, { return getProperties(creature, folderIds, {
type: 'attribute', type: 'attribute',
attributeType: type, attributeType: type,
}); });
}; };
const getSkillOfType = function(creature, type){ const getSkillOfType = function (creature, folderIds, type) {
return getProperties(creature, { return getProperties(creature, folderIds, {
type: 'skill', type: 'skill',
skillType: type, skillType: type,
}); });
@@ -401,18 +413,21 @@
export default { export default {
components: { components: {
HealthBar,
RestButton, RestButton,
BuffListItem,
AbilityListTile, AbilityListTile,
AttributeCard, AttributeCard,
ColumnLayout, ColumnLayout,
DamageMultiplierCard, DamageMultiplierCard,
HealthBarCardContainer,
HitDiceListTile, HitDiceListTile,
SkillListTile, SkillListTile,
ResourceCard, ResourceCard,
SpellSlotListTile, SpellSlotListTile,
ActionCard, ActionCard,
ToggleCard, ToggleCard,
EventButton,
FolderGroupCard,
}, },
props: { props: {
creatureId: { creatureId: {
@@ -420,23 +435,36 @@
required: true, required: true,
}, },
}, },
data(){return { data() {
return {
doCheckLoading: false, doCheckLoading: false,
}}, }
},
meteor: { meteor: {
creature() { creature() {
return Creatures.findOne(this.creatureId, { fields: { settings: 1 } }); return Creatures.findOne(this.creatureId, { fields: { settings: 1 } });
}, },
folders() {
return getProperties(this.creature, [], { type: 'folder', groupStats: true });
},
folderIds() {
return this.folders.map(f => f._id);
},
healthBars() {
return getAttributeOfType(this.creature, this.folderIds, 'healthBar');
},
abilities() { abilities() {
return getAttributeOfType(this.creature, 'ability'); return getAttributeOfType(this.creature, this.folderIds, 'ability');
}, },
stats() { stats() {
return getAttributeOfType(this.creature, 'stat'); return getAttributeOfType(this.creature, this.folderIds, 'stat');
}, },
toggles() { toggles() {
return CreatureProperties.find({ return CreatureProperties.find({
'ancestors.id': this.creatureId,
type: 'toggle', type: 'toggle',
'ancestors.id': this.creatureId,
'parent.id': { $nin: this.folderIds },
removed: { $ne: true }, removed: { $ne: true },
deactivatedByAncestor: { $ne: true }, deactivatedByAncestor: { $ne: true },
showUI: true, showUI: true,
@@ -445,68 +473,59 @@
}); });
}, },
modifiers() { modifiers() {
return getAttributeOfType(this.creature, 'modifier'); return getAttributeOfType(this.creature, this.folderIds, 'modifier');
}, },
resources() { resources() {
return getAttributeOfType(this.creature, 'resource'); return getAttributeOfType(this.creature, this.folderIds, 'resource');
}, },
spellSlots() { spellSlots() {
return getAttributeOfType(this.creature, 'spellSlot'); return getAttributeOfType(this.creature, this.folderIds, 'spellSlot');
}, },
hasSpells() { hasSpells() {
const cursor = getProperties(this.creature, { const cursor = getProperties(this.creature, this.folderIds, {
type: 'spell', type: 'spell',
}) })
return cursor && cursor.count(); return cursor && cursor.count();
}, },
hitDice() { hitDice() {
return getAttributeOfType(this.creature, 'hitDice'); return getAttributeOfType(this.creature, this.folderIds, 'hitDice');
}, },
checks() { checks() {
return getSkillOfType(this.creature, 'check'); return getSkillOfType(this.creature, this.folderIds, 'check');
}, },
savingThrows() { savingThrows() {
return getSkillOfType(this.creature, 'save'); return getSkillOfType(this.creature, this.folderIds, 'save');
}, },
skills() { skills() {
return getSkillOfType(this.creature, 'skill'); return getSkillOfType(this.creature, this.folderIds, 'skill');
}, },
tools() { tools() {
return getSkillOfType(this.creature, 'tool'); return getSkillOfType(this.creature, this.folderIds, 'tool');
}, },
weapons() { weapons() {
return getSkillOfType(this.creature, 'weapon'); return getSkillOfType(this.creature, this.folderIds, 'weapon');
}, },
armors() { armors() {
return getSkillOfType(this.creature, 'armor'); return getSkillOfType(this.creature, this.folderIds, 'armor');
}, },
languages() { languages() {
return getSkillOfType(this.creature, 'language'); return getSkillOfType(this.creature, this.folderIds, 'language');
},
events() {
const events = getProperties(this.creature, this.folderIds, { type: 'action', actionType: 'event' });
return uniqBy(events.fetch(), e => e.variableName);
}, },
actions() { actions() {
return getProperties(this.creature, {type: 'action'}); return getProperties(this.creature, this.folderIds, { type: 'action', actionType: { $ne: 'event' } });
}, },
appliedBuffs() { appliedBuffs() {
return getProperties(this.creature, {type: 'buff'}); return getProperties(this.creature, this.folderIds, { type: 'buff' });
}, },
multipliers() { multipliers() {
return getProperties(this.creature, { return getProperties(this.creature, this.folderIds, {
type: 'damageMultiplier' type: 'damageMultiplier'
}, { }, {
sort: { value: 1, order: 1 } sort: { value: 1, order: 1 }
});
},
attacks(){
let props = getProperties(this.creature, {type: 'attack'})
return props && props.map(attack => {
attack.children = CreatureProperties.find({
'ancestors.id': attack._id,
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1}
});
return attack;
}); });
}, },
}, },
@@ -526,13 +545,23 @@
}); });
}, },
incrementChange(_id, { type, value }) { incrementChange(_id, { type, value }) {
if (type === 'increment'){ damageProperty.call({
damageProperty.call({_id, operation: 'increment' ,value: -value}); _id,
operation: type,
value: -value
}, error => {
if (error) {
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);
} }
});
}, },
softRemove(_id) { softRemove(_id) {
softRemoveProperty.call({ _id }, error => { softRemoveProperty.call({ _id }, error => {
if (error) console.error(error); if (error) {
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);
}
}); });
}, },
castSpell() { castSpell() {
@@ -542,9 +571,16 @@
data: { data: {
creatureId: this.creatureId, creatureId: this.creatureId,
}, },
callback({spellId, slotId} = {}){ callback({ spellId, slotId, advantage, ritual } = {}) {
if (!spellId) return; if (!spellId) return;
doCastSpell.call({spellId, slotId}, error => { doCastSpell.call({
spellId,
slotId,
ritual,
scope: {
$attackAdvantage: advantage,
},
}, error => {
if (!error) return; if (!error) return;
snackbar({ text: error.reason || error.message || error.toString() }); snackbar({ text: error.reason || error.message || error.toString() });
console.error(error); console.error(error);
@@ -557,4 +593,5 @@
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -0,0 +1,339 @@
<template>
<div class="character-sheet-printed fill-height">
<v-fade-transition mode="out-in">
<div
v-if="!$subReady.singleCharacter"
key="character-loading"
class="fill-height layout justify-center align-center"
>
<v-progress-circular
indeterminate
color="primary"
size="64"
/>
</div>
<div v-else-if="!creature">
<v-layout
column
align-center
justify-center
>
<h2 style="margin: 48px 28px 16px">
Character not found
</h2>
<h3>
Either this character does not exist, or you don't have permission
to view it.
</h3>
</v-layout>
</div>
<v-theme-provider
v-else
light
>
<div class="page pa-3">
<div class="px-3 d-flex align-center">
<div class="logo-background" />
<div class="creature-name mr-3">
{{ creature.name }}
</div>
<div class="text-right flex mr-4">
<div v-if="creature.alignment || background">
{{ creature.alignment }} {{ background }}
</div>
<dir v-if="race || creature.gender">
{{ race }} {{ creature.gender }}
</dir>
<div v-if="level && classes && classes.length === 1">
Level {{ level }} {{ classes[0].name }}
</div>
<div v-else-if="level">
Level {{ level }} ({{ classes.map(c => `${c.name} ${c.level}`).join(', ') }})
</div>
</div>
<qrcode-vue
style="height: 100px"
render-as="svg"
:value="creatureUrl"
/>
</div>
<div
class="text-right mt-3 mr-4"
style="font-size: 8pt; margin-bottom: -4px;"
>
{{ creatureUrl }}
</div>
<printed-stats :creature-id="creatureId" />
<printed-inventory :creature-id="creatureId" />
<printed-spells
v-if="!creature.settings.hideSpellsTab"
:creature-id="creatureId"
/>
</div>
</v-theme-provider>
</v-fade-transition>
</div>
</template>
<script lang="js">
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import PrintedStats from '/imports/ui/creature/character/printedCharacterSheet/PrintedStats.vue';
import PrintedInventory from '/imports/ui/creature/character/printedCharacterSheet/PrintedInventory.vue';
import PrintedSpells from '/imports/ui/creature/character/printedCharacterSheet/PrintedSpells.vue';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
import QrcodeVue from 'qrcode.vue'
export default {
components: {
PrintedStats,
PrintedInventory,
PrintedSpells,
QrcodeVue,
},
computed: {
creatureId() {
return this.$route.params.id
},
creatureUrl() {
let props = this.$router.resolve({
name: 'characterSheet',
params: { id: this.creatureId},
});
return new URL(props?.href, document.baseURI).href
},
level() {
return this.variables?.level?.value;
},
highestLevels(){
let highestLevels = {};
let highestLevelsList = [];
this.classLevels.forEach(classLevel => {
let name = classLevel.variableName;
if (
!highestLevels[name] ||
highestLevels[name].level < classLevel.level
){
highestLevels[name] = classLevel;
}
});
for (let name in highestLevels){
highestLevelsList.push(highestLevels[name]);
}
highestLevelsList.sort((a, b) => a.level - b.level);
return highestLevelsList;
},
classes() {
return [
...this.highestLevels,
...this.classProperties
].sort((a, b) => a.order - b.order);
},
},
reactiveProvide: {
name: 'context',
include: ['creatureId', 'editPermission'],
},
watch: {
'creature.name'(value) {
this.$store.commit('setPageTitle', value ? ('Print ' + value) : 'Print Character Sheet');
},
},
mounted() {
this.$store.commit('setPageTitle',
(this.creature && this.creature.name) ?
('Print ' + this.creature.name) :
'Print Character Sheet'
);
this.nameObserver = Creatures.find({
creatureId: this.creatureId,
}, {
fields: { name: 1 },
}).observe({
added: ({ name }) =>
this.$store.commit('setPageTitle', name ? ('Print ' + name) : 'Print Character Sheet'),
changed: ({ name }) =>
this.$store.commit('setPageTitle', name ? ('Print ' + name) : 'Print Character Sheet'),
});
},
beforeDestroy() {
this.nameObserver.stop();
},
meteor: {
$subscribe: {
'singleCharacter'() {
return [this.creatureId];
},
},
creature() {
return Creatures.findOne(this.creatureId);
},
variables() {
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
},
race() {
if (this.variables?.race?.value?.valueType === 'string') return this.variables.race.value.value;
const prop = CreatureProperties.findOne({
'ancestors.id': this.creatureId,
tags: 'race',
removed: { $ne: true },
inactive: { $ne: true },
overridden: { $ne: true },
});
if (prop?.name) return prop.name;
return '';
},
background() {
if (this.variables?.background?.value?.valueType === 'string') return this.variables.background.value.value;
const prop = CreatureProperties.findOne({
'ancestors.id': this.creatureId,
tags: 'background',
removed: { $ne: true },
inactive: { $ne: true },
overridden: { $ne: true },
});
if (prop?.name) return prop.name;
return '';
},
classProperties(){
return CreatureProperties.find({
'ancestors.id': this.creatureId,
type: 'class',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1}
}).fetch();
},
classLevels() {
const classVariableNames = this.classProperties.map(c => c.variableName)
return CreatureProperties.find({
'ancestors.id': this.creatureId,
type: 'classLevel',
variableName: {$nin: classVariableNames},
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1}
});
},
editPermission() {
try {
assertEditPermission(this.creature, Meteor.userId());
return true;
} catch (e) {
return false;
}
},
},
}
</script>
<style>
.character-sheet-printed {
background: white;
color: black;
font-size: 11pt;
}
.page {
padding: 4px;
}
.character-sheet-printed .inactive {
opacity: 1 !important;
}
.character-sheet-printed .creature-name {
font-size: 24pt;
background-color: white;
}
.character-sheet-printed .logo-background {
width: 60px;
height: 60px;
margin-right: 8px;
background-image: url(/crown-dice-logo-cropped-transparent.png);
background-size: contain;
background-position: 0 center;
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
.character-sheet-printed .v-divider {
border-color: rgba(0,0,0,0.3);
max-width: unset;
}
.character-sheet-printed .double-border {
position: relative;
padding: 11px 10px;
page-break-inside: avoid;
}
.character-sheet-printed .double-border::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border-image-source: url(/images/print/doubleLineImageBorder.png);
border-image-slice: 110 126 fill;
border-image-width: 16px;
border-image-repeat: stretch;
box-sizing: content-box;
z-index: -1;
}
.character-sheet-printed .octagon-border {
position: relative;
padding: 4px 20px;
page-break-inside: avoid;
}
.character-sheet-printed .octagon-border::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border-image: url(/images/print/octagonBorder.png) 124 118 fill;
border-image-width: 22px;
z-index: -1;
}
.character-sheet-printed .stats .label {
font-size: 10pt;
font-variant: small-caps;
}
.character-sheet-printed .label {
font-size: 14pt;
font-variant: all-small-caps;
font-weight: 600;
}
.character-sheet-printed .span-all {
column-span: all;
}
@media screen {
.character-sheet-printed {
display: flex;
flex-direction: column;
align-items: center;
}
.character-sheet-printed .page {
width: 210mm;
}
}
@media print {
header {
display: none !important;
}
nav {
display: none !important;
}
.v-main {
padding: 0 !important;
}
}
</style>

View File

@@ -0,0 +1,270 @@
<template lang="html">
<div
class="inventory"
style="page-break-before: always;"
>
<column-layout wide-columns>
<div class="span-all">
<div class="double-border">
<div class="label text-center">
Inventory
</div>
<div class="d-flex inventory-stat">
<v-icon>$vuetify.icons.injustice</v-icon>
Weight Carried:
{{ weightCarried }} lb
</div>
<div class="d-flex inventory-stat">
<v-icon>$vuetify.icons.cash</v-icon>
Net worth:
<coin-value
class="ml-2"
:value="variables && variables.valueTotal && variables.valueTotal.value|| 0"
/>
</div>
<div class="d-flex inventory-stat">
<v-icon>$vuetify.icons.spell</v-icon>
Items attuned:
{{ variables.itemsAttuned && variables.itemsAttuned.value }}
</div>
</div>
</div>
<div class="span-all">
<div class="octagon-border label text-center">
Equipped
</div>
</div>
<div
v-for="item in equippedItems"
:key="item._id"
>
<printed-item
class="double-border"
:model="item"
/>
</div>
<div class="span-all">
<div class="octagon-border label text-center">
Carried
</div>
</div>
<div
v-for="item in carriedItems"
:key="item._id"
>
<printed-item
class="double-border"
:model="item"
/>
</div>
<template
v-for="container in containersWithoutAncestorContainers"
>
<div
:key="container._id"
class="span-all container-header"
>
<printed-container
class="octagon-border"
:model="container"
/>
</div>
<div
v-for="item in container.items"
:key="item._id"
>
<printed-item
class="double-border"
:model="item"
/>
</div>
</template>
</column-layout>
</div>
</template>
<script lang="js">
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/getParentRefByTag.js';
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
import CoinValue from '/imports/ui/components/CoinValue.vue';
import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js';
import PrintedItem from '/imports/ui/creature/character/printedCharacterSheet/components/PrintedItem.vue';
import PrintedContainer from '/imports/ui/creature/character/printedCharacterSheet/components/PrintedContainer.vue';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
export default {
components: {
ColumnLayout,
CoinValue,
PrintedItem,
PrintedContainer,
},
props: {
creatureId: {
type: String,
required: true,
},
},
data() {
return {
organize: false,
}
},
meteor: {
containers() {
return CreatureProperties.find({
'ancestors.id': this.creatureId,
type: 'container',
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 },
});
},
creature() {
return Creatures.findOne(this.creatureId, {
fields: {
color: 1,
variables: 1,
}
});
},
variables() {
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
},
containersWithoutAncestorContainers() {
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
$nin: this.containerIds
},
type: 'container',
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 },
}).map(c => {
c.items = CreatureProperties.find({
'parent.id': c._id,
type: { $in: ['item', 'container'] },
removed: { $ne: true },
equipped: { $ne: true },
deactivatedByAncestor: { $ne: true },
}, {
sort: { order: 1 },
}).fetch();
return c;
});
},
carriedItems() {
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
$nin: this.containerIds
},
type: 'item',
equipped: { $ne: true },
removed: { $ne: true },
deactivatedByAncestor: { $ne: true },
}, {
sort: { order: 1 },
});
},
equippedItems() {
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
},
type: 'item',
equipped: true,
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 },
});
},
equipmentParentRef() {
return getParentRefByTag(
this.creatureId, BUILT_IN_TAGS.equipment
) || getParentRefByTag(
this.creatureId, BUILT_IN_TAGS.inventory
) || {
id: this.creatureId,
collection: 'creatures'
};
},
carriedParentRef() {
return getParentRefByTag(
this.creatureId, BUILT_IN_TAGS.carried
) || getParentRefByTag(
this.creatureId, BUILT_IN_TAGS.inventory
) || {
id: this.creatureId,
collection: 'creatures'
};
},
},
computed: {
containerIds() {
return this.containers.map(container => container._id);
},
weightCarried() {
return stripFloatingPointOddities(
this.variables &&
this.variables.weightCarried &&
this.variables.weightCarried.value || 0
);
},
},
methods: {
clickProperty(_id) {
this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog',
elementId: `tree-node-${_id}`,
data: { _id },
});
},
},
}
</script>
<style lang="css" scoped>
.octagon-border {
position: relative;
padding: 4px 20px;
page-break-inside: avoid;
}
.octagon-border::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border-image: url(/images/print/octagonBorder.png) 124 118 fill;
border-image-width: 22px;
z-index: -1;
}
.label {
font-size: 14pt;
font-variant: small-caps;
flex-grow: 1;
}
.inventory-stat {
font-size: 12pt;
line-height: 32px;
}
.inventory-stat > .v-icon {
margin-right: 8px;
}
.container-header {
page-break-after: avoid;
page-break-inside: avoid;
}
</style>

View File

@@ -0,0 +1,128 @@
<template lang="html">
<div
class="spells"
style="page-break-before: always;"
>
<column-layout wide-columns>
<div class="span-all">
<div class="label text-center octagon-border">
Spells
</div>
</div>
<div
v-for="spell in spellsWithoutList"
:key="spell._id"
>
<printed-spell :model="spell" />
</div>
<template
v-for="spellList in spellListsWithoutAncestorSpellLists"
>
<div
:key="spellList._id"
class="span-all"
>
<printed-spell-list
:model="spellList"
/>
</div>
<div
v-for="spell in spellList.spells"
:key="spell._id"
>
<printed-spell :model="spell" />
</div>
</template>
</column-layout>
</div>
</template>
<script lang="js">
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import PrintedSpell from '/imports/ui/creature/character/printedCharacterSheet/components/PrintedSpell.vue';
import PrintedSpellList from '/imports/ui/creature/character/printedCharacterSheet/components/PrintedSpellList.vue';
export default {
components: {
ColumnLayout,
PrintedSpell,
PrintedSpellList,
},
props: {
creatureId: {
type: String,
required: true,
}
},
data() {
return {
organize: false,
}
},
meteor: {
spellLists() {
return CreatureProperties.find({
'ancestors.id': this.creatureId,
type: 'spellList',
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 }
});
},
spellsWithoutList() {
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
$nin: this.spellListIds,
},
type: 'spell',
removed: { $ne: true },
deactivatedByAncestor: { $ne: true },
deactivatedByToggle: { $ne: true },
}, {
sort: {
level: 1,
order: 1,
}
});
},
spellListsWithoutAncestorSpellLists() {
return CreatureProperties.find({
'ancestors.id': {
$eq: this.creatureId,
$nin: this.spellListIds,
},
type: 'spellList',
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 }
}).map(sl => {
sl.spells = CreatureProperties.find({
'ancestors.id': sl._id,
type: 'spell',
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: {
level: 1,
order: 1,
}
}).fetch();
return sl;
});
},
},
computed: {
spellListIds() {
return this.spellLists?.map(spellList => spellList._id);
},
},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -0,0 +1,639 @@
<template lang="html">
<div class="stats">
<column-layout>
<div
v-if="abilities.length"
class="ability-scores"
>
<div class="layout flex column">
<div
v-for="ability in abilities"
:key="ability._id"
class="ability"
>
<div class="score">
<div class="double-border top big-number">
<template v-if="creature.settings.swapScoresAndMods">
{{ ability.total }}
</template>
<template v-else>
{{ numberToSignedString(ability.modifier) }}
</template>
</div>
<div class="bottom">
<template v-if="creature.settings.swapScoresAndMods">
{{ numberToSignedString(ability.modifier) }}
</template>
<template v-else>
{{ ability.total }}
</template>
</div>
</div>
<div class="double-border name label">
{{ ability.name }}
</div>
</div>
</div>
</div>
<div
v-for="toggle in toggles"
:key="toggle._id"
class="number-label"
>
<div class="box double-border" />
<div class="label double-border">
{{ toggle.name }}
</div>
</div>
<div
v-for="stat in stats"
:key="stat._id"
class="number-label"
:class="stat.variableName == 'armor' && 'shield-number-label'"
>
<div
:class="stat.variableName == 'armor' ? 'shield-border' : 'octagon-border'"
class="number big-number"
>
{{ stat.value }}
</div>
<div class="label double-border">
{{ stat.name }}
</div>
</div>
<div
v-for="modifier in modifiers"
:key="modifier._id"
class="number-label"
>
<div class="number octagon-border big-number">
{{ numberToSignedString(modifier.value) }}
</div>
<div class="label double-border">
{{ modifier.name }}
</div>
</div>
<div
v-for="check in checks"
:key="check._id"
class="number-label"
>
<div class="number octagon-border big-number">
{{ numberToSignedString(check.value) }}
</div>
<div class="label double-border">
{{ check.name }}
</div>
</div>
<div
v-for="healthBar in healthBars"
:key="healthBar._id"
class="m-2"
>
<div class="double-border">
<div class="label">
Total: {{ healthBar.total }}
</div>
<div style="height: 60px;" />
<div
style="text-align: center;"
class="label"
>
{{ healthBar.name }}
</div>
</div>
</div>
<div v-if="multipliers && multipliers.length">
<printed-damage-multipliers
class="double-border"
:multipliers="multipliers"
/>
</div>
<div
v-if="hitDice.length"
class="hit-dice m-2"
>
<div class="double-border">
<div>
<span class="label">
Total:
</span>
<span
v-for="hitDie in hitDice"
:key="hitDie._id"
style="margin-right: 4px;"
>
{{ hitDie.total }}{{ hitDie.hitDiceSize }}
</span>
</div>
<div style="height: 60px;" />
<div
style="text-align: center;"
class="label"
>
Hit Dice
</div>
</div>
</div>
<div
v-for="resource in resources"
:key="resource._id"
>
<div
class="double-border"
:class="resource.total <= 8 && 'mb-2'"
>
<div
v-if="resource.total <= 8"
class="label"
>
{{ resource.name }}
</div>
<div
v-if="resource.total > 8"
>
total: {{ resource.total }}
<div style="height: 60px;" />
</div>
<div
v-if="resource.total <= 8"
class="d-flex justify-end"
>
<div
v-for="i in resource.total"
:key="i"
class="resource-bubble"
/>
</div>
<div
v-if="resource.total > 8"
class="label text-center"
>
{{ resource.name }}
</div>
</div>
</div>
<div
v-if="spellSlots && spellSlots.length"
>
<div class="double-border">
<div class="label text-center">
Spell Slots
</div>
<div
v-for="spellSlot in spellSlots"
:key="spellSlot._id"
class="mb-7"
:class="spellSlot.total <= 8 && 'mb-7'"
>
<div class="label">
{{ spellSlot.name }}
</div>
<div
v-if="spellSlot.total > 8"
>
Total: {{ spellSlot.total }}
</div>
<div
v-else
class="d-flex"
>
<div
v-for="i in spellSlot.total"
:key="i"
class="resource-bubble"
/>
</div>
</div>
</div>
</div>
<div
v-if="savingThrows.length"
>
<div
class="double-border"
>
<printed-skill
v-for="save in savingThrows"
:key="save._id"
:model="save"
:data-id="save._id"
/>
<div class="label text-center">
Saving Throws
</div>
</div>
</div>
<div
v-if="skills.length"
>
<div
class="double-border"
>
<printed-skill
v-for="skill in skills"
:key="skill._id"
:model="skill"
:data-id="skill._id"
/>
<div class="label text-center">
Skills
</div>
</div>
</div>
<div
v-if="weapons && weapons.length"
>
<div
class="double-border"
>
<printed-skill
v-for="weapon in weapons"
:key="weapon._id"
hide-modifier
:model="weapon"
:data-id="weapon._id"
/>
<div class="label text-center">
Weapons
</div>
</div>
</div>
<div
v-if="armors && armors.length"
>
<div
class="double-border"
>
<printed-skill
v-for="armor in armors"
:key="armor._id"
hide-modifier
:model="armor"
:data-id="armor._id"
/>
<div class="label text-center">
Armor
</div>
</div>
</div>
<div
v-if="tools && tools.length"
>
<div
class="double-border"
>
<printed-skill
v-for="tool in tools"
:key="tool._id"
hide-modifier
:model="tool"
:data-id="tool._id"
/>
<div class="label text-center">
Tools
</div>
</div>
</div>
<div
v-if="languages && languages.length"
>
<div
class="double-border"
>
<printed-skill
v-for="language in languages"
:key="language._id"
hide-modifier
:model="language"
:data-id="language._id"
/>
<div class="label text-center">
Languages
</div>
</div>
</div>
<div
v-for="note in notes"
:key="note._id"
>
<div class="double-border">
<div class="label text-center">
{{ note.name }}
</div>
<property-description
text
:model="note.summary"
/>
</div>
</div>
<div
v-for="action in actions"
:key="action._id"
>
<div class="double-border">
<printed-action
:model="action"
/>
</div>
</div>
<div
v-for="feature in features"
:key="feature._id"
>
<div class="double-border">
<div class="label text-center">
{{ feature.name }}
</div>
<property-description
text
:model="feature.summary"
/>
</div>
</div>
</column-layout>
</div>
</template>
<script lang="js">
import Creatures from '/imports/api/creature/creatures/Creatures.js';
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import PrintedAction from '/imports/ui/creature/character/printedCharacterSheet/components/PrintedAction.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import PrintedSkill from '/imports/ui/creature/character/printedCharacterSheet/components/PrintedSkill.vue';
import PrintedDamageMultipliers from '/imports/ui/creature/character/printedCharacterSheet/components/PrintedDamageMultipliers.vue';
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue';
const getProperties = function (creature, filter, options = {
sort: { order: 1 }
}) {
if (!creature) return;
if (creature.settings.hideUnusedStats) {
filter.hide = { $ne: true };
}
filter['ancestors.id'] = creature._id;
filter.removed = { $ne: true };
filter.inactive = { $ne: true };
filter.overridden = { $ne: true };
filter.$nor = [
{ hideWhenTotalZero: true, total: 0 },
{ hideWhenValueZero: true, value: 0 },
];
return CreatureProperties.find(filter, options);
};
const getAttributeOfType = function (creature, type) {
return getProperties(creature, {
type: 'attribute',
attributeType: type,
});
};
const getSkillOfType = function (creature, type) {
return getProperties(creature, {
type: 'skill',
skillType: type,
});
}
export default {
components: {
ColumnLayout,
PrintedDamageMultipliers,
PrintedAction,
PrintedSkill,
PropertyDescription,
},
props: {
creatureId: {
type: String,
required: true,
},
},
data() {
return {
doCheckLoading: false,
}
},
meteor: {
creature() {
return Creatures.findOne(this.creatureId, { fields: { settings: 1 } });
},
abilities() {
return getAttributeOfType(this.creature, 'ability');
},
stats() {
return getAttributeOfType(this.creature, 'stat');
},
toggles() {
return CreatureProperties.find({
'ancestors.id': this.creatureId,
type: 'toggle',
removed: { $ne: true },
deactivatedByAncestor: { $ne: true },
showUI: true,
}, {
sort: { order: 1 }
});
},
healthBars() {
return getAttributeOfType(this.creature, 'healthBar');
},
modifiers() {
return getAttributeOfType(this.creature, 'modifier');
},
resources() {
return getAttributeOfType(this.creature, 'resource');
},
spellSlots() {
return getAttributeOfType(this.creature, 'spellSlot');
},
hasSpells() {
const cursor = getProperties(this.creature, {
type: 'spell',
})
return cursor && cursor.count();
},
hitDice() {
return getAttributeOfType(this.creature, 'hitDice');
},
checks() {
return getSkillOfType(this.creature, 'check');
},
savingThrows() {
return getSkillOfType(this.creature, 'save');
},
skills() {
return getSkillOfType(this.creature, 'skill');
},
tools() {
return getSkillOfType(this.creature, 'tool');
},
weapons() {
return getSkillOfType(this.creature, 'weapon');
},
armors() {
return getSkillOfType(this.creature, 'armor');
},
languages() {
return getSkillOfType(this.creature, 'language');
},
actions() {
return getProperties(this.creature, { type: 'action' }, {
sort: { actionType: 1, order: 1 }
});
},
appliedBuffs() {
return getProperties(this.creature, { type: 'buff' });
},
multipliers() {
return getProperties(this.creature, {
type: 'damageMultiplier'
}, {
sort: { value: 1, order: 1 }
});
},
features() {
return getProperties(this.creature, { type: 'feature' });
},
notes(){
return getProperties(this.creature, { type: 'note', summary: {$exists: true} });
},
},
methods: {
numberToSignedString,
},
};
</script>
<style lang="css" scoped>
.shield-border {
min-width: 64px !important;
display: flex;
align-items: center;
justify-content: center;
position: relative;
aspect-ratio: 0.87;
padding: 12px;
}
.shield-border::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background: url(/images/print/shieldBorder.png);
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
z-index: -1;
}
.shield-number-label {
align-items: center !important;
}
.big-number {
font-size: 20pt;
}
.ability {
display: flex;
align-items: start;
margin: 4px 0;
}
.ability .score {
display: flex;
flex-direction: column;
align-items: center;
}
.ability .top {
min-width: 64px;
text-align: center;
margin-bottom: -10px;
padding: 14px;
z-index: 1;
}
.ability .bottom {
font-size: 10pt;
position: relative;
padding: 0 16px;
z-index: 2;
}
.ability .bottom::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
border: solid white;
border-image-source: url(/images/print/upwardPointingBorder.png);
border-image-slice: 0 85 fill;
border-image-width: 0 16px;
border-image-outset: 0px 0px;
border-image-repeat: stretch;
box-sizing: content-box;
z-index: -1;
}
.ability .name {
margin-top: 10px;
margin-left: -16px;
padding-left: 20px;
}
.number-label {
display: flex;
align-items: flex-start;
}
.label {
font-size: 10pt;
font-variant: small-caps;
flex-grow: 1;
}
.number-label .label {
margin-top: 4px;
margin-left: -30px;
padding-left: 34px;
z-index: -1;
}
.number-label .number {
min-width: 72px;
text-align: center;
z-index: 1;
}
.number-label .box {
width: 48px;
height: 48px;
margin-left: 10px;
z-index: 1;
}
.resource-bubble {
margin-bottom: -20px;
margin-top: 4px;
margin-right: 4px;
background-color: white;
border: solid black 2px;
border-radius: 50%;
width: 24px;
height: 24px;
}
</style>

View File

@@ -0,0 +1,247 @@
<template lang="html">
<div
class="action-card"
:class="cardClasses"
>
<div class="label text-center">
{{ actionTypeName }}
</div>
<div class="d-flex align-center">
<div class="avatar">
<div
v-if="rollBonus"
>
<template v-if="rollBonus && !rollBonusTooLong">
{{ rollBonus }}
</template>
<property-icon
v-else
:model="model"
color="rgba(0,0,0,0.7)"
/>
</div>
<property-icon
v-else
:model="model"
color="rgba(0,0,0,0.7)"
/>
</div>
<div
class="action-header flex d-flex column justify-center pl-1"
>
<div class="action-title my-1">
{{ model.name || propertyName }}
</div>
</div>
</div>
<div
v-if="Number.isFinite(model.uses)"
class="action-sub-title d-flex align-center"
>
{{ model.uses }} uses
</div>
<div class="pb-3">
<div
v-if="model.resources && model.resources.attributesConsumed.length ||
model.resources.itemsConsumed.length"
class="resources my-2"
>
<div
v-for="attributeConsumed in model.resources.attributesConsumed"
:key="attributeConsumed._id"
class="layout align-center justify-start"
>
Cost: {{ attributeConsumed.quantity && attributeConsumed.quantity.value }} {{ attributeConsumed.statName || attributeConsumed.variableName }}
</div>
<div
v-for="itemConsumed in model.resources.itemsConsumed"
:key="itemConsumed._id"
>
<template v-if="itemConsumed.itemName">
Uses: {{ itemConsumed.quantity && itemConsumed.quantity.value || 0 }} {{ itemConsumed.itemName || itemConsumed.tag }}
</template>
</div>
</div>
<template v-if="model.summary">
<markdown-text :markdown="model.summary.value || model.summary.text" />
</template>
<v-divider v-if="children && children.length" />
<tree-node-list
v-if="children && children.length"
start-expanded
:children="children"
@selected="e => $emit('sub-click', e)"
/>
</div>
</div>
</template>
<script lang="js">
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import AttributeConsumedView from '/imports/ui/properties/components/actions/AttributeConsumedView.vue';
import ItemConsumedView from '/imports/ui/properties/components/actions/ItemConsumedView.vue';
import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
import MarkdownText from '/imports/ui/components/MarkdownText.vue';
import TreeNodeList from '/imports/ui/components/tree/TreeNodeList.vue';
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { some } from 'lodash';
export default {
components: {
AttributeConsumedView,
ItemConsumedView,
MarkdownText,
PropertyIcon,
TreeNodeList,
},
inject: {
context: {
default: {},
},
theme: {
default: {
isDark: false,
},
},
},
props: {
model: {
type: Object,
required: true,
},
},
data() {
return {
activated: undefined,
doActionLoading: false,
hovering: false,
}
},
computed: {
rollBonus() {
if (!this.model.attackRoll) return;
return numberToSignedString(this.model.attackRoll.value);
},
rollBonusTooLong() {
return this.rollBonus && this.rollBonus.length > 3;
},
propertyName() {
return getPropertyName(this.model.type);
},
cardClasses() {
return {
'theme--dark': this.theme.isDark,
'theme--light': !this.theme.isDark,
'muted-text': this.model.insufficientResources,
'active': this.activated,
'elevation-8': this.hovering,
}
},
actionTypeName() {
return {
'action': 'Action',
'bonus': 'Bonus Action',
'attack': 'Attack',
'reaction': 'Reaction',
'free': 'Free Action',
'long': 'Long Action'
}[this.model.actionType] || this.model.actionType
}
},
meteor: {
children() {
const indicesOfTerminatingProps = [];
const decendants = CreatureProperties.find({
'ancestors.id': this.model._id,
'removed': { $ne: true },
}, {
sort: {order: 1}
}).map(prop => {
// Get all the props we don't want to show the decendants of and
// where they might appear in the ancestor list
if (prop.type === 'buff' || prop.type === 'folder') {
indicesOfTerminatingProps.push({
id: prop._id,
ancestorIndex: prop.ancestors.length,
});
}
return prop;
}).filter(prop => {
// Filter out folders entirely
if (prop.type === 'folder') return false;
// Filter out decendants of terminating props
return !some(indicesOfTerminatingProps, buffIndex => {
return prop.ancestors[buffIndex.ancestorIndex]?.id === buffIndex.id;
});
});
return nodeArrayToTree(decendants);
},
},
}
</script>
<style lang="css" scoped>
.action-card {
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1),
transform 0.075s ease;
}
.avatar {
font-size: 18pt;
text-align: center;
min-width: 40px;
min-height: 40px;
}
.label {
font-size: 10pt;
font-variant: small-caps;
flex-grow: 1;
}
.action-title {
font-size: 16px;
font-weight: 400;
line-height: 24px;
position: relative;
text-align: left;
transition: .3s cubic-bezier(.25, .8, .5, 1);
width: 100%;
}
.resources {
font-size: 10pt;
}
.action-child {
height: 32px;
}
.theme--light.muted-text {
color: rgba(0, 0, 0, .3) !important;
}
.theme--dark.muted-text {
color: hsla(0, 0%, 100%, .3) !important;
}
.action-card {
transition: transform 0.15s cubic;
}
</style>
<style lang="css">
.action-card.theme--light.muted-text .v-icon {
color: rgba(0, 0, 0, .3) !important;
}
.action-card.theme--dark.muted-text .v-icon {
color: hsla(0, 0%, 100%, .3) !important;
}
.action-card .property-description>p:last-of-type {
margin-bottom: 0;
}
</style>

View File

@@ -0,0 +1,135 @@
<template>
<div class="container">
<div class="d-flex justify-center">
<property-icon
class="ml-2"
color="rgba(0,0,0,0.7)"
:model="model"
/>
<div class="label">
{{ model.name }}
</div>
</div>
<div
v-if="model.value !== undefined || model.weight !== undefined"
class="weight-value my-2 d-flex justify-space-between"
>
<div class="value ml-4">
<div
v-if="model.value !== undefined"
>
<v-layout align-center>
<v-icon
class="mr-2"
small
>
$vuetify.icons.two_coins
</v-icon>
<coin-value
class="mr-2"
:value="model.value"
/>
</v-layout>
<v-layout
align-center
class="mb-2"
>
<v-icon
class="mr-2"
small
>
$vuetify.icons.cash
</v-icon>
<coin-value
:value="model.contentsValue"
/>
<span
class="ml-1"
>
contents
</span>
</v-layout>
</div>
</div>
<div class="weight ml-4">
<div
v-if="model.weight !== undefined"
>
<v-layout align-center>
<v-icon
class="mr-2"
small
>
$vuetify.icons.weight
</v-icon>
{{ model.weight }} lb
</v-layout>
<v-layout
align-center
class="mb-2"
>
<v-icon
class="mr-2"
small
>
$vuetify.icons.injustice
</v-icon>
{{ model.contentsWeight }} lb
<span
class="ml-1"
>
contents
</span>
</v-layout>
</div>
</div>
</div>
<property-description
text
:model="model.description"
/>
</div>
</template>
<script lang="js">
import treeNodeViewMixin from '/imports/ui/properties/treeNodeViews/treeNodeViewMixin.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js';
import CoinValue from '/imports/ui/components/CoinValue.vue';
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue';
import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js';
export default {
components: {
CoinValue,
PropertyDescription,
},
mixins: [treeNodeViewMixin],
inject: {
context: { default: {} }
},
props: {
preparingSpells: Boolean,
},
data() {
return {
incrementLoading: false,
}
},
computed: {
hasClickListener() {
return this.$listeners && !!this.$listeners.click;
},
},
}
</script>
<style lang="css" scoped>
.item-avatar {
min-width: 32px;
}
</style>

View File

@@ -0,0 +1,71 @@
<template lang="html">
<div>
<div
v-for="(multiplier, multiplierIndex) in multipliers"
:key="multiplier._id"
:data-id="multiplier._id"
@click="$emit('click-multiplier', {_id: multiplier._id})"
>
<v-divider v-if="multiplierIndex" />
<div>
<div
v-if="multiplier.name"
class="label text-center"
>
{{ multiplier.name }}
</div>
<div class="font-weight-medium">
{{ title(multiplier) }}
</div>
<div class="d-flex flex-wrap align-center">
{{ multiplier.damageTypes.join(', ') }}
</div>
<div
v-if="multiplier.includeTags && multiplier.includeTags.length"
class="d-flex flex-wrap align-center"
>
<div>
For:
</div>
{{ multiplier.includeTags.join(', ') }}
</div>
<div
v-if="multiplier.excludeTags && multiplier.excludeTags.length"
class="d-flex flex-wrap align-center"
>
<div>
Except:
</div>
{{ multiplier.excludeTags.join(', ') }}
</div>
</div>
</div>
</div>
</template>
<script lang="js">
export default {
props: {
multipliers:{
type: Array,
required: true,
}
},
methods: {
title(prop){
switch (prop.value){
case 0: return 'Immunity';
case 0.5: return 'Resistance';
case 2: return 'Vulnerability';
}
}
}
}
</script>
<style lang="css" scoped>
.label {
font-size: 10pt;
font-variant: small-caps;
}
</style>

View File

@@ -0,0 +1,173 @@
<template>
<div class="item">
<div class="d-flex justify-space-between">
<div class="label">
{{ title }}
<template v-if="attunementText">
({{ attunementText }})
</template>
</div>
<property-icon
class="ml-2"
color="rgba(0,0,0,0.7)"
:model="model"
/>
</div>
<div
v-if="model.value !== undefined || model.weight !== undefined"
class="weight-value my-2 d-flex justify-space-between"
>
<div class="value ml-4">
<div
v-if="model.value !== undefined"
>
<v-layout
v-if="model.quantity > 1"
align-center
class="mb-2"
>
<v-icon
class="mr-2"
small
>
$vuetify.icons.cash
</v-icon>
<coin-value
:value="model.value * model.quantity"
/>
</v-layout>
<v-layout align-center>
<v-icon
class="mr-2"
small
>
$vuetify.icons.two_coins
</v-icon>
<coin-value
class="mr-2"
:value="model.value"
/>
<span
v-if="model.quantity > 1"
class="ml-1"
>
each
</span>
</v-layout>
</div>
</div>
<div class="weight ml-4">
<div
v-if="model.weight !== undefined"
>
<v-layout
v-if="model.quantity > 1"
align-center
class="mb-2"
>
<v-icon
class="mr-2"
small
>
$vuetify.icons.injustice
</v-icon>
{{ totalWeight }} lb
</v-layout>
<v-layout align-center>
<v-icon
class="mr-2"
small
>
$vuetify.icons.weight
</v-icon>
{{ model.weight }} lb
<span
v-if="model.quantity > 1"
class="ml-1"
>
each
</span>
</v-layout>
</div>
</div>
</div>
<property-description
text
:model="model.description"
/>
</div>
</template>
<script lang="js">
import treeNodeViewMixin from '/imports/ui/properties/treeNodeViews/treeNodeViewMixin.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js';
import CoinValue from '/imports/ui/components/CoinValue.vue';
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue';
import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js';
export default {
components: {
CoinValue,
PropertyDescription,
},
mixins: [treeNodeViewMixin],
inject: {
context: { default: {} }
},
props: {
preparingSpells: Boolean,
},
data() {
return {
incrementLoading: false,
}
},
computed: {
hasClickListener() {
return this.$listeners && !!this.$listeners.click;
},
title() {
let model = this.model;
if (!model) return;
if (model.quantity !== 1) {
if (model.plural) {
return `${model.quantity} ${model.plural}`;
} else if (model.name) {
return `${model.quantity} ${model.name}`;
}
} else if (model.name) {
return model.name;
}
let prop = PROPERTIES[model.type]
return prop && prop.name;
},
totalValue() {
return stripFloatingPointOddities(this.model.value * this.model.quantity);
},
totalWeight() {
return stripFloatingPointOddities(this.model.weight * this.model.quantity);
},
attunementText() {
if (this.model.requiresAttunement) {
if (this.model.attuned) return 'Attuned';
return 'Requires attunement';
}
return undefined;
}
},
}
</script>
<style lang="css" scoped>
.item-avatar {
min-width: 32px;
}
.item .label {
font-size: 14pt;
font-variant: all-small-caps;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,105 @@
<template lang="html">
<div
class="printed-skill pl-0 d-flex align-center"
>
<div class="d-flex align-center">
<div
v-if="!hideModifier"
class="d-flex align-center"
>
<proficiency-icon
:value="model.proficiency"
class="prof-icon"
/>
<div class="prof-mod ml-2 mr-4 text-right">
{{ displayedModifier }}
</div>
<v-icon
v-if="model.advantage > 0"
size="20px"
>
mdi-chevron-double-up
</v-icon>
<v-icon
v-if="model.advantage < 0"
size="20px"
>
mdi-chevron-double-down
</v-icon>
</div>
<proficiency-icon
v-else
:value="model.proficiency"
class="prof-icon mr-2"
/>
<div class="text-truncate">
{{ model.name }}
<template v-if="model.conditionalBenefits && model.conditionalBenefits.length">
*
</template>
<template v-if="'passiveBonus' in model">
({{ passiveScore }})
</template>
</div>
</div>
</div>
</template>
<script lang="js">
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import ProficiencyIcon from '/imports/ui/properties/shared/ProficiencyIcon.vue';
export default {
components: {
ProficiencyIcon,
},
inject: {
context: {
default: {},
},
},
props: {
model: {
type: Object,
required: true,
},
hideModifier: Boolean,
},
data() {
return {
checkLoading: false,
}
},
computed: {
displayedModifier() {
let mod = this.model.value;
if (this.model.fail) {
return 'fail';
} else {
return numberToSignedString(mod);
}
},
passiveScore() {
return 10 + this.model.value + this.model.passiveBonus;
}
},
}
</script>
<style lang="css" scoped>
.printed-skill{
min-height: 30px;
}
.prof-icon {
min-width: 30px;
}
.prof-mod {
min-width: 24px;
}
.v-icon.theme--light {
color: rgba(0, 0, 0, 0.7) !important;
}
</style>

View File

@@ -0,0 +1,82 @@
<template lang="html">
<div
class="double-border"
>
<div
v-if="model.name"
class="label"
>
{{ model.name }}
</div>
<div v-if="model.level">
{{ levelText }} {{ model.school }} {{ model.ritual ? '(ritual)' : '' }}
</div>
<div v-else>
{{ model.school }} cantrip
</div>
<div>
Casting Time: {{ model.castingTime }}
</div>
<div>
Range: {{ model.range }}
</div>
<div>
Components: {{ spellComponents }}
</div>
<div>
Duration: {{ model.duration }}
</div>
<property-description
text
:model="model.summary"
/>
<v-divider class="my-2" />
<property-description
text
:model="model.description"
/>
</div>
</template>
<script lang="js">
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue';
const levelText = [
'cantrip', '1st-level', '2nd-level', '3rd-level', '4th-level', '5th-level',
'6th-level', '7th-level', '8th-level', '9th-level'
];
export default {
components: {
PropertyDescription,
},
props: {
model: {
type: Object,
required: true,
},
},
computed: {
levelText() {
return levelText[this.model.level]
},
spellComponents() {
let components = [];
if (this.model.ritual) components.push('Ritual');
if (this.model.concentration) components.push('Concentration');
if (this.model.verbal) components.push('Verbal');
if (this.model.somatic) components.push('Somatic');
if (this.model.material) components.push(`Material (${this.model.material})`);
return components.join(', ');
},
}
}
</script>
<style lang="css" scoped>
.label {
font-size: 14pt;
font-variant: all-small-caps;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<div class="octagon-border">
<div class="label text-center">
{{ model.name }}
</div>
<div>
Spell Save DC: {{ model.dc && model.dc.value }}
</div>
<div v-if="model.ability">
Spell casting ability: {{ model.ability }}
</div>
<div v-if="model.ability">
Spell casting ability modifier: {{ model.abilityMod }}
</div>
<div>
Spell Attack Bonus: {{ model.attackRollBonus && model.attackRollBonus.value }}
</div>
<div>
Maximum prepared spells: {{ model.maxPrepared && model.maxPrepared.value }}
</div>
<property-description
text
:model="model.description"
/>
</div>
</template>
<script lang="js">
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue';
export default {
components: {
PropertyDescription,
},
props: {
model: {
type: Object,
required: true,
},
},
computed: {
}
}
</script>

View File

@@ -16,6 +16,14 @@
flat flat
@change="propertyHelpChanged" @change="propertyHelpChanged"
/> />
<v-btn
v-if="tab === 1"
icon
data-id="help-button"
@click="helpDialog"
>
<v-icon>mdi-help</v-icon>
</v-btn>
<text-field <text-field
v-if="tab === 2" v-if="tab === 2"
prepend-inner-icon="mdi-magnify" prepend-inner-icon="mdi-magnify"
@@ -173,7 +181,7 @@
<script lang="js"> <script lang="js">
import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import LibraryNodes from '/imports/api/library/LibraryNodes.js';
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue'; import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js'; import PROPERTIES, { getPropertyName } from '/imports/constants/PROPERTIES.js';
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue'; import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue'; import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue';
import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js'; import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js';
@@ -235,7 +243,11 @@ export default {
}, },
toolbarColor(){ toolbarColor(){
return getThemeColor('secondary'); return getThemeColor('secondary');
} },
docsPath() {
const propDef = PROPERTIES[this.type];
return propDef && propDef.docsPath;
},
}, },
watch: { watch: {
type(newType){ type(newType){
@@ -259,6 +271,15 @@ export default {
}); });
}); });
}, },
helpDialog() {
this.$store.commit('pushDialogStack', {
component: 'help-dialog',
elementId: 'help-button',
data: {
path: this.docsPath,
},
});
},
searchChanged(val, ack){ searchChanged(val, ack){
this._subs.searchLibraryNodes.setData('searchTerm', val); this._subs.searchLibraryNodes.setData('searchTerm', val);
this._subs.searchLibraryNodes.setData('limit', undefined); this._subs.searchLibraryNodes.setData('limit', undefined);

View File

@@ -89,4 +89,5 @@
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -28,9 +28,11 @@ export default {
default: undefined, default: undefined,
}, },
}, },
data() { return { data() {
return {
type: undefined, type: undefined,
};}, };
},
methods: { methods: {
getPropertyName, getPropertyName,
back() { back() {
@@ -45,4 +47,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -30,11 +30,14 @@
DialogBase, DialogBase,
LibraryAndNode, LibraryAndNode,
}, },
data(){return { data() {
return {
node: undefined, node: undefined,
};}, };
},
}; };
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -83,4 +83,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -192,7 +192,6 @@ import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/
import Libraries from '/imports/api/library/Libraries.js'; import Libraries from '/imports/api/library/Libraries.js';
import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue'; import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue';
import PropertyTags from '/imports/ui/properties/viewers/shared/PropertyTags.vue'; import PropertyTags from '/imports/ui/properties/viewers/shared/PropertyTags.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import { clone } from 'lodash'; import { clone } from 'lodash';
export default { export default {
@@ -217,13 +216,15 @@ export default {
default: undefined, default: undefined,
}, },
}, },
data(){return { data() {
return {
selectedNodeIds: [], selectedNodeIds: [],
searchInput: undefined, searchInput: undefined,
searchValue: undefined, searchValue: undefined,
showDisabled: false, showDisabled: false,
disabledNodeCount: undefined, disabledNodeCount: undefined,
}}, }
},
reactiveProvide: { reactiveProvide: {
name: 'context', name: 'context',
include: ['creatureId'], include: ['creatureId'],

View File

@@ -63,7 +63,9 @@ export default {
}}, }},
computed: { computed: {
accentColor() { accentColor() {
if (this.theme.isDark){ if (this.model.color) {
return this.model.color
} else if (this.theme.isDark){
return this.$vuetify.theme.themes.dark.primary; return this.$vuetify.theme.themes.dark.primary;
} else { } else {
return this.$vuetify.theme.themes.light.primary; return this.$vuetify.theme.themes.light.primary;

View File

@@ -218,13 +218,15 @@ export default {
default: undefined, default: undefined,
}, },
}, },
data(){return { data() {
return {
selectedNodeIds: [], selectedNodeIds: [],
searchInput: undefined, searchInput: undefined,
searchValue: undefined, searchValue: undefined,
showDisabled: false, showDisabled: false,
disabledNodeCount: undefined, disabledNodeCount: undefined,
}}, }
},
reactiveProvide: { reactiveProvide: {
name: 'context', name: 'context',
include: ['creatureId'], include: ['creatureId'],

View File

@@ -4,7 +4,10 @@
Delete {{ typeName }} Delete {{ typeName }}
</v-toolbar-title> </v-toolbar-title>
<div> <div>
<v-alert type="warning" outlined> <v-alert
type="warning"
outlined
>
This can't be undone This can't be undone
</v-alert> </v-alert>
<p v-if="name"> <p v-if="name">
@@ -12,9 +15,9 @@
</p> </p>
<v-text-field <v-text-field
v-if="name" v-if="name"
v-model="inputName"
label="Confirmation" label="Confirmation"
outlined outlined
v-model="inputName"
/> />
<div class="layout justify-center"> <div class="layout justify-center">
<v-btn <v-btn
@@ -45,12 +48,20 @@ export default {
DialogBase, DialogBase,
}, },
props: { props: {
typeName: String, typeName: {
name: String, type: String,
default: undefined,
}, },
data(){return { name: {
type: String,
default: undefined,
},
},
data() {
return {
inputName: undefined, inputName: undefined,
}}, }
},
computed: { computed: {
nameMatch() { nameMatch() {
if (!this.name) return true; if (!this.name) return true;
@@ -63,4 +74,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -65,9 +65,11 @@
}, },
darkBody: Boolean, darkBody: Boolean,
}, },
data(){ return { data() {
return {
offsetTop: 0, offsetTop: 0,
}}, }
},
computed: { computed: {
isDark() { isDark() {
return isDarkColor(this.computedColor); return isDarkColor(this.computedColor);
@@ -99,13 +101,17 @@
z-index: 2; z-index: 2;
border-radius: 2px 2px 0 0; border-radius: 2px 2px 0 0;
} }
#base-dialog-body, .unwrapped-content {
#base-dialog-body,
.unwrapped-content {
flex-grow: 1; flex-grow: 1;
overflow: auto; overflow: auto;
} }
#base-dialog-body.dark-body { #base-dialog-body.dark-body {
background-color: #fafafa; background-color: #fafafa;
} }
.theme--dark #base-dialog-body.dark-body { .theme--dark #base-dialog-body.dark-body {
background-color: #303030; background-color: #303030;
} }

View File

@@ -1,18 +1,26 @@
const AddCreaturePropertyDialog = () => import('/imports/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue'); // Load commonly used dialogs immediately
import AddCreaturePropertyDialog from '/imports/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue';
import CharacterCreationDialog from '/imports/ui/creature/character/CharacterCreationDialog.vue';
import CastSpellWithSlotDialog from '/imports/ui/properties/components/spells/CastSpellWithSlotDialog.vue';
import CreatureFormDialog from '/imports/ui/creature/CreatureFormDialog.vue';
import CreaturePropertyCreationDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyCreationDialog.vue';
import CreaturePropertyDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue';
import CreaturePropertyFromLibraryDialog from '/imports/ui/creature/creatureProperties/CreaturePropertyFromLibraryDialog.vue';
import CreatureRootDialog from '/imports/ui/creature/character/CreatureRootDialog.vue';
import DeleteConfirmationDialog from '/imports/ui/dialogStack/DeleteConfirmationDialog.vue';
import ExperienceInsertDialog from '/imports/ui/creature/experiences/ExperienceInsertDialog.vue';
import ExperienceListDialog from '/imports/ui/creature/experiences/ExperienceListDialog.vue';
import HelpDialog from '/imports/ui/dialogStack/HelpDialog.vue';
import LevelUpDialog from '/imports/ui/creature/slots/LevelUpDialog.vue';
import SelectLibraryNodeDialog from '/imports/ui/library/SelectLibraryNodeDialog.vue';
import SlotFillDialog from '/imports/ui/creature/slots/SlotFillDialog.vue';
import TierTooLowDialog from '/imports/ui/user/TierTooLowDialog.vue';
import TransferOwnershipDialog from '/imports/ui/sharing/TransferOwnershipDialog.vue';
// Lazily load less common dialogs
const ArchiveDialog = () => import('/imports/ui/creature/archive/ArchiveDialog.vue'); const ArchiveDialog = () => import('/imports/ui/creature/archive/ArchiveDialog.vue');
const CharacterCreationDialog = () => import('/imports/ui/creature/character/CharacterCreationDialog.vue');
const CastSpellWithSlotDialog = () => import('/imports/ui/properties/components/spells/CastSpellWithSlotDialog.vue');
const CreatureFormDialog = () => import('/imports/ui/creature/CreatureFormDialog.vue');
const CreaturePropertyCreationDialog = () => import('/imports/ui/creature/creatureProperties/CreaturePropertyCreationDialog.vue');
const CreaturePropertyDialog = () => import('/imports/ui/creature/creatureProperties/CreaturePropertyDialog.vue');
const CreaturePropertyFromLibraryDialog = () => import('/imports/ui/creature/creatureProperties/CreaturePropertyFromLibraryDialog.vue');
const CreatureRootDialog = () => import('/imports/ui/creature/character/CreatureRootDialog.vue');
const DeleteConfirmationDialog = () => import('/imports/ui/dialogStack/DeleteConfirmationDialog.vue');
const DeleteUserAccountDialog = () => import('/imports/ui/user/DeleteUserAccountDialog.vue'); const DeleteUserAccountDialog = () => import('/imports/ui/user/DeleteUserAccountDialog.vue');
const ExperienceInsertDialog = () => import( '/imports/ui/creature/experiences/ExperienceInsertDialog.vue');
const ExperienceListDialog = () => import( '/imports/ui/creature/experiences/ExperienceListDialog.vue');
const InviteDialog = () => import('/imports/ui/user/InviteDialog.vue'); const InviteDialog = () => import('/imports/ui/user/InviteDialog.vue');
const LevelUpDialog = () => import('/imports/ui/creature/slots/LevelUpDialog.vue');
const LibraryCollectionCreationDialog = () => import('/imports/ui/library/LibraryCollectionCreationDialog.vue'); const LibraryCollectionCreationDialog = () => import('/imports/ui/library/LibraryCollectionCreationDialog.vue');
const LibraryCollectionEditDialog = () => import('/imports/ui/library/LibraryCollectionEditDialog.vue'); const LibraryCollectionEditDialog = () => import('/imports/ui/library/LibraryCollectionEditDialog.vue');
const LibraryCreationDialog = () => import('/imports/ui/library/LibraryCreationDialog.vue'); const LibraryCreationDialog = () => import('/imports/ui/library/LibraryCreationDialog.vue');
@@ -21,11 +29,7 @@ const LibraryNodeCreationDialog = () => import('/imports/ui/library/LibraryNodeC
const LibraryNodeDialog = () => import('/imports/ui/library/LibraryNodeDialog.vue'); const LibraryNodeDialog = () => import('/imports/ui/library/LibraryNodeDialog.vue');
const MoveLibraryNodeDialog = () => import('/imports/ui/library/MoveLibraryNodeDialog.vue'); const MoveLibraryNodeDialog = () => import('/imports/ui/library/MoveLibraryNodeDialog.vue');
const SelectCreaturesDialog = () => import('/imports/ui/tabletop/SelectCreaturesDialog.vue'); const SelectCreaturesDialog = () => import('/imports/ui/tabletop/SelectCreaturesDialog.vue');
const SelectLibraryNodeDialog = () => import('/imports/ui/library/SelectLibraryNodeDialog.vue');
const ShareDialog = () => import('/imports/ui/sharing/ShareDialog.vue'); const ShareDialog = () => import('/imports/ui/sharing/ShareDialog.vue');
const SlotFillDialog = () => import('/imports/ui/creature/slots/SlotFillDialog.vue');
const TierTooLowDialog = () => import('/imports/ui/user/TierTooLowDialog.vue');
const TransferOwnershipDialog = () => import('/imports/ui/sharing/TransferOwnershipDialog.vue');
const UsernameDialog = () => import('/imports/ui/user/UsernameDialog.vue'); const UsernameDialog = () => import('/imports/ui/user/UsernameDialog.vue');
export default { export default {
@@ -42,6 +46,7 @@ export default {
DeleteUserAccountDialog, DeleteUserAccountDialog,
ExperienceInsertDialog, ExperienceInsertDialog,
ExperienceListDialog, ExperienceListDialog,
HelpDialog,
InviteDialog, InviteDialog,
LevelUpDialog, LevelUpDialog,
LibraryCollectionCreationDialog, LibraryCollectionCreationDialog,

View File

@@ -0,0 +1,108 @@
<template lang="html">
<dialog-base>
<v-icon
slot="toolbar"
class="mr-2"
>
mdi-help
</v-icon>
<v-toolbar-title slot="toolbar">
Help: {{ title }}
</v-toolbar-title>
<div>
<v-progress-circular
v-if="!doc && !$subReady.docs"
indeterminate
color="primary"
size="32"
/>
<div v-else-if="!doc">
Help document not found for {{ title }}
</div>
<markdown-text
v-else
:markdown="doc"
@click="linkClick"
/>
</div>
<v-spacer slot="actions" />
<v-btn
slot="actions"
text
@click="$store.dispatch('popDialogStack')"
>
Close
</v-btn>
</dialog-base>
</template>
<script lang="js">
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
import { propsByDocsPath } from '/imports/constants/PROPERTIES.js';
import MarkdownText from '/imports/ui/components/MarkdownText.vue';
import Docs from '/imports/api/docs/Docs.js';
export default {
components: {
DialogBase,
MarkdownText,
},
props: {
path: {
type: String,
required: true,
}
},
computed: {
prop() {
return propsByDocsPath.get(this.path);
},
title() {
if (this.prop) {
return this.prop.name;
} else {
const titleCase = this.path.replace(
/(\w*)(\W+)/g,
function (txt, word) {
return word.charAt(0).toUpperCase() + word.substr(1).toLowerCase() + ' ';
}
);
return titleCase || 'Character Sheet';
}
}
},
meteor: {
$subscribe: {
'docs'() {
return [this.path];
},
},
doc() {
const doc = Docs.findOne(this.path);
return doc && doc.text;
},
},
methods: {
linkClick(e) {
const target = e.target || e.srcElement;
const href = target && target.href;
if (!href) return;
const path = href.split('/docs/')[1];
if (!path) return;
e.preventDefault();
target.dataset.id = path;
this.$store.commit('pushDialogStack', {
component: 'help-dialog',
elementId: path,
data: {
path,
},
});
},
},
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,49 +0,0 @@
<template lang="html">
<div>
<div
v-for="fn in functions"
:key="fn.name"
class="mb-3"
>
<h3>{{ fn.name }}</h3>
<div class="my-2">
{{ fn.comment }}
</div>
<table>
<tr
v-for="example in fn.examples"
:key="example.input"
>
<td>
<code>{{ example.input }}</code>
</td>
<td>
<v-icon>mdi-arrow-right-thick</v-icon>
</td>
<td>
<code>{{ example.result }}</code>
</td>
</tr>
</table>
</div>
</div>
</template>
<script lang="js">
import functions from '/imports/parser/functions.js';
export default {
computed:{
functions(){
let fns = [];
for (let name in functions){
let f = functions[name];
fns.push({name, ...f});
}
return fns;
}
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -44,10 +44,12 @@ export default {
required: true, required: true,
}, },
}, },
data(){return { data() {
return {
restoreLoading: false, restoreLoading: false,
removeLoading: false, removeLoading: false,
}}, }
},
meteor: { meteor: {
characterSlots() { characterSlots() {
return characterSlotsRemaining(Meteor.userId()); return characterSlotsRemaining(Meteor.userId());

View File

@@ -34,7 +34,6 @@
</template> </template>
<script lang="js"> <script lang="js">
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default { export default {
props: { props: {
@@ -43,10 +42,12 @@ export default {
required: true, required: true,
}, },
}, },
data(){return { data() {
return {
restoreLoading: false, restoreLoading: false,
removeLoading: false, removeLoading: false,
}}, }
},
methods: { methods: {
remove() { remove() {

View File

@@ -6,9 +6,7 @@
> >
<Sidebar /> <Sidebar />
</v-navigation-drawer> </v-navigation-drawer>
<router-view <router-view name="toolbar" />
name="toolbar"
/>
<v-app-bar <v-app-bar
v-if="!$route.matched[0] || !$route.matched[0].components.toolbar" v-if="!$route.matched[0] || !$route.matched[0].components.toolbar"
app app
@@ -19,22 +17,16 @@
> >
<v-app-bar-nav-icon @click="toggleDrawer" /> <v-app-bar-nav-icon @click="toggleDrawer" />
<v-toolbar-title> <v-toolbar-title>
<v-fade-transition <v-fade-transition mode="out-in">
mode="out-in"
>
<div :key="$store.state.pageTitle"> <div :key="$store.state.pageTitle">
{{ $store.state.pageTitle }} {{ $store.state.pageTitle }}
</div> </div>
</v-fade-transition> </v-fade-transition>
</v-toolbar-title> </v-toolbar-title>
<v-spacer /> <v-spacer />
<v-fade-transition <v-fade-transition mode="out-in">
mode="out-in"
>
<div :key="$route.meta.title"> <div :key="$route.meta.title">
<router-view <router-view name="toolbarItems" />
name="toolbarItems"
/>
</div> </div>
</v-fade-transition> </v-fade-transition>
<v-fade-transition <v-fade-transition
@@ -45,22 +37,16 @@
:key="$route.meta.title" :key="$route.meta.title"
style="width: 100%" style="width: 100%"
> >
<router-view <router-view name="toolbarExtension" />
name="toolbarExtension"
/>
</div> </div>
</v-fade-transition> </v-fade-transition>
</v-app-bar> </v-app-bar>
<v-main> <v-main>
<v-fade-transition <v-fade-transition mode="out-in">
mode="out-in"
>
<router-view /> <router-view />
</v-fade-transition> </v-fade-transition>
</v-main> </v-main>
<router-view <router-view name="rightDrawer" />
name="rightDrawer"
/>
<dialog-stack /> <dialog-stack />
<snackbar-queue /> <snackbar-queue />
</v-app> </v-app>
@@ -80,10 +66,12 @@
DialogStack, DialogStack,
SnackbarQueue, SnackbarQueue,
}, },
data(){return { data() {
return {
name: 'Home', name: 'Home',
tabs: 0, tabs: 0,
}}, }
},
computed: { computed: {
drawer: { drawer: {
get() { get() {
@@ -124,4 +112,5 @@
</script> </script>
<style> <style>
</style> </style>

View File

@@ -99,6 +99,7 @@
{title: 'Files', icon: 'mdi-file-multiple', to: '/my-files'}, {title: 'Files', icon: 'mdi-file-multiple', to: '/my-files'},
{title: 'Feedback', icon: 'mdi-bug', to: '/feedback'}, {title: 'Feedback', icon: 'mdi-bug', to: '/feedback'},
{title: 'About', icon: 'mdi-sign-text', to: '/about'}, {title: 'About', icon: 'mdi-sign-text', to: '/about'},
{title: 'Documentation', icon: 'mdi-book-open-variant', to: '/docs'},
{title: 'Patreon', icon: 'mdi-patreon', href: 'https://www.patreon.com/dicecloud'}, {title: 'Patreon', icon: 'mdi-patreon', href: 'https://www.patreon.com/dicecloud'},
{title: 'Github', icon: 'mdi-github', href: 'https://github.com/ThaumRystra/DiceCloud/tree/version-2'}, {title: 'Github', icon: 'mdi-github', href: 'https://github.com/ThaumRystra/DiceCloud/tree/version-2'},
]; ];

View File

@@ -144,4 +144,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -43,9 +43,11 @@ import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions
import { mapMutations } from 'vuex'; import { mapMutations } from 'vuex';
export default { export default {
data(){ return { data() {
return {
loading: false, loading: false,
}}, }
},
meteor: { meteor: {
libraryCollection() { libraryCollection() {
return LibraryCollections.findOne(this.$route.params.id); return LibraryCollections.findOne(this.$route.params.id);
@@ -106,4 +108,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -1,7 +1,5 @@
<template lang="html"> <template lang="html">
<v-fade-transition <v-fade-transition hide-on-leave>
hide-on-leave
>
<tree-node-list <tree-node-list
v-if="slowShouldSubscribe && $subReady.libraryNodes" v-if="slowShouldSubscribe && $subReady.libraryNodes"
group="library" group="library"
@@ -53,9 +51,11 @@
default: undefined, default: undefined,
}, },
}, },
data(){return { data() {
return {
slowShouldSubscribe: this.shouldSubscribe, slowShouldSubscribe: this.shouldSubscribe,
};}, };
},
watch: { watch: {
shouldSubscribe(newValue) { shouldSubscribe(newValue) {
if (this.timeoutId) { if (this.timeoutId) {
@@ -132,4 +132,5 @@
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -5,7 +5,6 @@
New Library New Library
</v-toolbar-title> </v-toolbar-title>
</template> </template>
<template>
<text-field <text-field
label="Name" label="Name"
:value="library.name" :value="library.name"
@@ -18,7 +17,6 @@
:debounce-time="0" :debounce-time="0"
@change="descriptionChanged" @change="descriptionChanged"
/> />
</template>
<template slot="actions"> <template slot="actions">
<v-spacer /> <v-spacer />
<v-btn <v-btn
@@ -39,13 +37,15 @@
components: { components: {
DialogBase, DialogBase,
}, },
data(){ return { data() {
return {
library: { library: {
name: 'New Library', name: 'New Library',
description: undefined, description: undefined,
}, },
valid: true, valid: true,
}}, }
},
methods: { methods: {
nameChanged(val, ack) { nameChanged(val, ack) {
if (val) { if (val) {
@@ -66,4 +66,5 @@
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -164,4 +164,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

View File

@@ -18,9 +18,11 @@ export default {
SelectablePropertyDialog, SelectablePropertyDialog,
LibraryNodeInsertForm, LibraryNodeInsertForm,
}, },
data() { return { data() {
return {
type: undefined, type: undefined,
};}, };
},
methods: { methods: {
getPropertyName, getPropertyName,
}, },
@@ -28,4 +30,5 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

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