Compare commits

...

55 Commits

Author SHA1 Message Date
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
Stefan Zermatten
b6ed9ffb74 Merge branch 'version-2-dev' into version-2 2022-08-25 15:24:25 +02:00
Stefan Zermatten
a84da7d8a5 Buffs can skip var freezing, freeze inline calcs 2022-08-25 15:10:56 +02:00
Stefan Zermatten
249aebea0f Allowed some properties to return damaged action values
When a prop is damaged during an action, it now tries
to show its new value during the rest of that action
2022-08-25 15:10:36 +02:00
Stefan Zermatten
11a527481e Show Point buy on Build tab 2022-08-25 13:18:24 +02:00
Stefan Zermatten
8d729216b5 Properties now have their variable name as a default tag 2022-08-25 12:15:12 +02:00
Stefan Zermatten
1677e8c424 Fixed silence missing from trigger form 2022-08-25 12:14:32 +02:00
Stefan Zermatten
987aacbb67 Silence for triggers also 2022-08-25 12:12:07 +02:00
Stefan Zermatten
2714d0b9d5 Added the ability to silence most action props 2022-08-25 12:10:51 +02:00
Stefan Zermatten
1d98c41168 Fixed slotLevel not having the right value in spell scope 2022-08-25 11:40:30 +02:00
Stefan Zermatten
e42ec4b862 Continued work on point buy UI 2022-08-23 14:44:35 +02:00
Stefan Zermatten
59fc5ab851 Continued work on point buy UI 2022-08-22 15:07:40 +02:00
Stefan Zermatten
5d14c392e8 Added creature new variables to API 2022-08-22 11:58:48 +02:00
Stefan Zermatten
c6ca8c1fa4 Added point buy to computation engine 2022-08-19 14:03:12 +02:00
Stefan Zermatten
28307e26c3 Fixed some issues with skill display 2022-08-19 14:03:03 +02:00
Stefan Zermatten
6d42eb62f0 Merge branch 'version-2' into version-2-dev 2022-08-19 09:18:55 +02:00
Stefan Zermatten
877c9ca099 Fixed cache bashing in checks
Cache should only return clones of data,
not references to the cached data
2022-08-17 17:21:18 +02:00
Stefan Zermatten
9b652fc133 Added point buy form 2022-08-17 13:42:47 +02:00
333 changed files with 11123 additions and 6904 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

@@ -24,7 +24,7 @@ const damageProperty = new ValidatedMethod({
run({ _id, operation, value }) { run({ _id, operation, value }) {
// Get action context // Get action context
const prop = CreatureProperties.findOne(_id); let prop = CreatureProperties.findOne(_id);
if (!prop) throw new Meteor.Error( if (!prop) throw new Meteor.Error(
'Damage property failed', 'Property doesn\'t exist' 'Damage property failed', 'Property doesn\'t exist'
); );
@@ -43,6 +43,14 @@ const damageProperty = new ValidatedMethod({
); );
} }
// Replace the prop by its actionContext counterpart if possible
if (prop.variableName) {
const actionContextProp = actionContext.scope[prop.variableName];
if (actionContextProp?._id === prop._id) {
prop = actionContextProp;
}
}
const result = damagePropertyWork({ prop, operation, value, actionContext }); const result = damagePropertyWork({ prop, operation, value, actionContext });
// Insert the log // Insert the log
@@ -94,6 +102,9 @@ export function damagePropertyWork({ prop, operation, value, actionContext }) {
}, { }, {
selector: prop selector: prop
}); });
// Also write it straight to the prop so that it is updated in the actionContext
prop.damage = damage;
prop.value = newValue;
} else if (operation === 'increment') { } else if (operation === 'increment') {
let currentValue = prop.value || 0; let currentValue = prop.value || 0;
let currentDamage = prop.damage || 0; let currentDamage = prop.damage || 0;
@@ -111,6 +122,9 @@ export function damagePropertyWork({ prop, operation, value, actionContext }) {
}, { }, {
selector: prop selector: prop
}); });
// Also write it straight to the prop so that it is updated in the actionContext
prop.damage += increment;
prop.value -= increment;
} }
applyTriggers(actionContext.triggers?.damageProperty?.after, prop, actionContext); applyTriggers(actionContext.triggers?.damageProperty?.after, prop, actionContext);

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

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

@@ -20,7 +20,7 @@ export default function applyAction(node, actionContext) {
recalculateInlineCalculations(prop.summary, actionContext); recalculateInlineCalculations(prop.summary, actionContext);
content.value = prop.summary.value; content.value = prop.summary.value;
} }
actionContext.addLog(content); if (!prop.silent) actionContext.addLog(content);
// Spend the resources // Spend the resources
const failed = spendResources(prop, actionContext); const failed = spendResources(prop, actionContext);
@@ -188,7 +188,7 @@ function applyChildren(node, actionContext) {
function spendResources(prop, actionContext){ function spendResources(prop, actionContext){
// Check Uses // Check Uses
if (prop.usesLeft <= 0){ if (prop.usesLeft <= 0){
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Error', name: 'Error',
value: `${prop.name || 'action'} does not have enough uses left`, value: `${prop.name || 'action'} does not have enough uses left`,
}); });
@@ -196,7 +196,7 @@ function spendResources(prop, actionContext){
} }
// Resources // Resources
if (prop.insufficientResources){ if (prop.insufficientResources){
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Error', name: 'Error',
value: 'This creature doesn\'t have sufficient resources to perform this action', value: 'This creature doesn\'t have sufficient resources to perform this action',
}); });
@@ -257,7 +257,7 @@ function spendResources(prop, actionContext){
}, { }, {
selector: prop selector: prop
}); });
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Uses left', name: 'Uses left',
value: prop.usesLeft - 1, value: prop.usesLeft - 1,
inline: true, inline: true,
@@ -288,12 +288,12 @@ function spendResources(prop, actionContext){
}); });
// Log all the spending // Log all the spending
if (gainLog.length) actionContext.addLog({ if (gainLog.length && !prop.silent) actionContext.addLog({
name: 'Gained', name: 'Gained',
value: gainLog.join('\n'), value: gainLog.join('\n'),
inline: true, inline: true,
}); });
if (spendLog.length) actionContext.addLog({ if (spendLog.length && !prop.silent) actionContext.addLog({
name: 'Spent', name: 'Spent',
value: spendLog.join('\n'), value: spendLog.join('\n'),
inline: true, inline: true,

View File

@@ -24,7 +24,7 @@ export default function applyAdjustment(node, actionContext){
damageTargets.forEach(target => { damageTargets.forEach(target => {
let stat = target.variables[prop.stat]; let stat = target.variables[prop.stat];
if (!stat?.type) { if (!stat?.type) {
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Error', name: 'Error',
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set` value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`
}); });
@@ -36,7 +36,7 @@ export default function applyAdjustment(node, actionContext){
value, value,
actionContext, actionContext,
}); });
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Attribute damage', name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` + value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`, ` ${value}`,
@@ -44,7 +44,7 @@ export default function applyAdjustment(node, actionContext){
}); });
}); });
} else { } else {
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Attribute damage', name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` + value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`, ` ${value}`,

View File

@@ -36,25 +36,25 @@ export default function applyBranch(node, actionContext){
break; break;
case 'hit': case 'hit':
if (scope['$attackHit']?.value){ if (scope['$attackHit']?.value){
if (!targets.length) actionContext.addLog({value: '**On hit**'}); if (!targets.length && !prop.silent) actionContext.addLog({value: '**On hit**'});
applyChildren(); applyChildren();
} }
break; break;
case 'miss': case 'miss':
if (scope['$attackMiss']?.value){ if (scope['$attackMiss']?.value){
if (!targets.length) actionContext.addLog({value: '**On miss**'}); if (!targets.length && !prop.silent) actionContext.addLog({value: '**On miss**'});
applyChildren(); applyChildren();
} }
break; break;
case 'failedSave': case 'failedSave':
if (scope['$saveFailed']?.value){ if (scope['$saveFailed']?.value){
if (!targets.length) actionContext.addLog({value: '**On failed save**'}); if (!targets.length && !prop.silent) actionContext.addLog({value: '**On failed save**'});
applyChildren(); applyChildren();
} }
break; break;
case 'successfulSave': case 'successfulSave':
if (scope['$saveSucceeded']?.value){ if (scope['$saveSucceeded']?.value){
if (!targets.length) actionContext.addLog({value: '**On save**',}); if (!targets.length && !prop.silent) actionContext.addLog({value: '**On save**',});
applyChildren(); applyChildren();
} }
break; break;

View File

@@ -13,6 +13,7 @@ import logErrors from './shared/logErrors.js';
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js'; import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js'; import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
export default function applyBuff(node, actionContext) { export default function applyBuff(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext); applyNodeTriggers(node, 'before', actionContext);
@@ -32,7 +33,9 @@ export default function applyBuff(node, actionContext){
}); });
} }
addChildrenToPropList(node.children); addChildrenToPropList(node.children);
if (!prop.skipCrystalization) {
crystalizeVariables({ propList, actionContext }); crystalizeVariables({ propList, actionContext });
}
let oldParent = { let oldParent = {
id: prop.parent.id, id: prop.parent.id,
@@ -43,7 +46,7 @@ export default function applyBuff(node, actionContext){
copyNodeListToTarget(propList, target, oldParent); copyNodeListToTarget(propList, target, oldParent);
//Log the buff //Log the buff
if (prop.name || prop.description?.value){ if ((prop.name || prop.description?.value) && !prop.silent) {
if (target._id === actionContext.creature._id) { if (target._id === actionContext.creature._id) {
// Targeting self // Targeting self
actionContext.addLog({ actionContext.addLog({
@@ -96,6 +99,7 @@ function crystalizeVariables({propList, actionContext}){
delete prop._skipCrystalize; delete prop._skipCrystalize;
return; return;
} }
// Iterate through all the calculations and crystalize them
computedSchemas[prop.type].computedFields().forEach(calcKey => { computedSchemas[prop.type].computedFields().forEach(calcKey => {
applyFnToKey(prop, calcKey, (prop, key) => { applyFnToKey(prop, calcKey, (prop, key) => {
const calcObj = get(prop, key); const calcObj = get(prop, key);
@@ -132,5 +136,36 @@ function crystalizeVariables({propList, actionContext}){
calcObj.hash = cyrb53(calcObj.calculation); calcObj.hash = cyrb53(calcObj.calculation);
}); });
}); });
// For each key in the schema
computedSchemas[prop.type].inlineCalculationFields().forEach(calcKey => {
// That ends in .inlineCalculations
applyFnToKey(prop, calcKey, (prop, key) => {
const inlineCalcObj = get(prop, key);
if (!inlineCalcObj) return;
// If there is no text, skip
if (!inlineCalcObj.text) {
return;
}
// Replace all the existing calculations
let index = -1;
inlineCalcObj.text = inlineCalcObj.text.replace(INLINE_CALCULATION_REGEX, () => {
index += 1;
return `{${inlineCalcObj.inlineCalculations[index].calculation}}`;
});
// Set the value to the uncomputed string
inlineCalcObj.value = inlineCalcObj.text;
// Write a new hash
const inlineCalcHash = cyrb53(inlineCalcObj.text);
if (inlineCalcHash === inlineCalcObj.hash) {
// Skip if nothing changed
return;
}
inlineCalcObj.hash = inlineCalcHash;
});
});
}); });
} }

View File

@@ -13,7 +13,7 @@ export default function applyBuffRemover(node, actionContext) {
const prop = node.node; const prop = node.node;
// Log Name // Log Name
if (prop.name){ if (prop.name && !prop.silent){
actionContext.addLog({ name: prop.name }); actionContext.addLog({ name: prop.name });
} }
@@ -29,7 +29,7 @@ export default function applyBuffRemover(node, actionContext) {
}); });
return; return;
} }
removeBuff(nearestBuff, actionContext); removeBuff(nearestBuff, actionContext, prop);
} else { } else {
// Get all the buffs targeted by tags // Get all the buffs targeted by tags
const allBuffs = getPropertiesOfType(actionContext.creature._id, 'buff'); const allBuffs = getPropertiesOfType(actionContext.creature._id, 'buff');
@@ -41,7 +41,7 @@ export default function applyBuffRemover(node, actionContext) {
if (prop.removeAll) { if (prop.removeAll) {
// Remove all matching buffs // Remove all matching buffs
targetedBuffs.forEach(buff => { targetedBuffs.forEach(buff => {
removeBuff(buff, actionContext); removeBuff(buff, actionContext, prop);
}); });
} else { } else {
// Sort in reverse order // Sort in reverse order
@@ -49,7 +49,7 @@ export default function applyBuffRemover(node, actionContext) {
// Remove the one with the highest order // Remove the one with the highest order
const buff = targetedBuffs[0]; const buff = targetedBuffs[0];
if (buff) { if (buff) {
removeBuff(buff, actionContext); removeBuff(buff, actionContext, prop);
} }
} }
} }
@@ -60,8 +60,8 @@ export default function applyBuffRemover(node, actionContext) {
node.children.forEach(child => applyProperty(child, actionContext)); node.children.forEach(child => applyProperty(child, actionContext));
} }
function removeBuff(buff, actionContext) { function removeBuff(buff, actionContext, prop) {
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Removed', name: 'Removed',
value: `${buff.name || 'Buff'}` value: `${buff.name || 'Buff'}`
}); });

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';
@@ -128,7 +128,7 @@ export default function applyDamage(node, actionContext){
// There are no targets, just log the result // There are no targets, just log the result
logValue.push(`**${damage}** ${suffix}`); logValue.push(`**${damage}** ${suffix}`);
} }
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: logName, name: logName,
value: logValue.join('\n'), value: logValue.join('\n'),
inline: true, inline: true,
@@ -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;
@@ -219,6 +222,16 @@ function dealDamage({target, damageType, amount, actionContext}){
if (damageType === 'healing') damageLeft = -totalDamage; if (damageType === 'healing') damageLeft = -totalDamage;
healthBars.forEach(healthBar => { healthBars.forEach(healthBar => {
if (damageLeft === 0) return; if (damageLeft === 0) return;
// Replace the healthbar by the one in the action context if we can
// The damagePropertyWork function bashes the prop with the damage
// So we can use the new value in later action properties
if (healthBar.variableName) {
const targetHealthBar = target.variables[healthBar.variableName];
if (targetHealthBar?._id === healthBar._id) {
healthBar = targetHealthBar;
}
}
// Do the damage
let damageAdded = damagePropertyWork({ let damageAdded = damagePropertyWork({
prop: healthBar, prop: healthBar,
operation: 'increment', operation: 'increment',
@@ -226,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

@@ -20,7 +20,7 @@ export default function applySavingThrow(node, actionContext){
}); });
return node.children.forEach(child => applyProperty(child, actionContext)); return node.children.forEach(child => applyProperty(child, actionContext));
} }
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: prop.name, name: prop.name,
value: `DC **${dc}**`, value: `DC **${dc}**`,
inline: true, inline: true,
@@ -94,7 +94,7 @@ export default function applySavingThrow(node, actionContext){
} else { } else {
scope['$saveFailed'] = {value: true}; scope['$saveFailed'] = {value: true};
} }
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: saveSuccess ? 'Successful save' : 'Failed save', name: saveSuccess ? 'Successful save' : 'Failed save',
value: resultPrefix + '\n**' + result + '**', value: resultPrefix + '\n**' + result + '**',
inline: true, inline: true,

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

@@ -65,7 +65,7 @@ export function applyTrigger(trigger, prop, actionContext) {
recalculateInlineCalculations(trigger.description, actionContext); recalculateInlineCalculations(trigger.description, actionContext);
content.value = trigger.description.value; content.value = trigger.description.value;
} }
actionContext.addLog(content); if(!trigger.silent) actionContext.addLog(content);
// Get all the trigger's properties and apply them // Get all the trigger's properties and apply them
const properties = getPropertyDecendants(actionContext.creature._id, trigger._id); const properties = getPropertyDecendants(actionContext.creature._id, trigger._id);

View File

@@ -9,7 +9,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { doActionWork } from '/imports/api/engine/actions/doAction.js'; import { doActionWork } from '/imports/api/engine/actions/doAction.js';
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js'; import ActionContext from '/imports/api/engine/actions/ActionContext.js';
const doAction = new ValidatedMethod({ const doAction = new ValidatedMethod({
@@ -21,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: [],
@@ -42,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;
@@ -65,9 +68,8 @@ const doAction = new ValidatedMethod({
let slotLevel = spell.level || 0; let slotLevel = spell.level || 0;
let slot; let slot;
actionContext.scope['slotLevel'] = slotLevel; // If a spell requires a slot, make sure a slot is spent
if (spell.level && !spell.castWithoutSpellSlots && !(ritual && spell.ritual)) {
if (slotId && !spell.castWithoutSpellSlots){
slot = CreatureProperties.findOne(slotId); slot = CreatureProperties.findOne(slotId);
if (!slot) { if (!slot) {
throw new Meteor.Error('No slot', throw new Meteor.Error('No slot',
@@ -104,10 +106,18 @@ 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;
// Do the action // Do the action
doActionWork({ doActionWork({

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

@@ -14,6 +14,7 @@ const linkDependenciesByType = {
effect: linkEffects, effect: linkEffects,
proficiency: linkProficiencies, proficiency: linkProficiencies,
roll: linkRoll, roll: linkRoll,
pointBuy: linkPointBuy,
propertySlot: linkSlot, propertySlot: linkSlot,
skill: linkSkill, skill: linkSkill,
spell: linkAction, spell: linkAction,
@@ -242,6 +243,28 @@ function linkDamageMultiplier(dependencyGraph, prop) {
}); });
} }
function linkPointBuy(dependencyGraph, prop){
dependOnCalc({ dependencyGraph, prop, key: 'min' });
dependOnCalc({ dependencyGraph, prop, key: 'max' });
dependOnCalc({ dependencyGraph, prop, key: 'cost' });
dependOnCalc({ dependencyGraph, prop, key: 'total' });
prop.values?.forEach(row => {
// Wrap the document in a new object so we don't bash it unintentionally
const pointBuyRow = {
...row,
type: 'pointBuyRow',
tableName: prop.name,
tableId: prop._id,
}
dependencyGraph.addNode(row._id, pointBuyRow);
linkVariableName(dependencyGraph, pointBuyRow);
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.min' });
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.max' });
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.cost' });
});
if (prop.inactive) return;
}
function linkProficiencies(dependencyGraph, prop){ function linkProficiencies(dependencyGraph, prop){
// The stats depend on the proficiency // The stats depend on the proficiency
if (prop.inactive) return; if (prop.inactive) return;

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

@@ -2,6 +2,7 @@ import _variable from './computeByType/computeVariable.js';
import action from './computeByType/computeAction.js'; import action from './computeByType/computeAction.js';
import attribute from './computeByType/computeAttribute.js'; import attribute from './computeByType/computeAttribute.js';
import skill from './computeByType/computeSkill.js'; import skill from './computeByType/computeSkill.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 _calculation from './computeByType/computeCalculation.js'; import _calculation from './computeByType/computeCalculation.js';
@@ -13,6 +14,7 @@ export default Object.freeze({
attribute, attribute,
container, container,
skill, skill,
pointBuy,
propertySlot, propertySlot,
spell: action, spell: action,
}); });

View File

@@ -0,0 +1,53 @@
import { has } from 'lodash';
import evaluateCalculation from '../../utility/evaluateCalculation.js';
export default function computePointBuy(computation, node) {
const prop = node.data;
const tableMin = prop.min?.value || null;
const tableMax = prop.max?.value || null;
prop.spent = 0;
prop.values?.forEach(row => {
// Clean up added properties
// delete row.tableId;
// delete row.tableName;
// delete row.type;
row.spent = 0;
if (row.value === undefined) return;
const min = has(row, 'min.value') ? row.min.value : tableMin;
const max = has(row, 'max.value') ? row.max.value : tableMax;
const costFunction = EJSON.clone(row.cost || prop.cost);
if (costFunction) costFunction.parseLevel = 'reduce';
// Check min and max
if (min !== null && row.value < min) {
row.value = min;
}
if (max !== null && row.value > max) {
row.value = max;
}
// Evaluate the cost function
if (!costFunction) return;
evaluateCalculation(costFunction, { ...computation.scope, value: row.value });
// Write calculation errors
costFunction.errors?.forEach(error => {
if (error?.message) {
row.errors = row.errors || [];
error.message = 'Cost calculation error.\n' + error.message;
row.errors.push(error);
}
});
if (Number.isFinite(costFunction.value)) {
row.spent = costFunction.value;
prop.spent += costFunction.value;
}
});
prop.pointsLeft = (prop.total?.value || 0) - (prop.spent || 0);
if (prop.spent > prop.total?.value) {
prop.errors = prop.errors || [];
prop.errors.push({
type: 'pointBuyError',
message: 'Spent more than total points available',
});
}
}

View File

@@ -4,7 +4,7 @@
// by computeVariableAsSkill // by computeVariableAsSkill
export default function computeSkill(computation, node){ export default function computeSkill(computation, node){
const prop = node.data; const prop = node.data;
prop.proficiency = prop.baseProficiency; prop.proficiency = prop.baseProficiency || 0;
let profBonus = computation.scope['proficiencyBonus']?.value || 0; let profBonus = computation.scope['proficiencyBonus']?.value || 0;
// Multiply the proficiency bonus by the actual proficiency // Multiply the proficiency bonus by the actual proficiency
if(prop.proficiency === 0.49){ if(prop.proficiency === 0.49){

View File

@@ -8,7 +8,13 @@ export default function aggregateDefinition({node, linkedNode, link}){
// get current defining prop // get current defining prop
const definingProp = node.data.definingProp; const definingProp = node.data.definingProp;
// Find the last defining prop // Find the last defining prop
if (!definingProp || prop.order > definingProp.order){ if (
!definingProp ||
prop.type !== 'pointBuyRow' && (
definingProp.type === 'pointBuyRow' ||
prop.order > definingProp.order
)
) {
// override the current defining prop // override the current defining prop
overrideProp(definingProp, node); overrideProp(definingProp, node);
// set this prop as the new defining prop // set this prop as the new defining prop
@@ -18,9 +24,32 @@ export default function aggregateDefinition({node, linkedNode, link}){
} }
// Aggregate the base value due to the defining properties // Aggregate the base value due to the defining properties
const propBaseValue = prop.baseValue?.value; let propBaseValue = prop.baseValue?.value;
// Point buy rows use prop.value instead of prop.baseValue
if (prop.type === 'pointBuyRow') {
propBaseValue = prop.value;
}
if (propBaseValue === undefined) return; if (propBaseValue === undefined) return;
// Store a summary of the definition as a base value effect
node.data.effects = node.data.effects || [];
if (prop.type === 'pointBuyRow') {
node.data.effects.push({
_id: prop.tableId,
name: prop.tableName,
operation: 'base',
amount: { value: propBaseValue },
type: 'pointBuy',
});
} else {
node.data.effects.push({
_id: prop._id,
name: prop.name,
operation: 'base',
amount: { value: propBaseValue },
type: prop.type,
});
}
if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue){ if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue){
node.data.baseValue = propBaseValue; node.data.baseValue = propBaseValue;
} }

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,11 +21,23 @@ 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,
// ancestors: linkedNode.data.ancestors, // ancestors: linkedNode.data.ancestors,
}); });
@@ -32,7 +46,7 @@ export default function aggregateEffect({node, linkedNode, link}){
// 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 // Skip aggregating if the result is not resolved completely
if (typeof result === 'string') return; if (typeof result === 'string' || result === undefined) 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

@@ -33,6 +33,9 @@ export default function computeVariableAsSkill(computation, node, prop){
const aggregator = node.data.effectAggregator; const aggregator = node.data.effectAggregator;
const aggregatorBase = aggregator?.base || 0; const aggregatorBase = aggregator?.base || 0;
// Store effects
prop.effects = node.data.effects;
// If there is no aggregator, determine if the prop can hide, then exit // If there is no aggregator, determine if the prop can hide, then exit
if (!aggregator){ if (!aggregator){
prop.hide = statBase === undefined && prop.hide = statBase === undefined &&
@@ -71,8 +74,6 @@ export default function computeVariableAsSkill(computation, node, prop){
prop.fail = aggregator.fail; prop.fail = aggregator.fail;
// Rollbonus // Rollbonus
prop.rollBonuses = aggregator.rollBonus; prop.rollBonuses = aggregator.rollBonus;
// Store effects
prop.effects = node.data.effects;
} }
function aggregateAbilityEffects({computation, skillNode, abilityNode}){ function aggregateAbilityEffects({computation, skillNode, abilityNode}){

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

@@ -9,8 +9,10 @@ export default function getEffectivePropTags(prop) {
} }
// Tags for some string properties // Tags for some string properties
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

@@ -97,7 +97,10 @@ export function getCreature(creatureId) {
if (loadedCreatures.has(creatureId)) { if (loadedCreatures.has(creatureId)) {
const loadedCreature = loadedCreatures.get(creatureId); const loadedCreature = loadedCreatures.get(creatureId);
const creature = loadedCreature.creature; const creature = loadedCreature.creature;
if (creature) return creature; if (creature) {
const cloneCreature = EJSON.clone(creature);
return cloneCreature;
}
} }
// console.time(`Cache miss on Creature: ${creatureId}`); // console.time(`Cache miss on Creature: ${creatureId}`);
const creature = Creatures.findOne(creatureId); const creature = Creatures.findOne(creatureId);
@@ -109,7 +112,10 @@ export function getVariables(creatureId) {
if (loadedCreatures.has(creatureId)) { if (loadedCreatures.has(creatureId)) {
const loadedCreature = loadedCreatures.get(creatureId); const loadedCreature = loadedCreatures.get(creatureId);
const variables = loadedCreature.variables; const variables = loadedCreature.variables;
if (variables) return variables; if (variables) {
const cloneVarables = EJSON.clone(variables);
return cloneVarables;
}
} }
// console.time(`Cache miss on variables: ${creatureId}`); // console.time(`Cache miss on variables: ${creatureId}`);
const variables = CreatureVariables.findOne({_creatureId: creatureId}); const variables = CreatureVariables.findOne({_creatureId: creatureId});

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

@@ -114,6 +114,11 @@ let ActionSchema = createPropertySchema({
type: 'fieldToCompute', type: 'fieldToCompute',
optional: true, optional: true,
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
const ComputedOnlyActionSchema = createPropertySchema({ const ComputedOnlyActionSchema = createPropertySchema({

View File

@@ -31,6 +31,11 @@ const AdjustmentSchema = createPropertySchema({
allowedValues: ['set', 'increment'], allowedValues: ['set', 'increment'],
defaultValue: 'increment', defaultValue: 'increment',
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
const ComputedOnlyAdjustmentSchema = createPropertySchema({ const ComputedOnlyAdjustmentSchema = createPropertySchema({

View File

@@ -69,6 +69,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,6 +117,14 @@ 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,
@@ -176,6 +194,7 @@ let ComputedOnlyAttributeSchema = createPropertySchema({
effects: { effects: {
type: Array, type: Array,
optional: true, optional: true,
removeBeforeCompute: true,
}, },
'effects.$': { 'effects.$': {
type: Object, type: Object,

View File

@@ -37,6 +37,11 @@ let BranchSchema = createPropertySchema({
optional: true, optional: true,
parseLevel: 'compile', parseLevel: 'compile',
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
let ComputedOnlyBranchSchema = createPropertySchema({ let ComputedOnlyBranchSchema = createPropertySchema({

View File

@@ -68,6 +68,11 @@ let BuffRemoverSchema = createPropertySchema({
type: String, type: String,
max: STORAGE_LIMITS.tagLength, max: STORAGE_LIMITS.tagLength,
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
let ComputedOnlyBuffRemoverSchema = createPropertySchema({}); let ComputedOnlyBuffRemoverSchema = createPropertySchema({});

View File

@@ -29,6 +29,16 @@ let BuffSchema = createPropertySchema({
], ],
defaultValue: 'target', defaultValue: 'target',
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
// Prevent the children from being crystalized
skipCrystalization: {
type: Boolean,
optional: true,
},
}); });
let ComputedOnlyBuffSchema = createPropertySchema({ let ComputedOnlyBuffSchema = createPropertySchema({

View File

@@ -27,6 +27,11 @@ const DamageSchema = createPropertySchema({
defaultValue: 'slashing', defaultValue: 'slashing',
regEx: VARIABLE_NAME_REGEX, regEx: VARIABLE_NAME_REGEX,
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
const ComputedOnlyDamageSchema = createPropertySchema({ const ComputedOnlyDamageSchema = createPropertySchema({

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
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'; import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
/* /*
* PointBuys are reason-value attached to skills and abilities * PointBuys are reason-value attached to skills and abilities
@@ -13,13 +14,6 @@ let PointBuySchema = createPropertySchema({
optional: true, optional: true,
max: STORAGE_LIMITS.name, max: STORAGE_LIMITS.name,
}, },
variableName: {
type: String,
optional: true,
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
},
ignored: { ignored: {
type: Boolean, type: Boolean,
optional: true, optional: true,
@@ -27,10 +21,18 @@ let PointBuySchema = createPropertySchema({
'values': { 'values': {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.pointBuyRowsCount,
}, },
'values.$': { 'values.$': {
type: Object, type: Object,
}, },
'values.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
if (!this.isSet) return Random.id();
}
},
'values.$.name': { 'values.$.name': {
type: String, type: String,
optional: true, optional: true,
@@ -47,6 +49,18 @@ let PointBuySchema = createPropertySchema({
type: Number, type: Number,
optional: true, optional: true,
}, },
'values.$.min': {
type: 'fieldToCompute',
optional: true,
},
'values.$.max': {
type: 'fieldToCompute',
optional: true,
},
'values.$.cost': {
type: 'fieldToCompute',
optional: true,
},
min: { min: {
type: 'fieldToCompute', type: 'fieldToCompute',
optional: true, optional: true,
@@ -62,6 +76,7 @@ let PointBuySchema = createPropertySchema({
cost: { cost: {
type: 'fieldToCompute', type: 'fieldToCompute',
optional: true, optional: true,
parseLevel: 'compile',
}, },
}); });
@@ -74,11 +89,46 @@ const ComputedOnlyPointBuySchema = createPropertySchema({
type: 'computedOnlyField', type: 'computedOnlyField',
optional: true, optional: true,
}, },
total: { cost: {
type: 'computedOnlyField',
optional: true,
parseLevel: 'compile',
},
'values': {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.pointBuyRowsCount,
},
'values.$': {
type: Object,
},
'values.$.min': {
type: 'computedOnlyField', type: 'computedOnlyField',
optional: true, optional: true,
}, },
cost: { 'values.$.max': {
type: 'computedOnlyField',
optional: true,
},
'values.$.cost': {
type: 'computedOnlyField',
optional: true,
parseLevel: 'compile',
},
'values.$.spent': {
type: Number,
optional: true,
removeBeforeCompute: true,
},
'values.$.errors': {
type: Array,
optional: true,
removeBeforeCompute: true,
},
'values.$.errors.$': {
type: ErrorSchema,
},
total: {
type: 'computedOnlyField', type: 'computedOnlyField',
optional: true, optional: true,
}, },
@@ -87,6 +137,19 @@ const ComputedOnlyPointBuySchema = createPropertySchema({
optional: true, optional: true,
removeBeforeCompute: true, removeBeforeCompute: true,
}, },
pointsLeft: {
type: Number,
optional: true,
removeBeforeCompute: true,
},
errors: {
type: Array,
optional: true,
removeBeforeCompute: true,
},
'errors.$': {
type: ErrorSchema,
},
}); });
const ComputedPointBuySchema = new SimpleSchema() const ComputedPointBuySchema = new SimpleSchema()

View File

@@ -30,6 +30,11 @@ let SavingThrowSchema = createPropertySchema({
optional: true, optional: true,
max: STORAGE_LIMITS.variableName, max: STORAGE_LIMITS.variableName,
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
const ComputedOnlySavingThrowSchema = createPropertySchema({ const ComputedOnlySavingThrowSchema = createPropertySchema({

View File

@@ -135,6 +135,7 @@ let ComputedOnlySkillSchema = createPropertySchema({
effects: { effects: {
type: Array, type: Array,
optional: true, optional: true,
removeBeforeCompute: true,
}, },
'effects.$': { 'effects.$': {
type: Object, type: Object,

View File

@@ -109,6 +109,11 @@ let TriggerSchema = createPropertySchema({
type: String, type: String,
max: STORAGE_LIMITS.tagLength, max: STORAGE_LIMITS.tagLength,
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
const ComputedOnlyTriggerSchema = createPropertySchema({ const ComputedOnlyTriggerSchema = createPropertySchema({

View File

@@ -16,6 +16,7 @@ import { ComputedOnlyFeatureSchema } from '/imports/api/properties/Features.js';
import { ComputedOnlyFolderSchema } from '/imports/api/properties/Folders.js'; import { ComputedOnlyFolderSchema } from '/imports/api/properties/Folders.js';
import { ComputedOnlyItemSchema } from '/imports/api/properties/Items.js'; import { ComputedOnlyItemSchema } from '/imports/api/properties/Items.js';
import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes.js'; import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes.js';
import { ComputedOnlyPointBuySchema } from '/imports/api/properties/PointBuys.js';
import { ComputedOnlyProficiencySchema } from '/imports/api/properties/Proficiencies.js'; import { ComputedOnlyProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { ComputedOnlyReferenceSchema } from '/imports/api/properties/References.js'; import { ComputedOnlyReferenceSchema } from '/imports/api/properties/References.js';
import { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls.js'; import { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls.js';
@@ -46,6 +47,7 @@ const propertySchemasIndex = {
folder: ComputedOnlyFolderSchema, folder: ComputedOnlyFolderSchema,
item: ComputedOnlyItemSchema, item: ComputedOnlyItemSchema,
note: ComputedOnlyNoteSchema, note: ComputedOnlyNoteSchema,
pointBuy: ComputedOnlyPointBuySchema,
proficiency: ComputedOnlyProficiencySchema, proficiency: ComputedOnlyProficiencySchema,
propertySlot: ComputedOnlySlotSchema, propertySlot: ComputedOnlySlotSchema,
reference: ComputedOnlyReferenceSchema, reference: ComputedOnlyReferenceSchema,

View File

@@ -16,6 +16,7 @@ import { ComputedFeatureSchema } from '/imports/api/properties/Features.js';
import { FolderSchema } from '/imports/api/properties/Folders.js'; import { FolderSchema } from '/imports/api/properties/Folders.js';
import { ComputedItemSchema } from '/imports/api/properties/Items.js'; import { ComputedItemSchema } from '/imports/api/properties/Items.js';
import { ComputedNoteSchema } from '/imports/api/properties/Notes.js'; import { ComputedNoteSchema } from '/imports/api/properties/Notes.js';
import { ComputedPointBuySchema } from '/imports/api/properties/PointBuys.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js'; import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { ReferenceSchema } from '/imports/api/properties/References.js'; import { ReferenceSchema } from '/imports/api/properties/References.js';
import { ComputedRollSchema } from '/imports/api/properties/Rolls.js'; import { ComputedRollSchema } from '/imports/api/properties/Rolls.js';
@@ -44,6 +45,7 @@ const propertySchemasIndex = {
feature: ComputedFeatureSchema, feature: ComputedFeatureSchema,
folder: FolderSchema, folder: FolderSchema,
note: ComputedNoteSchema, note: ComputedNoteSchema,
pointBuy: ComputedPointBuySchema,
proficiency: ProficiencySchema, proficiency: ProficiencySchema,
propertySlot: ComputedSlotSchema, propertySlot: ComputedSlotSchema,
reference: ReferenceSchema, reference: ReferenceSchema,

View File

@@ -14,6 +14,7 @@ import { EffectSchema } from '/imports/api/properties/Effects.js';
import { FeatureSchema } from '/imports/api/properties/Features.js'; import { FeatureSchema } from '/imports/api/properties/Features.js';
import { FolderSchema } from '/imports/api/properties/Folders.js'; import { FolderSchema } from '/imports/api/properties/Folders.js';
import { NoteSchema } from '/imports/api/properties/Notes.js'; import { NoteSchema } from '/imports/api/properties/Notes.js';
import { PointBuySchema } from '/imports/api/properties/PointBuys.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js'; import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { ReferenceSchema } from '/imports/api/properties/References.js'; import { ReferenceSchema } from '/imports/api/properties/References.js';
import { RollSchema } from '/imports/api/properties/Rolls.js'; import { RollSchema } from '/imports/api/properties/Rolls.js';
@@ -44,6 +45,7 @@ const propertySchemasIndex = {
feature: FeatureSchema, feature: FeatureSchema,
folder: FolderSchema, folder: FolderSchema,
note: NoteSchema, note: NoteSchema,
pointBuy: PointBuySchema,
proficiency: ProficiencySchema, proficiency: ProficiencySchema,
propertySlot: SlotSchema, propertySlot: SlotSchema,
reference: ReferenceSchema, reference: ReferenceSchema,

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

@@ -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,36 +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: ['folder'], suggestedParents: ['note', 'folder'],
},
pointBuy: {
icon: 'mdi-table',
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',
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'],
}, },
@@ -126,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: [],
}, },
@@ -182,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

@@ -32,6 +32,7 @@ const STORAGE_LIMITS = Object.freeze({
tagCount: 64, tagCount: 64,
writersCount: 20, writersCount: 20,
libraryCollectionCount: 32, libraryCollectionCount: 32,
pointBuyRowsCount: 32,
}); });
export default STORAGE_LIMITS; export default STORAGE_LIMITS;

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

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import computeCreature from '/imports/api/engine/computeCreature.js'; import computeCreature from '/imports/api/engine/computeCreature.js';
import VERSION from '/imports/constants/VERSION.js'; import VERSION from '/imports/constants/VERSION.js';
@@ -40,6 +41,9 @@ Meteor.publish('api-creature', function(creatureId){
CreatureProperties.find({ CreatureProperties.find({
'ancestors.id': creatureId, 'ancestors.id': creatureId,
}), }),
CreatureVariables.find({
_creatureId: creatureId,
}),
]; ];
}, { }, {
url: 'api/creature/:0' url: 'api/creature/:0'

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

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

@@ -0,0 +1,72 @@
<template lang="html">
<v-btn
v-bind="$attrs"
:disabled="isDisabled"
:loading="loading"
@click="click"
>
<slot />
</v-btn>
</template>
<script lang="js">
import { debounce } from 'lodash';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default {
inject: {
context: { default: {} }
},
props: {
disabled: Boolean,
debounce: {
type: Number,
default: undefined,
},
},
data() {
return {
loading: false,
timesClicked: 0,
};
},
computed: {
isDisabled(){
return this.context.editPermission === false || this.disabled;
},
debounceTime() {
if (Number.isFinite(this.debounce)){
return this.debounce;
} else if (Number.isFinite(this.context.debounceTime)){
return this.context.debounceTime;
} else {
return 750;
}
},
},
created(){
this.debounceClicks = debounce(this.clicks, this.debounceTime);
},
beforeDestroy(){
this.debounceClicks.flush();
},
methods: {
click() {
this.timesClicked += 1;
this.debounceClicks();
this.$emit('click', this.acknowledgeChange);
},
clicks() {
this.$emit('clicks', this.timesClicked, this.acknowledgeChange);
this.loading = true;
this.timesClicked = 0;
},
acknowledgeChange(error){
this.loading = false;
if (error) {
snackbar({ text: error.reason || error.message || error.toString() });
}
},
},
};
</script>

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],
@@ -115,7 +117,7 @@ export default {
}, },
change(val) { change(val) {
this.dirty = true; this.dirty = true;
if (this.hasChangeListener) this.loading = true; if (this.hasChangeListener()) this.loading = true;
this.$emit('change', val, this.acknowledgeChange); this.$emit('change', val, this.acknowledgeChange);
}, },
hasChangeListener() { hasChangeListener() {

View File

@@ -0,0 +1,34 @@
<template lang="html">
<v-slider
ref="input"
v-bind="$attrs"
class="dc-text-field"
:hide-details="!(errors && errors.length)"
:loading="loading"
:error-messages="errors"
:value="safeValue"
:disabled="isDisabled"
:outlined="!regular"
@change="change"
@focus="focused = true"
@blur="focused = false"
>
<template #prepend>
<slot name="prepend" />
</template>
<template #append>
<slot name="append" />
</template>
</v-slider>
</template>
<script lang="js">
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
export default {
mixins: [SmartInput],
props: {
regular: Boolean,
},
};
</script>

View File

@@ -5,17 +5,21 @@ import IconPicker from '/imports/ui/components/global/IconPicker.vue';
import TextField from '/imports/ui/components/global/TextField.vue'; import TextField from '/imports/ui/components/global/TextField.vue';
import TextArea from '/imports/ui/components/global/TextArea.vue'; import TextArea from '/imports/ui/components/global/TextArea.vue';
import SmartSelect from '/imports/ui/components/global/SmartSelect.vue'; import SmartSelect from '/imports/ui/components/global/SmartSelect.vue';
import SmartBtn from '/imports/ui/components/global/SmartBtn.vue';
import SmartCombobox from '/imports/ui/components/global/SmartCombobox.vue'; import SmartCombobox from '/imports/ui/components/global/SmartCombobox.vue';
import SmartCheckbox from '/imports/ui/components/global/SmartCheckbox.vue'; import SmartCheckbox from '/imports/ui/components/global/SmartCheckbox.vue';
import SmartSwitch from '/imports/ui/components/global/SmartSwitch.vue'; import SmartSwitch from '/imports/ui/components/global/SmartSwitch.vue';
import SvgIcon from '/imports/ui/components/global/SvgIcon.vue'; import SvgIcon from '/imports/ui/components/global/SvgIcon.vue';
import SmartSlider from '/imports/ui/components/global/SmartSlider.vue';
Vue.component('DatePicker', DatePicker); Vue.component('DatePicker', DatePicker);
Vue.component('IconPicker', IconPicker); Vue.component('IconPicker', IconPicker);
Vue.component('TextField', TextField); Vue.component('TextField', TextField);
Vue.component('TextArea', TextArea); Vue.component('TextArea', TextArea);
Vue.component('SmartSelect', SmartSelect); Vue.component('SmartSelect', SmartSelect);
Vue.component('SmartBtn', SmartBtn);
Vue.component('SmartCombobox', SmartCombobox); Vue.component('SmartCombobox', SmartCombobox);
Vue.component('SmartCheckbox', SmartCheckbox); Vue.component('SmartCheckbox', SmartCheckbox);
Vue.component('SmartSlider', SmartSlider);
Vue.component('SmartSwitch', SmartSwitch); Vue.component('SmartSwitch', SmartSwitch);
Vue.component('SvgIcon', SvgIcon); Vue.component('SvgIcon', SvgIcon);

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

@@ -149,7 +149,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 +267,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

@@ -55,6 +55,7 @@
/> />
<v-spacer /> <v-spacer />
<v-btn <v-btn
v-if="node.parent.id === parentSlotId"
icon icon
:disabled="context.editPermission === false" :disabled="context.editPermission === false"
@click.stop="remove(node)" @click.stop="remove(node)"
@@ -91,6 +92,7 @@
<build-tree-node-list <build-tree-node-list
v-if="showExpanded" v-if="showExpanded"
:children="computedChildren" :children="computedChildren"
:parent-slot-id="computedSlotId"
@selected="e => $emit('selected', e)" @selected="e => $emit('selected', e)"
/> />
<div v-else> <div v-else>
@@ -147,6 +149,10 @@ export default {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
parentSlotId: {
type: String,
default: undefined,
},
}, },
data(){return { data(){return {
expanded: false, expanded: false,
@@ -197,6 +203,21 @@ export default {
} }
return this.children; return this.children;
}, },
computedSlotId() {
if (this.condenseChild) {
if (this.children[0].node.type === 'propertySlot') {
return this.children[0].node._id;
} else {
return undefined;
}
} else {
if (this.node.type === 'propertySlot') {
return this.node._id;
} else {
return undefined;
}
}
},
canExpand(){ canExpand(){
return !!this.computedChildren.length || this.canFillWithMany; return !!this.computedChildren.length || this.canFillWithMany;
}, },

View File

@@ -5,6 +5,7 @@
:key="child.node._id" :key="child.node._id"
:node="child.node" :node="child.node"
:children="child.children" :children="child.children"
:parent-slot-id="parentSlotId"
@selected="e => $emit('selected', e)" @selected="e => $emit('selected', e)"
/> />
</div> </div>
@@ -22,9 +23,15 @@
type: Array, type: Array,
default: () => [], default: () => [],
}, },
parentSlotId: {
type: String,
default: undefined,
}, },
data(){ return { },
data() {
return {
expanded: false, expanded: false,
}}, }
},
}; };
</script> </script>

View File

@@ -98,7 +98,7 @@
Next Next
</v-btn> </v-btn>
<v-btn <v-btn
:disabled="biographyAlert" :disabled="!!biographyAlert"
:text="step < 2" :text="step < 2"
:color="step < 2? '' : 'accent'" :color="step < 2? '' : 'accent'"
@click="submit" @click="submit"

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

@@ -25,6 +25,7 @@
<v-menu <v-menu
bottom bottom
left left
transition="slide-y-transition"
> >
<template #activator="{ on }"> <template #activator="{ on }">
<v-badge <v-badge
@@ -48,12 +49,21 @@
<v-icon class="mr-2"> <v-icon class="mr-2">
mdi-file-hidden mdi-file-hidden
</v-icon> </v-icon>
{{ hiddenCount }} hidden {{ hiddenCount > 1 ? 'slots' : 'slot' }} {{ hiddenCount }} hidden {{ hiddenCount > 1 ? 'properties' : 'property' }}
</v-subheader> </v-subheader>
<v-list-item
v-for="pointBuy in hiddenPointBuys"
:key="pointBuy._id"
@click="unhideProp(pointBuy._id)"
>
<v-list-item-title>
{{ getPropertyTitle(pointBuy) }}
</v-list-item-title>
</v-list-item>
<v-list-item <v-list-item
v-for="slot in hiddenSlots" v-for="slot in hiddenSlots"
:key="slot._id" :key="slot._id"
@click="unhideSlot(slot._id)" @click="unhideProp(slot._id)"
> >
<v-list-item-title> <v-list-item-title>
{{ getPropertyTitle(slot) }} {{ getPropertyTitle(slot) }}
@@ -226,7 +236,7 @@ export default {
].sort((a, b) => a.order - b.order); ].sort((a, b) => a.order - b.order);
}, },
hiddenCount() { hiddenCount() {
return this.hiddenSlots.length; return this.hiddenSlots.length + this.hiddenPointBuys.length;
}, },
}, },
meteor: { meteor: {
@@ -236,6 +246,16 @@ export default {
variables() { variables() {
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {}; return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
}, },
hiddenPointBuys() {
return CreatureProperties.find({
type: 'pointBuy',
'ancestors.id': this.creatureId,
ignored: true,
pointsLeft: {$ne: 0},
removed: {$ne: true},
inactive: {$ne: true},
}).fetch();
},
hiddenSlots(){ hiddenSlots(){
return CreatureProperties.find({ return CreatureProperties.find({
type: 'propertySlot', type: 'propertySlot',
@@ -284,7 +304,7 @@ export default {
slotBuildTree(){ slotBuildTree(){
const slots = CreatureProperties.find({ const slots = CreatureProperties.find({
'ancestors.id': this.creatureId, 'ancestors.id': this.creatureId,
type: 'propertySlot', type: {$in: ['propertySlot', 'pointBuy']},
$or: [ $or: [
{'slotCondition.value': {$nin: [false, 0, '']}}, {'slotCondition.value': {$nin: [false, 0, '']}},
{'slotCondition.value': {$exists: false}}, {'slotCondition.value': {$exists: false}},
@@ -308,16 +328,15 @@ export default {
]); ]);
traverse(tree, (child, parents) => { traverse(tree, (child, parents) => {
const model = child.node; const model = child.node;
const isSlotWithSpace = model.type === 'propertySlot' && const isSlotWithSpace = model.type === 'propertySlot' && (
model.spaceLeft > 0 || model.spaceLeft > 0 ||
!model.quantityExpected || !model.quantityExpected ||
model.quantityExpected.value === 0; model.quantityExpected.value === 0
);
if(isSlotWithSpace) { if(isSlotWithSpace) {
model._canFill = true; model._canFill = true;
parents.forEach(node => { parents.forEach(node => {
if (node.node.type === 'propertySlot'){
node.node._descendantCanFill = true; node.node._descendantCanFill = true;
}
}); });
} }
}); });
@@ -385,7 +404,7 @@ export default {
}); });
}, },
getPropertyTitle, getPropertyTitle,
unhideSlot(_id) { unhideProp(_id) {
updateCreatureProperty.call({ updateCreatureProperty.call({
_id, _id,
path: ['ignored'], path: ['ignored'],

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,7 +1,5 @@
<template lang="html"> <template lang="html">
<div <div class="stats-tab ma-2">
class="stats-tab ma-2"
>
<health-bar-card-container :creature-id="creatureId" /> <health-bar-card-container :creature-id="creatureId" />
<column-layout> <column-layout>
@@ -171,9 +169,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
@@ -253,18 +249,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"
@@ -381,6 +365,10 @@
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);
}; };
@@ -420,9 +408,11 @@
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 } });
@@ -494,19 +484,6 @@
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;
}); });
}, },
}, },
@@ -542,9 +519,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 +541,5 @@
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
</style> </style>

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

@@ -5,6 +5,18 @@
leave-absolute leave-absolute
hide-on-leave hide-on-leave
> >
<div
v-for="pointBuy in pointBuys"
:key="pointBuy._id"
style="transition: all 0.3s !important"
>
<point-buy-card
:model="pointBuy"
hover
@ignore="ignoreProp(pointBuy._id)"
@click="editPointBuy(pointBuy._id)"
/>
</div>
<div <div
v-for="slot in slots" v-for="slot in slots"
:key="slot._id" :key="slot._id"
@@ -13,7 +25,7 @@
<slot-card <slot-card
:model="slot" :model="slot"
hover hover
@ignore="ignoreSlot(slot._id)" @ignore="ignoreProp(slot._id)"
@click="fillSlot(slot._id)" @click="fillSlot(slot._id)"
/> />
</div> </div>
@@ -24,6 +36,7 @@
<script lang="js"> <script lang="js">
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import SlotCard from '/imports/ui/creature/slots/SlotCard.vue'; import SlotCard from '/imports/ui/creature/slots/SlotCard.vue';
import PointBuyCard from '/imports/ui/properties/components/pointBuy/PointBuyCard.vue';
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue'; import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js'; import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js'; import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
@@ -32,13 +45,14 @@ import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default { export default {
components: { components: {
SlotCard, SlotCard,
PointBuyCard,
ColumnLayout, ColumnLayout,
}, },
inject: { inject: {
context: { default: {} } context: { default: {} }
}, },
methods: { methods: {
ignoreSlot(_id){ ignoreProp(_id){
updateCreatureProperty.call({ updateCreatureProperty.call({
_id, _id,
path: ['ignored'], path: ['ignored'],
@@ -75,6 +89,16 @@ export default {
} }
}); });
}, },
editPointBuy(_id){
this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog',
elementId: `point-buy-card-${_id}`,
data: {
_id,
startInEditTab: true,
},
});
},
}, },
meteor: { meteor: {
slots(){ slots(){
@@ -99,7 +123,16 @@ export default {
removed: {$ne: true}, removed: {$ne: true},
inactive: {$ne: true}, inactive: {$ne: true},
}); });
} },
pointBuys(){
return CreatureProperties.find({
type: 'pointBuy',
'ancestors.id': this.context.creatureId,
ignored: { $ne: true },
removed: {$ne: true},
inactive: {$ne: true},
});
},
} }
} }
</script> </script>

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

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