Compare commits

..

27 Commits

Author SHA1 Message Date
Stefan Zermatten
c55d572134 Bumped version 2022-11-16 23:52:08 +02:00
Stefan Zermatten
0a2b60990e Merge branch 'version-2-dev' into version-2 2022-11-16 23:51:37 +02:00
Stefan Zermatten
a437ff5aef Fixed log of recovering HD not having names 2022-11-09 15:02:41 +02:00
Stefan Zermatten
3d31d62860 Completed folder stat grouping UI 2022-11-09 14:58:52 +02:00
Stefan Zermatten
8377231254 Began work on stat grouping by folder 2022-11-09 00:00:54 +02:00
Stefan Zermatten
1ec29365cb Added custom sheet events
Made rest buttons optional
2022-11-08 23:01:09 +02:00
Stefan Zermatten
60b21c1901 Fixed bugs with effects
they were not providing advantage or conditional benefits
2022-11-08 18:29:27 +02:00
Stefan Zermatten
03f87b0afa Added spellcasting ability to spell lists 2022-11-08 18:09:00 +02:00
Stefan Zermatten
48291d2c8f Added help to property creation forms 2022-11-08 17:17:26 +02:00
Stefan Zermatten
1cedf55fbf Merge branch 'version-2-print' into version-2-dev 2022-11-08 17:00:52 +02:00
Stefan Zermatten
bed4d4b162 Fixed logo not showing 2022-11-08 16:59:52 +02:00
Stefan Zermatten
a1d992ec8d Fixed blank multipliers box 2022-11-07 16:38:54 +02:00
Stefan Zermatten
008ef62517 Printing implemented, needs print button on sheet 2022-11-07 16:18:35 +02:00
Stefan Zermatten
c436309ba8 Work on column-based print layout 2022-11-07 00:07:42 +02:00
Stefan Zermatten
0bfdb73b47 Added a quick exit for migrations if the database is new 2022-11-04 12:34:37 +02:00
Stefan Zermatten
a462cc5ca2 Updated packages 2022-11-04 12:34:22 +02:00
Stefan Zermatten
5d57a74667 Merge branch 'version-2-dev' into version-2 2022-11-03 20:52:26 +02:00
Stefan Zermatten
21b0029df7 bumped version 2022-11-03 20:51:58 +02:00
Stefan Zermatten
c0ccafa787 Added overflow stops to health bars 2022-11-03 20:50:10 +02:00
Stefan Zermatten
d63ad9ea8f Added hide when total/value zero to attributes 2022-11-03 20:39:02 +02:00
Stefan Zermatten
8f56a60fb1 Added copy-to and related sharing permissions 2022-11-03 20:18:59 +02:00
Stefan Zermatten
358ae46627 Began work on copy to for library nodes 2022-11-03 19:08:44 +02:00
Stefan Zermatten
0b1db3c40c Updated meteor 2022-10-18 15:40:41 +02:00
Stefan Zermatten
0ad7e659d2 updated docs to include create a class guide 2022-10-18 15:40:17 +02:00
Stefan Zermatten
58c3875dc7 Hotifix: Casting cantrips without a spell slot 2022-10-12 07:36:42 +02:00
Stefan Zermatten
84f506f1fe Added $checkDiceRoll $checkRoll $checkModifier variables 2022-10-12 07:32:39 +02:00
Stefan Zermatten
d0a3ccc76a bumped version 2022-10-10 16:54:57 +02:00
87 changed files with 4444 additions and 1030 deletions

View File

@@ -11,7 +11,7 @@ accounts-google@1.4.0
email@2.2.1
meteor-base@1.5.1
mobile-experience@1.1.0
mongo@1.16.0-beta280.7
mongo@1.16.0
session@1.2.0
tracker@1.2.0
logging@1.3.1
@@ -48,4 +48,4 @@ simple:rest-bearer-token-parser
simple:rest-json-error-handler
littledata:synced-cron
mdg:meteor-apm-agent
typescript
typescript@4.5.4

View File

@@ -1 +1 @@
METEOR@2.8-beta.7
METEOR@2.8.0

View File

@@ -27,10 +27,10 @@ coffeescript@2.4.1
coffeescript-compiler@2.4.1
dburles:mongo-collection-instances@0.3.6
ddp@1.4.0
ddp-client@2.5.0
ddp-client@2.6.0
ddp-common@1.4.0
ddp-rate-limiter@1.1.0
ddp-server@2.5.0
ddp-server@2.6.0
diff-sequence@1.1.1
dynamic-import@0.7.2
ecmascript@0.16.2
@@ -57,7 +57,7 @@ localstorage@1.2.0
logging@1.3.1
mdg:meteor-apm-agent@3.5.1
mdg:validated-method@1.2.0
meteor@1.10.1-beta280.7
meteor@1.10.1
meteor-base@1.5.1
meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3
@@ -65,18 +65,18 @@ meteortesting:mocha-core@8.1.2
mikowals:batch-insert@1.3.0
minifier-css@1.6.1
minifier-js@2.7.5
minimongo@1.9.0-beta280.7
minimongo@1.9.0
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.8
modules@0.19.0-beta280.7
modules@0.19.0
modules-runtime@0.13.0
mongo@1.16.0-beta280.7
mongo@1.16.0
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
mongo-livedata@1.0.12
npm-mongo@4.9.0-beta280.7
npm-mongo@4.9.0
oauth@2.1.2
oauth2@1.3.1
ordered-dict@1.1.0

View File

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

View File

@@ -49,7 +49,7 @@ const restCreature = new ValidatedMethod({
applyTriggers(afterTriggers, null, actionContext);
// Insert log
actionContext.writeLog();
actionContext.writeLog();
},
});
@@ -57,88 +57,124 @@ function doRestWork(restType, actionContext) {
const creatureId = actionContext.creature._id;
// Long rests reset short rest properties as well
let resetFilter;
if (restType === 'shortRest'){
if (restType === 'shortRest') {
resetFilter = 'shortRest'
} else {
resetFilter = {$in: ['shortRest', 'longRest']}
resetFilter = { $in: ['shortRest', 'longRest'] }
}
resetProperties(creatureId, resetFilter, actionContext);
// Reset half hit dice on a long rest, starting with the highest dice
if (restType === 'longRest') {
resetHitDice(creatureId, actionContext);
}
}
export function resetProperties(creatureId, resetFilter, actionContext) {
// Only apply to active properties
let filter = {
const filter = {
'ancestors.id': creatureId,
reset: resetFilter,
removed: { $ne: true },
inactive: { $ne: true },
};
// update all attribute's damage
filter.type = 'attribute';
CreatureProperties.update(filter, {
const attributeFilter = {
...filter,
type: 'attribute',
damage: { $ne: 0 },
}
CreatureProperties.find(attributeFilter, {
fields: { name: 1, damage: 1 }
}).forEach(prop => {
actionContext.addLog({
name: prop.name,
value: prop.damage >= 0 ? `Restored ${prop.damage}` : `Removed ${-prop.damage}`
});
});
CreatureProperties.update(attributeFilter, {
$set: {
damage: 0,
dirty: true,
}
}, {
selector: {type: 'attribute'},
selector: { type: 'attribute' },
multi: true,
});
// Update all action-like properties' usesUsed
filter.type = {$in: [
'action',
'attack',
'spell'
]};
CreatureProperties.update(filter, {
const actionFilter = {
...filter,
type: {
$in: ['action', 'spell']
},
usesUsed: { $ne: 0 },
};
CreatureProperties.find(actionFilter, {
fields: { name: 1, usesUsed: 1 }
}).forEach(prop => {
actionContext.addLog({
name: prop.name,
value: prop.usesUsed >= 0 ? `Restored ${prop.usesUsed} uses` : `Removed ${-prop.usesUsed} uses`
});
});
CreatureProperties.update(actionFilter, {
$set: {
usesUsed: 0,
dirty: true,
}
}, {
selector: {type: 'action'},
selector: { type: 'action' },
multi: true,
});
// Reset half hit dice on a long rest, starting with the highest dice
if (restType === 'longRest'){
let hitDice = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'attribute',
attributeType: 'hitDice',
removed: {$ne: true},
inactive: {$ne: true},
}, {
fields: {
hitDiceSize: 1,
damage: 1,
total: 1,
}
function resetHitDice(creatureId, actionContext) {
let hitDice = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'attribute',
attributeType: 'hitDice',
removed: { $ne: true },
inactive: { $ne: true },
}, {
fields: {
name: 1,
hitDiceSize: 1,
damage: 1,
total: 1,
}
}).fetch();
// Use a collator to do sorting in natural order
let collator = new Intl.Collator('en', {
numeric: true, sensitivity: 'base'
});
// Get the hit dice in decending order of hitDiceSize
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
hitDice.sort(compare);
// Get the total number of hit dice that can be recovered this rest
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
let recoverableHd = Math.max(Math.floor(totalHd * resetMultiplier), 1);
// recover each hit dice in turn until the recoverable amount is used up
let amountToRecover, resultingDamage;
hitDice.forEach(hd => {
if (!recoverableHd) return;
amountToRecover = Math.min(recoverableHd, hd.damage || 0);
if (!amountToRecover) return;
recoverableHd -= amountToRecover;
resultingDamage = hd.damage - amountToRecover;
actionContext.addLog({
name: hd.name,
value: amountToRecover >= 0 ? `Restored ${amountToRecover} hit dice` : `Removed ${-amountToRecover} hit dice`
});
CreatureProperties.update(hd._id, {
$set: {
damage: resultingDamage,
dirty: true,
}
}).fetch();
// Use a collator to do sorting in natural order
let collator = new Intl.Collator('en', {
numeric: true, sensitivity: 'base'
}, {
selector: { type: 'attribute' },
});
// Get the hit dice in decending order of hitDiceSize
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
hitDice.sort(compare);
// Get the total number of hit dice that can be recovered this rest
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1);
// recover each hit dice in turn until the recoverable amount is used up
let amountToRecover, resultingDamage;
hitDice.forEach(hd => {
if (!recoverableHd) return;
amountToRecover = Math.min(recoverableHd, hd.damage || 0);
if (!amountToRecover) return;
recoverableHd -= amountToRecover;
resultingDamage = hd.damage - amountToRecover;
CreatureProperties.update(hd._id, {
$set: {
damage: resultingDamage,
dirty: true,
}
}, {
selector: {type: 'attribute'},
});
});
}
});
}
export default restCreature;

View File

@@ -7,6 +7,7 @@ import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/met
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js';
export default function applyAction(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
@@ -16,7 +17,7 @@ export default function applyAction(node, actionContext) {
// Log the name and summary
let content = { name: prop.name };
if (prop.summary?.text){
if (prop.summary?.text) {
recalculateInlineCalculations(prop.summary, actionContext);
content.value = prop.summary.value;
}
@@ -29,24 +30,27 @@ export default function applyAction(node, actionContext) {
const attack = prop.attackRoll || prop.attackRollBonus;
// Attack if there is an attack roll
if (attack && attack.calculation){
if (targets.length){
if (attack && attack.calculation) {
if (targets.length) {
targets.forEach(target => {
applyAttackToTarget({attack, target, actionContext});
applyAttackToTarget({ attack, target, actionContext });
// Apply the children, but only to the current target
actionContext.targets = [target];
applyChildren(node, actionContext);
});
} else {
applyAttackWithoutTarget({attack, actionContext});
applyAttackWithoutTarget({ attack, actionContext });
applyChildren(node, actionContext);
}
} else {
applyChildren(node, actionContext);
}
if (prop.actionType === 'event' && prop.variableName) {
resetProperties(actionContext.creature._id, prop.variableName, actionContext);
}
}
function applyAttackWithoutTarget({attack, actionContext}){
function applyAttackWithoutTarget({ attack, actionContext }) {
delete actionContext.scope['$attackHit'];
delete actionContext.scope['$attackMiss'];
delete actionContext.scope['$criticalHit'];
@@ -62,16 +66,16 @@ function applyAttackWithoutTarget({attack, actionContext}){
criticalMiss,
} = rollAttack(attack, scope);
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
if (scope['$attackAdvantage'] === 1){
if (scope['$attackAdvantage'] === 1) {
name += ' (Advantage)';
} else if(scope['$attackAdvantage'] === -1){
} else if (scope['$attackAdvantage'] === -1) {
name += ' (Disadvantage)';
}
if (!criticalMiss){
scope['$attackHit'] = {value: true}
if (!criticalMiss) {
scope['$attackHit'] = { value: true }
}
if (!criticalHit){
scope['$attackMiss'] = {value: true};
if (!criticalHit) {
scope['$attackMiss'] = { value: true };
}
actionContext.addLog({
@@ -81,7 +85,7 @@ function applyAttackWithoutTarget({attack, actionContext}){
});
}
function applyAttackToTarget({attack, target, actionContext}){
function applyAttackToTarget({ attack, target, actionContext }) {
const scope = actionContext.scope;
delete scope['$attackHit'];
delete scope['$attackMiss'];
@@ -99,15 +103,15 @@ function applyAttackToTarget({attack, target, actionContext}){
criticalMiss,
} = rollAttack(attack, scope);
if (target.variables.armor){
if (target.variables.armor) {
const armor = target.variables.armor.value;
let name = criticalHit ? 'Critical Hit!' :
criticalMiss ? 'Critical Miss!' :
result > armor ? 'Hit!' : 'Miss!';
if (scope['$attackAdvantage'] === 1){
result > armor ? 'Hit!' : 'Miss!';
if (scope['$attackAdvantage'] === 1) {
name += ' (Advantage)';
} else if(scope['$attackAdvantage'] === -1){
} else if (scope['$attackAdvantage'] === -1) {
name += ' (Disadvantage)';
}
@@ -116,15 +120,15 @@ function applyAttackToTarget({attack, target, actionContext}){
value: `${resultPrefix}\n**${result}**`,
inline: true,
});
if (criticalMiss || result < armor){
scope['$attackMiss'] = {value: true};
if (criticalMiss || result < armor) {
scope['$attackMiss'] = { value: true };
} else {
scope['$attackHit'] = {value: true};
scope['$attackHit'] = { value: true };
}
} else {
actionContext.addLog({
name: 'Error',
value:'Target has no `armor`',
value: 'Target has no `armor`',
});
actionContext.addLog({
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
@@ -134,10 +138,10 @@ function applyAttackToTarget({attack, target, actionContext}){
}
}
function rollAttack(attack, scope){
function rollAttack(attack, scope) {
const rollModifierText = numberToSignedString(attack.value, true);
let value, resultPrefix;
if (scope['$attackAdvantage'] === 1){
if (scope['$attackAdvantage'] === 1) {
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
@@ -146,7 +150,7 @@ function rollAttack(attack, scope){
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (scope['$attackAdvantage'] === -1){
} else if (scope['$attackAdvantage'] === -1) {
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
@@ -159,25 +163,25 @@ function rollAttack(attack, scope){
value = rollDice(1, 20)[0];
resultPrefix = `1d20 [${value}] ${rollModifierText}`
}
scope['$attackRoll'] = {value};
scope['$attackRoll'] = { value };
const result = value + attack.value;
const {criticalHit, criticalMiss} = applyCrits(value, scope);
return {resultPrefix, result, value, criticalHit, criticalMiss};
const { criticalHit, criticalMiss } = applyCrits(value, scope);
return { resultPrefix, result, value, criticalHit, criticalMiss };
}
function applyCrits(value, scope){
function applyCrits(value, scope) {
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
let criticalHit = value >= criticalHitTarget;
let criticalMiss;
if (criticalHit){
scope['$criticalHit'] = {value: true};
if (criticalHit) {
scope['$criticalHit'] = { value: true };
} else {
criticalMiss = value === 1;
if (criticalMiss){
scope['$criticalMiss'] = {value: true};
if (criticalMiss) {
scope['$criticalMiss'] = { value: true };
}
}
return {criticalHit, criticalMiss};
return { criticalHit, criticalMiss };
}
function applyChildren(node, actionContext) {
@@ -185,9 +189,9 @@ function applyChildren(node, actionContext) {
node.children.forEach(child => applyProperty(child, actionContext));
}
function spendResources(prop, actionContext){
function spendResources(prop, actionContext) {
// Check Uses
if (prop.usesLeft <= 0){
if (prop.usesLeft <= 0) {
if (!prop.silent) actionContext.addLog({
name: 'Error',
value: `${prop.name || 'action'} does not have enough uses left`,
@@ -195,7 +199,7 @@ function spendResources(prop, actionContext){
return true;
}
// Resources
if (prop.insufficientResources){
if (prop.insufficientResources) {
if (!prop.silent) actionContext.addLog({
name: 'Error',
value: 'This creature doesn\'t have sufficient resources to perform this action',
@@ -209,14 +213,14 @@ function spendResources(prop, actionContext){
try {
prop.resources.itemsConsumed.forEach(itemConsumed => {
recalculateCalculation(itemConsumed.quantity, actionContext);
if (!itemConsumed.itemId){
if (!itemConsumed.itemId) {
throw 'No ammo was selected for this prop';
}
let item = CreatureProperties.findOne(itemConsumed.itemId);
if (!item || item.ancestors[0].id !== prop.ancestors[0].id){
if (!item || item.ancestors[0].id !== prop.ancestors[0].id) {
throw 'The prop\'s ammo was not found on the creature';
}
if (!item.equipped){
if (!item.equipped) {
throw 'The selected ammo is not equipped';
}
if (
@@ -229,16 +233,16 @@ function spendResources(prop, actionContext){
value: itemConsumed.quantity.value,
});
let logName = item.name;
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1){
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1) {
logName = item.plural || logName;
}
if (itemConsumed.quantity.value > 0){
if (itemConsumed.quantity.value > 0) {
spendLog.push(logName + ': ' + itemConsumed.quantity.value);
} else if (itemConsumed.quantity.value < 0){
} else if (itemConsumed.quantity.value < 0) {
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
}
});
} catch (e){
} catch (e) {
actionContext.addLog({
name: 'Error',
value: e,
@@ -251,9 +255,9 @@ function spendResources(prop, actionContext){
itemQuantityAdjustments.forEach(adjustQuantityWork);
// Use uses
if (prop.usesLeft){
if (prop.usesLeft) {
CreatureProperties.update(prop._id, {
$inc: {usesUsed: 1}
$inc: { usesUsed: 1 }
}, {
selector: prop
});
@@ -270,7 +274,7 @@ function spendResources(prop, actionContext){
if (!attConsumed.quantity?.value) return;
let stat = actionContext.scope[attConsumed.variableName];
if (!stat){
if (!stat) {
spendLog.push(stat.name + ': ' + ' not found');
return;
}
@@ -280,9 +284,9 @@ function spendResources(prop, actionContext){
value: attConsumed.quantity.value,
actionContext,
});
if (attConsumed.quantity.value > 0){
if (attConsumed.quantity.value > 0) {
spendLog.push(stat.name + ': ' + attConsumed.quantity.value);
} else if (attConsumed.quantity.value < 0){
} else if (attConsumed.quantity.value < 0) {
gainLog.push(stat.name + ': ' + -attConsumed.quantity.value);
}
});

View File

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

View File

@@ -69,7 +69,7 @@ const doAction = new ValidatedMethod({
let slot;
// If a spell requires a slot, make sure a slot is spent
if (!spell.castWithoutSpellSlots && !(ritual && spell.ritual)) {
if (spell.level && !spell.castWithoutSpellSlots && !(ritual && spell.ritual)) {
slot = CreatureProperties.findOne(slotId);
if (!slot) {
throw new Meteor.Error('No slot',

View File

@@ -23,7 +23,7 @@ const doCheck = new ValidatedMethod({
numRequests: 10,
timeInterval: 5000,
},
run({propId, scope}) {
run({ propId, scope }) {
const prop = CreatureProperties.findOne(propId);
const creatureId = prop.ancestors[0].id;
const actionContext = new ActionContext(creatureId, [creatureId], this);
@@ -33,13 +33,13 @@ const doCheck = new ValidatedMethod({
assertEditPermission(actionContext.creature, this.userId);
// Do the check
doCheckWork({prop, actionContext});
doCheckWork({ prop, actionContext });
},
});
export default doCheck;
export function doCheckWork({prop, actionContext}){
export function doCheckWork({ prop, actionContext }) {
applyTriggers(actionContext.triggers.check?.before, prop, actionContext);
rollCheck(prop, actionContext);
@@ -54,17 +54,17 @@ function rollCheck(prop, actionContext) {
// get the modifier for the roll
let rollModifier;
let logName = `${prop.name} check`;
if (prop.type === 'skill'){
if (prop.type === 'skill') {
rollModifier = prop.value;
if (prop.skillType === 'save'){
if (prop.name.match(/save/i)){
if (prop.skillType === 'save') {
if (prop.name.match(/save/i)) {
logName = prop.name;
} else {
logName = prop.name ? `${prop.name} save` : 'Saving Throw';
}
}
} else if (prop.type === 'attribute'){
if (prop.attributeType === 'ability'){
} else if (prop.type === 'attribute') {
if (prop.attributeType === 'ability') {
rollModifier = prop.modifier;
} else {
rollModifier = prop.value;
@@ -80,7 +80,7 @@ function rollCheck(prop, actionContext) {
rollModifier += effectBonus;
let value, values, resultPrefix;
if (scope['$checkAdvantage'] === 1){
if (scope['$checkAdvantage'] === 1) {
logName += ' (Advantage)';
const [a, b] = rollDice(2, 20);
if (a >= b) {
@@ -90,7 +90,7 @@ function rollCheck(prop, actionContext) {
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
}
} else if (scope['$checkAdvantage'] === -1){
} else if (scope['$checkAdvantage'] === -1) {
logName += ' (Disadvantage)';
const [a, b] = rollDice(2, 20);
if (a <= b) {
@@ -106,6 +106,9 @@ function rollCheck(prop, actionContext) {
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
}
const result = (value + rollModifier) || 0;
scope['$checkDiceRoll'] = value;
scope['$checkRoll'] = result;
scope['$checkModifier'] = rollModifier;
actionContext.addLog({
name: logName,
value: `${resultPrefix} **${result}**`,
@@ -116,7 +119,7 @@ function applyUnresolvedEffects(prop, scope) {
let effectBonus = 0;
let effectString = '';
if (!prop.effects) {
return { effectBonus, effectString};
return { effectBonus, effectString };
}
prop.effects.forEach(effect => {
if (!effect.amount?.parseNode) return;
@@ -127,5 +130,5 @@ function applyUnresolvedEffects(prop, scope) {
effectBonus += effect.amount.value;
effectString += ` ${effect.amount.value < 0 ? '-' : '+'} [${effect.amount.calculation}] ${Math.abs(effect.amount.value)}`
});
return { effectBonus, effectString};
return { effectBonus, effectString };
}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { pick } from 'lodash';
export default function aggregateEffect({node, linkedNode, link}){
export default function aggregateEffect({ node, linkedNode, link }) {
if (link.data !== 'effect') return;
// store the effect aggregator, its presence indicates that the variable is
// targeted by effects
@@ -38,6 +38,7 @@ export default function aggregateEffect({node, linkedNode, link}){
operation: linkedNode.data.operation,
amount: effectAmount,
type: linkedNode.data.type,
text: linkedNode.data.text,
// ancestors: linkedNode.data.ancestors,
});
@@ -45,14 +46,13 @@ export default function aggregateEffect({node, linkedNode, link}){
const aggregator = node.data.effectAggregator;
// Get the result of the effect
const result = linkedNode.data.amount?.value;
// Skip aggregating if the result is not resolved completely
if (typeof result === 'string' || result === undefined) return;
// Aggregate the effect based on its operation
switch(linkedNode.data.operation){
switch (linkedNode.data.operation) {
case 'base':
// Take the largest base value
if (Number.isFinite(result)){
if(Number.isFinite(aggregator.base)){
if (Number.isFinite(result)) {
if (Number.isFinite(aggregator.base)) {
aggregator.base = Math.max(aggregator.base, result);
} else {
aggregator.base = result;

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
/*
* Actions are things a character can do
@@ -24,9 +25,17 @@ let ActionSchema = createPropertySchema({
// long actions take longer than 1 round to cast
actionType: {
type: String,
allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long'],
allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long', 'event'],
defaultValue: 'action',
},
// If the action type is an event, what is the variable name of that event?
variableName: {
type: String,
optional: true,
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
},
// Who is the action directed at
target: {
type: String,
@@ -56,8 +65,10 @@ let ActionSchema = createPropertySchema({
// How this action's uses are reset automatically
reset: {
type: String,
allowedValues: ['longRest', 'shortRest'],
optional: true,
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
},
// Resources
resources: {
@@ -74,7 +85,7 @@ let ActionSchema = createPropertySchema({
'resources.itemsConsumed.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
autoValue() {
if (!this.isSet) return Random.id();
}
},
@@ -101,7 +112,7 @@ let ActionSchema = createPropertySchema({
'resources.attributesConsumed.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
autoValue() {
if (!this.isSet) return Random.id();
}
},
@@ -218,4 +229,4 @@ const ComputedActionSchema = new SimpleSchema()
.extend(ActionSchema)
.extend(ComputedOnlyActionSchema);
export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema};
export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema };

View File

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

View File

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

View File

@@ -17,6 +17,12 @@ let SpellListSchema = createPropertySchema({
type: 'fieldToCompute',
optional: true,
},
// The variable name of the ability this spell relies on
ability: {
type: String,
optional: true,
max: STORAGE_LIMITS.variableName,
},
// Calculation of The attack roll bonus used by spell attacks in this list
attackRollBonus: {
type: 'fieldToCompute',
@@ -38,6 +44,12 @@ const ComputedOnlySpellListSchema = createPropertySchema({
type: 'computedOnlyField',
optional: true,
},
// Computed value determined by the ability
abilityMod: {
type: SimpleSchema.Integer,
optional: true,
removeBeforeCompute: true,
},
attackRollBonus: {
type: 'computedOnlyField',
optional: true,

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ const docPaths = [
'dependency-loops',
'docs',
'tags',
'walkthroughs/create-a-class',
];
const docs = new Map();
docPaths.forEach(path => {

View File

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

View File

@@ -0,0 +1,46 @@
<template>
<smart-select
label="Reset"
clearable
style="flex-basis: 300px;"
:hint="hint"
:items="resetOptions"
:value="value"
:error-messages="errorMessages"
:menu-props="{auto: true, lazy: true}"
@change="(value, ack) => $emit('change', value, ack)"
/>
</template>
<script lang="js">
import createListOfProperties from '/imports/ui/properties/forms/shared/lists/createListOfProperties.js';
export default {
props: {
value: [String, Number, Date, Array, Object, Boolean],
errorMessages: [String, Array],
hint: {
type: String,
default: undefined,
}
},
meteor: {
resetOptions() {
const eventActions = createListOfProperties({
type: 'action',
actionType: 'event',
}, true);
const defaultEvents = [
{
text: 'Short rest',
value: 'shortRest',
}, {
text: 'Long rest',
value: 'longRest',
}
];
return [...defaultEvents, ...eventActions];
},
},
}
</script>

View File

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

View File

@@ -39,6 +39,11 @@
:input-value="model.settings.hideUnusedStats"
@change="value => $emit('change', {path: ['settings','hideUnusedStats'], value: !!value})"
/>
<v-switch
label="Hide rest buttons"
:input-value="model.settings.hideRestButtons"
@change="value => $emit('change', {path: ['settings','hideRestButtons'], value: !!value})"
/>
<v-switch
label="Show spells tab"
:input-value="!model.settings.hideSpellsTab"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,7 @@
:embedded="embedded"
@duplicate="duplicate"
@move="move"
@copy="copy"
@remove="remove"
@toggle-editing="editing = !editing"
@color-changed="value => change({path: ['color'], value})"
@@ -95,10 +96,13 @@
import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormIndex.js';
import propertyViewerIndex from '/imports/ui/properties/viewers/shared/propertyViewerIndex.js';
import { get } from 'lodash';
import { assertDocEditPermission } from '/imports/api/sharing/sharingPermissions.js';
import {
assertDocEditPermission, assertDocCopyPermission
} from '/imports/api/sharing/sharingPermissions.js';
import { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
import copyLibraryNodeTo from '/imports/api/library/methods/copyLibraryNodeTo.js';
let formIndex = {};
for (let key in propertyFormIndex){
@@ -126,7 +130,7 @@
},
reactiveProvide: {
name: 'context',
include: ['editPermission', 'isLibraryForm'],
include: ['editPermission', 'copyPermission', 'isLibraryForm'],
},
data(){return {
editing: !!this.startInEditTab,
@@ -162,6 +166,14 @@
return false;
}
},
copyPermission(){
try {
assertDocCopyPermission(this.model, Meteor.userId());
return true;
} catch (e) {
return false;
}
},
},
methods: {
getPropertyName,
@@ -200,6 +212,37 @@
}
});
},
copy(){
const thisId = this._id;
this.$store.commit('pushDialogStack', {
component: 'move-library-node-dialog',
elementId: 'property-toolbar-menu-button',
data: {
action: 'Copy',
},
callback(parentId){
if (!parentId) return;
copyLibraryNodeTo.call({
_id: thisId,
parent: {
collection: 'libraryNodes',
id: parentId
},
}, (error) => {
if (error) {
console.error(error);
snackbar({
text: error.reason || error.message || error.toString(),
});
} else {
snackbar({
text: 'Copied successfully',
});
}
});
}
});
},
change({path, value, ack}){
updateLibraryNode.call({_id: this.currentId, path, value}, (error) =>{
if (ack){

View File

@@ -12,6 +12,13 @@
:value="model.color"
@input="value => change({path: ['color'], value})"
/>
<v-btn
icon
data-id="help-button"
@click="helpDialog"
>
<v-icon>mdi-help</v-icon>
</v-btn>
</template>
<component
:is="type"
@@ -44,6 +51,7 @@ import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormI
import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js';
import ColorPicker from '/imports/ui/components/ColorPicker.vue';
import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex.js';
import PROPERTIES from '/imports/constants/PROPERTIES.js';
export default {
components: {
@@ -77,6 +85,12 @@ export default {
isLibraryForm: true,
};
},
computed: {
docsPath() {
const propDef = PROPERTIES[this.type];
return propDef && propDef.docsPath;
},
},
watch: {
type(newType) {
if (!newType) return;
@@ -87,6 +101,17 @@ export default {
this.model = model;
},
},
methods: {
helpDialog() {
this.$store.commit('pushDialogStack', {
component: 'help-dialog',
elementId: 'help-button',
data: {
path: this.docsPath,
},
});
},
}
}
</script>

View File

@@ -16,7 +16,7 @@
color="primary"
@click="$store.dispatch('popDialogStack', node._id)"
>
Move
{{ action || 'Move' }}
</v-btn>
</template>
</dialog-base>
@@ -30,6 +30,12 @@ export default {
DialogBase,
LibraryAndNode,
},
props: {
action: {
type: String,
default: undefined,
},
},
data() {
return {
node: undefined,

View File

@@ -1,7 +1,10 @@
<template>
<v-container class="documentation">
<v-row>
<v-col cols="12">
<v-row justify="center">
<v-col
cols="12"
lg="8"
>
<v-fade-transition mode="out-in">
<v-card
v-if="doc"

View File

@@ -1,7 +1,10 @@
<template>
<v-container class="documentation">
<v-row>
<v-col cols="12">
<v-row justify="center">
<v-col
cols="12"
lg="8"
>
<v-card>
<v-card-text class="markdown">
<h1>Functions</h1>

View File

@@ -0,0 +1,80 @@
<template lang="html">
<v-btn
:loading="doActionLoading"
:disabled="context.editPermission === false"
outlined
class="event-button"
style="min-width: 160px; max-width: 100%;"
:color="model.color"
@click="doAction"
>
<property-icon
style="margin-left: -4px; margin-right: 8px;"
:model="model"
/>
<div
class="text-truncate"
>
{{ model.name }}
</div>
</v-btn>
</template>
<script lang="js">
import doAction from '/imports/api/engine/actions/doAction.js';
import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default {
components: {
PropertyIcon,
},
inject: {
context: { default: {} }
},
props: {
model: {
type: Object,
required: true,
},
},
data(){return {
activated: undefined,
doActionLoading: false,
hovering: false,
}},
methods: {
click(e) {
this.$emit('click', e);
},
doAction({ advantage }) {
this.doActionLoading = true;
this.shwing();
doAction.call({
actionId: this.model._id,
scope: {
$attackAdvantage: advantage,
}
}, error => {
this.doActionLoading = false;
if (error) {
console.error(error);
snackbar({ text: error.reason });
}
});
},
shwing() {
this.activated = true;
setTimeout(() => {
this.activated = undefined;
}, 150);
}
}
}
</script>
<style lang="css">
.event-button .v-btn__content {
max-width: 100%;
}
</style>

View File

@@ -5,54 +5,19 @@
@mouseover="hasClickListener ? hovering = true : undefined"
@mouseleave="hasClickListener ? hovering = false : undefined"
>
<div class="layout align-center">
<roll-popup
v-if="model.attributeType === 'modifier' || model.type === 'skill'"
button-class="px-0"
text
height="70"
min-width="72"
:roll-text="computedValue && computedValue.toString()"
:name="model.name"
:advantage="model.advantage"
:loading="checkLoading"
:disabled="!context.editPermission"
@roll="check"
>
<v-card-title class="value text-h4 flex-shrink-0">
{{ computedValue }}
</v-card-title>
</roll-popup>
<v-card-title
v-else
class="value text-h4 flex-shrink-0"
>
{{ computedValue }}
</v-card-title>
<v-card-title class="name text-subtitle-1 text-truncate d-block pl-0">
{{ model.name }}
</v-card-title>
</div>
<attribute-card-content :model="model" />
<card-highlight :active="hasClickListener && hovering" />
</v-card>
</template>
<script lang="js">
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import RollPopup from '/imports/ui/components/RollPopup.vue';
import doCheck from '/imports/api/engine/actions/doCheck.js';
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
import AttributeCardContent from '/imports/ui/properties/components/attributes/AttributeCardContent.vue';
export default {
components: {
RollPopup,
CardHighlight,
},
inject: {
context: {
default: {},
},
AttributeCardContent,
},
props: {
model: {
@@ -68,41 +33,11 @@
hasClickListener(){
return this.$listeners && !!this.$listeners.click
},
computedValue(){
if (this.model.attributeType === 'modifier' || this.model.type === 'skill'){
return numberToSignedString(this.model.value);
} else {
return this.model.value
}
}
},
methods: {
signed: numberToSignedString,
click(e){
this.$emit('click', e);
},
check({advantage}){
this.checkLoading = true;
doCheck.call({
propId: this.model._id,
scope: {
$checkAdvantage: advantage,
},
}, error => {
this.checkLoading = false;
if (error){
console.error(error);
snackbar({text: error.reason});
}
});
},
},
}
</script>
<style lang="css" scoped>
.value {
min-width: 72px;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,97 @@
<template>
<div
class="layout align-center"
@click="$emit('click')"
@mouseover="$emit('mouseover')"
@mouseleave="$emit('mouseleave')"
>
<roll-popup
v-if="model.attributeType === 'modifier' || model.type === 'skill'"
button-class="px-0"
text
height="70"
min-width="72"
:roll-text="computedValue && computedValue.toString()"
:name="model.name"
:advantage="model.advantage"
:loading="checkLoading"
:disabled="!context.editPermission"
@roll="check"
>
<v-card-title class="value text-h4 flex-shrink-0">
{{ computedValue }}
</v-card-title>
</roll-popup>
<v-card-title
v-else
class="value text-h4 flex-shrink-0"
>
{{ computedValue }}
</v-card-title>
<v-card-title class="name text-subtitle-1 text-truncate d-block pl-0">
{{ model.name }}
</v-card-title>
</div>
</template>
<script lang="js">
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import RollPopup from '/imports/ui/components/RollPopup.vue';
import doCheck from '/imports/api/engine/actions/doCheck.js';
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default {
components: {
RollPopup,
},
inject: {
context: {
default: {},
},
},
props: {
model: {
type: Object,
required: true,
},
},
data(){return {
checkLoading: false,
hovering: false,
}},
computed: {
computedValue(){
if (this.model.attributeType === 'modifier' || this.model.type === 'skill'){
return numberToSignedString(this.model.value);
} else {
return this.model.value
}
}
},
methods: {
signed: numberToSignedString,
check({advantage}){
this.checkLoading = true;
doCheck.call({
propId: this.model._id,
scope: {
$checkAdvantage: advantage,
},
}, error => {
this.checkLoading = false;
if (error){
console.error(error);
snackbar({text: error.reason});
}
});
},
},
}
</script>
<style lang="css" scoped>
.value {
min-width: 72px;
justify-content: center;
}
</style>

View File

@@ -6,7 +6,7 @@
style="min-height: 42px;"
:class="{ hover }"
class="my-1 health-bar"
:data-id="_id"
:data-id="model._id"
>
<div
class="subheading text-truncate pa-2 name"
@@ -14,14 +14,10 @@
@mouseleave="hover = false"
@click="$emit('click')"
>
{{ name }}
{{ model.name }}
</div>
<v-flex
style="
height: 24px;
flex-basis: 300px;
flex-grow: 100;
"
style="height: 24px; flex-basis: 300px; flex-grow: 100;"
>
<div
column
@@ -50,8 +46,7 @@
'white--text': isTextLight,
'black--text': !isTextLight,
}"
style="
font-size: 15px;
style="font-size: 15px;
line-height: 24px;
font-weight: 600;
position: absolute;
@@ -59,30 +54,30 @@
top: 0;
right: 0;
bottom: 0;
text-align: center;
"
text-align: center;"
>
{{ value }} / {{ maxValue }}
{{ model.value }} / {{ model.total }}
</div>
</div>
</div>
<transition name="transition">
<v-menu
v-model="editing"
absolute
transition="scale-transition"
origin="center center"
content-class="no-menu-shadow"
:position-x="x"
:position-y="y"
:min-width="305"
:close-on-content-click="false"
>
<increment-menu
v-show="editing"
:value="value"
:value="model.value"
:open="editing"
@change="changeIncrementMenu"
@close="cancelEdit"
/>
</transition>
<transition name="background-transition">
<div
v-if="editing"
class="page-tint"
@click="cancelEdit"
/>
</transition>
</v-menu>
</v-flex>
</v-layout>
</template>
@@ -104,31 +99,9 @@ export default {
},
},
props: {
value: {
type: Number,
default: undefined,
},
maxValue: {
type: Number,
default: undefined,
},
name: {
type: String,
default: undefined,
},
color: {
type: String,
default() {
return this.$vuetify.theme.currentTheme.primary
},
},
midColor: {
type: String,
default: undefined,
},
lowColor: {
type: String,
default: undefined,
model: {
type: Object,
required: true,
},
_id: String,
},
@@ -136,24 +109,29 @@ export default {
return {
editing: false,
hover: false,
x: 0,
y: 0,
};
},
computed: {
fillFraction() {
let fraction = this.value / this.maxValue;
let fraction = this.model.value / this.model.total;
if (fraction < 0) fraction = 0;
if (fraction > 1) fraction = 1;
return fraction;
},
color() {
return this.model.color || this.$vuetify.theme.currentTheme.primary
},
barColor() {
const fraction = this.value / this.maxValue;
const fraction = this.model.value / this.model.total;
if (!Number.isFinite(fraction)) return this.color;
if (fraction > 0.5) {
return this.color;
} else if (this.midColor && this.lowColor) {
return chroma.mix(this.lowColor, this.midColor, fraction * 2).hex();
} else if (this.midColor) {
return this.midColor;
} else if (this.model.healthBarColorMid && this.model.healthBarColorLow) {
return chroma.mix(this.model.healthBarColorLow, this.model.healthBarColorMid, fraction * 2).hex();
} else if (this.model.healthBarColorMid) {
return this.model.healthBarColorMid;
}
return this.color;
},
@@ -166,7 +144,7 @@ export default {
isTextLight() {
return isDarkColor(this.barBackgroundColor);
/* Change color at the halfway mark
const fraction = this.value / this.maxValue;
const fraction = this.model.value / this.model.total;
if (fraction >= 0.5){
return isDarkColor(this.barColor);
} else {
@@ -176,8 +154,14 @@ export default {
}
},
methods: {
edit() {
this.editing = true;
edit(e) {
e.preventDefault()
this.editing = false;
this.x = e.clientX - 165;
this.y = e.clientY - 24;
this.$nextTick(() => {
this.editing = true
});
},
cancelEdit() {
this.editing = false;
@@ -199,6 +183,10 @@ export default {
z-index: 7;
position: relative;
}
.no-menu-shadow {
box-shadow: none !important;
}
</style>
<style scoped>

View File

@@ -3,13 +3,7 @@
<health-bar
v-for="attribute in attributes"
:key="attribute._id"
:value="attribute.value"
:max-value="attribute.total"
:name="attribute.name"
:color="attribute.color"
:mid-color="attribute.healthBarColorMid"
:low-color="attribute.healthBarColorLow"
:_id="attribute._id"
:model="attribute"
@change="e => $emit('change', {_id: attribute._id, change: e})"
@click="e => $emit('click', {_id: attribute._id})"
/>

View File

@@ -42,6 +42,10 @@ export default {
removed: { $ne: true },
inactive: { $ne: true },
overridden: { $ne: true },
$nor: [
{ hideWhenTotalZero: true, total: 0 },
{ hideWhenValueZero: true, value: 0 },
],
};
if (creature.settings.hideUnusedStats) {
filter.hide = { $ne: true };

View File

@@ -3,57 +3,26 @@
class="resource-card"
:class="hover ? 'elevation-8': ''"
>
<v-layout>
<div class="buttons layout column justify-center pl-3">
<v-btn
icon
small
:disabled="(model.value >= model.total && !model.ignoreUpperLimit) || context.editPermission === false"
@click="increment(1)"
>
<v-icon>mdi-chevron-up</v-icon>
</v-btn>
<v-btn
icon
small
:disabled="(model.value <= 0 && !model.ignoreLowerLimit) || context.editPermission === false"
@click="increment(-1)"
>
<v-icon>mdi-chevron-down</v-icon>
</v-btn>
</div>
<div class="layout align-center value pl-2 pr-3">
<div class="text-h4">
{{ model.value }}
</div>
<div
v-if="model.total !== 0"
class="text-h6 ml-2 max-value"
>
/{{ model.total }}
</div>
</div>
<div
class="content layout align-center pr-3"
@click="click"
@mouseover="hover = true"
@mouseleave="hover = false"
>
<div class="text-truncate ">
{{ model.name }}
</div>
</div>
</v-layout>
<resource-card-content
:model="model"
:hover="hover"
@mouseover="hover = true"
@mouseleave="hover = false"
@click="$emit('click')"
@change="e => $emit('change', e)"
/>
<card-highlight :active="hover" />
</v-card>
</template>
<script lang="js">
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
import ResourceCardContent from '/imports/ui/properties/components/attributes/ResourceCardContent.vue';
export default {
components: {
CardHighlight,
ResourceCardContent,
},
inject: {
context: { default: {} }
@@ -69,46 +38,16 @@ export default {
hover: false,
}
},
methods: {
click(e) {
this.$emit('click', e);
},
increment(value) {
this.$emit('change', { type: 'increment', value })
},
},
};
</script>
<style lang="css" scoped>
<style lang="css">
.resource-card {
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.resource-card>div {
.resource-card > div {
padding-top: 16px;
padding-bottom: 16px;
}
.buttons,
.value {
flex-shrink: 0;
flex-grow: 0;
}
.buttons>.v-btn {
margin: 0;
}
.content {
cursor: pointer;
}
.max-value {
color: rgba(0, 0, 0, .54);
}
.theme--dark .max-value {
color: rgba(255, 255, 255, 0.54);
}
</style>

View File

@@ -0,0 +1,89 @@
<template>
<v-layout>
<div class="buttons layout column justify-center pl-3">
<v-btn
icon
small
:disabled="(model.value >= model.total && !model.ignoreUpperLimit) || context.editPermission === false"
@click="increment(1)"
>
<v-icon>mdi-chevron-up</v-icon>
</v-btn>
<v-btn
icon
small
:disabled="(model.value <= 0 && !model.ignoreLowerLimit) || context.editPermission === false"
@click="increment(-1)"
>
<v-icon>mdi-chevron-down</v-icon>
</v-btn>
</div>
<div class="layout align-center value pl-2 pr-3">
<div class="text-h4">
{{ model.value }}
</div>
<div
v-if="model.total !== 0"
class="text-h6 ml-2 max-value"
>
/{{ model.total }}
</div>
</div>
<div
class="content layout align-center pr-3"
@click="click"
@mouseover="$emit('mouseover')"
@mouseleave="$emit('mouseleave')"
>
<div class="text-truncate ">
{{ model.name }}
</div>
</div>
</v-layout>
</template>
<script lang="js">
export default {
inject: {
context: { default: {} }
},
props: {
model: {
type: Object,
required: true,
},
hover: {
type: Boolean,
}
},
methods: {
click(e) {
this.$emit('click', e);
},
increment(value) {
this.$emit('change', { type: 'increment', value })
},
},
};
</script>
<style lang="css" scoped>
.buttons,
.value {
flex-shrink: 0;
flex-grow: 0;
}
.buttons>.v-btn {
margin: 0;
}
.content {
cursor: pointer;
}
.max-value {
color: rgba(0, 0, 0, .54);
}
.theme--dark .max-value {
color: rgba(255, 255, 255, 0.54);
}
</style>

View File

@@ -61,7 +61,6 @@ export default {
required: true,
},
dark: Boolean,
hideCastButton: Boolean,
disabled: Boolean,
},
computed: {

View File

@@ -0,0 +1,30 @@
<template>
<v-list-item
@click="$emit('click')"
>
<v-list-item-content>
<v-list-item-title>
{{ model.name }}
</v-list-item-title>
</v-list-item-content>
<v-list-item-action v-if="!model.hideRemoveButton">
<v-btn
icon
@click.stop="$emit('remove')"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
</template>
<script lang="js">
export default {
props: {
model: {
type: Object,
required: true,
},
},
}
</script>

View File

@@ -0,0 +1,81 @@
<template>
<div
v-if="model.name || (properties && properties.length)"
>
<v-card
class="folder-group-card pb-2"
>
<v-subheader v-if="model.name">
{{ model.name }}
</v-subheader>
<component
:is="prop.type"
v-for="prop in properties"
:key="prop._id"
:model="prop"
:data-id="prop._id"
@click="$emit('click-property', {_id: prop._id})"
@sub-click="_id => $emit('sub-click', _id)"
@remove="$emit('remove', prop._id)"
/>
</v-card>
</div>
</template>
<script lang="js">
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import propComponents from '/imports/ui/properties/components/folders/propertyComponentIndex.js';
export default {
components: {
...propComponents,
},
props: {
model: {
type: Object,
required: true,
}
},
meteor: {
properties() {
const props = [];
CreatureProperties.find({
'parent.id': this.model._id,
removed: { $ne: true },
overridden: { $ne: true },
$or: [
{
type: 'toggle',
showUI: true,
deactivatedByAncestor: { $ne: true },
},
{
inactive: { $ne: true }
},
],
$nor: [
{ hideWhenTotalZero: true, total: 0 },
{ hideWhenValueZero: true, value: 0 },
],
}, {
sort: { order: 1 },
}).forEach(prop => {
if (this.$options.components[prop.type]) {
props.push(prop);
}
});
return props;
},
},
}
</script>
<style>
.folder-group-card .v-card {
box-shadow: none !important;
border-radius: 0 !important;
}
.folder-group-card .drag-handle {
display: none !important;
}
</style>

View File

@@ -0,0 +1,35 @@
<template>
<div
v-if="model.actionType === 'event'"
class="d-flex justify-center"
>
<event-button
class="ma-1"
:model="model"
/>
</div>
<action-card
v-else
:model="model"
@click="$emit('click')"
@sub-click="_id => $emit('sub-click', _id)"
/>
</template>
<script lang="js">
import ActionCard from '/imports/ui/properties/components/actions/ActionCard.vue';
import EventButton from '/imports/ui/properties/components/actions/EventButton.vue';
export default {
components: {
ActionCard,
EventButton,
},
props: {
model: {
type: Object,
required: true,
},
},
}
</script>

View File

@@ -0,0 +1,95 @@
<template>
<div class="attribute">
<ability-list-tile
v-if="model.attributeType === 'ability'"
:model="model"
@click="$emit('click')"
/>
<hit-dice-list-tile
v-else-if="model.attributeType === 'hitDice'"
:model="model"
@click="$emit('click')"
@change="({ type, value }) => damageProperty({type, value: -value})"
/>
<health-bar
v-else-if="model.attributeType === 'healthBar'"
:model="model"
@change="damageProperty"
@click="$emit('click')"
/>
<spell-slot-list-tile
v-else-if="model.attributeType === 'spellSlot'"
:model="model"
@click="$emit('click')"
/>
<resource-card-content
v-else-if="model.attributeType === 'resource'"
:model="model"
@click="$emit('click')"
@change="({ type, value }) => damageProperty({type, value: -value})"
@mouseover="hover = true"
@mouseleave="hover = false"
/>
<attribute-card-content
v-else-if="model.attributeType !== 'utility'"
class="pointer"
:model="model"
@click="$emit('click')"
@mouseover="hover = true"
@mouseleave="hover = false"
/>
<card-highlight :active="hover" />
</div>
</template>
<script lang="js">
import AbilityListTile from '/imports/ui/properties/components/attributes/AbilityListTile.vue';
import HitDiceListTile from '/imports/ui/properties/components/attributes/HitDiceListTile.vue';
import HealthBar from '/imports/ui/properties/components/attributes/HealthBar.vue';
import SpellSlotListTile from '/imports/ui/properties/components/attributes/SpellSlotListTile.vue';
import ResourceCardContent from '/imports/ui/properties/components/attributes/ResourceCardContent.vue';
import AttributeCardContent from '/imports/ui/properties/components/attributes/AttributeCardContent.vue';
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
export default {
components: {
AbilityListTile,
HitDiceListTile,
HealthBar,
SpellSlotListTile,
ResourceCardContent,
AttributeCardContent,
CardHighlight,
},
props: {
model: {
type: Object,
required: true,
},
},
data() {
return {
hover: false,
}},
methods: {
damageProperty(change) {
damageProperty.call({
_id: this.model._id,
operation: change.type,
value: change.value
});
},
}
}
</script>
<style lang="css" scoped>
.attribute {
position: relative;
}
.pointer {
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,61 @@
import action from '/imports/ui/properties/components/folders/folderGroupComponents/ActionGroupComponent.vue';
//import adjustment from '';
import attribute from './folderGroupComponents/AttributeGroupComponent.vue';
import buff from '/imports/ui/properties/components/buffs/BuffListItem.vue';
//import buffRemover from '';
//import branch from '';
//import constant from '';
import container from '/imports/ui/properties/components/inventory/ContainerCard.vue';
//import classComponent from '';
//import classLevel from '';
//import damage from '';
//import damageMultiplier from '';
//import effect from '';
import feature from '/imports/ui/properties/components/features/FeatureCard.vue';
// import folder from '';
import item from '/imports/ui/properties/components/inventory/ItemListTile.vue';
import note from '/imports/ui/properties/components/persona/NoteCard.vue';
//import pointBuy from '';
//import proficiency from '';
//import propertySlot from '';
//import reference from '';
//import roll from '';
//import savingThrow from '';
import skill from '/imports/ui/properties/components/skills/SkillListTile.vue';
//import slotFiller from '';
//import spellList from '';
//import spell from '';
import toggle from '/imports/ui/properties/components/toggles/ToggleCard.vue';
//import trigger from '';
export default {
action,
//adjustment,
attribute,
buff,
//buffRemover,
//branch,
//constant,
container,
//class: classComponent,
//classLevel,
//damage,
//damageMultiplier,
//effect,
feature,
//folder,
item,
note,
//pointBuy,
//proficiency,
//propertySlot,
//reference,
//roll,
//savingThrow,
skill,
//slotFiller,
//spellList,
//spell,
toggle,
//trigger,
};

View File

@@ -32,7 +32,7 @@
@change="changeQuantity"
/>
</v-list-item-action>
<v-list-item-action>
<v-list-item-action class="drag-handle">
<v-icon
:disabled="context.editPermission === false"
style="height: 100%; width: 40px; cursor: move;"

View File

@@ -1,7 +1,9 @@
<template lang="html">
<v-card
:hover="hasClickListener"
:class="hover ? 'elevation-8': ''"
@click="click"
@mouseover="hover = true"
@mouseleave="hover = false"
>
<div class="layout align-center">
<div
@@ -18,19 +20,29 @@
{{ model.name }}
</v-card-title>
</div>
<card-highlight :active="hover" />
</v-card>
</template>
<script lang="js">
import flipToggle from '/imports/api/creature/creatureProperties/methods/flipToggle.js';
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
export default {
components: {
CardHighlight,
},
props: {
model: {
type: Object,
required: true,
},
},
data() {
return {
hover: false,
}
},
computed: {
hasClickListener(){
return this.$listeners && !!this.$listeners.click

View File

@@ -41,6 +41,17 @@
</v-col>
</v-row>
<v-slide-x-transition mode="out-in">
<text-field
v-if="model.actionType === 'event'"
label="Event variable name"
:value="model.variableName"
hint="Variable name of the event that this action represents"
:error-messages="errors.variableName"
@change="change('variableName', ...arguments)"
/>
</v-slide-x-transition>
<v-slide-x-transition mode="out-in">
<v-switch
v-if="!isAttack"
@@ -154,15 +165,10 @@
/>
</v-col>
</v-row>
<smart-select
label="Reset"
clearable
<reset-selector
hint="When number of uses used should be reset to zero"
style="flex-basis: 300px;"
:items="resetOptions"
:value="model.reset"
:error-messages="errors.reset"
:menu-props="{auto: true, lazy: true}"
@change="change('reset', ...arguments)"
/>
</form-section>
@@ -171,77 +177,74 @@
</template>
<script lang="js">
import ResourcesForm from '/imports/ui/properties/forms/ResourcesForm.vue';
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
import IconColorMenu from '/imports/ui/properties/forms/shared/IconColorMenu.vue';
import ResourcesForm from '/imports/ui/properties/forms/ResourcesForm.vue';
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
import IconColorMenu from '/imports/ui/properties/forms/shared/IconColorMenu.vue';
import ResetSelector from '/imports/ui/components/ResetSelector.vue';
export default {
components: {
ResourcesForm,
IconColorMenu,
},
mixins: [propertyFormMixin],
data(){
let data = {
actionTypes: [
{
text: 'Action',
value: 'action',
}, {
text: 'Bonus action',
value: 'bonus',
}, {
text: 'Attack action',
value: 'attack',
help: 'Attack actions replace a single attack when you choose to use your Action to attack',
}, {
text: 'Reaction',
value: 'reaction',
}, {
text: 'Free action',
value: 'free',
help: 'You can take one free action on your turn without using an action or bonus action'
}, {
text: 'Long action',
value: 'long',
help: 'Long actions take longer than one turn to complete'
},
],
targetOptions: [
{
text: 'Self',
value: 'self',
}, {
text: 'Single target',
value: 'singleTarget',
}, {
text: 'Multiple targets',
value: 'multipleTargets',
},
],
resetOptions: [
{
text: 'Short rest',
value: 'shortRest',
}, {
text: 'Long rest',
value: 'longRest',
}
],
attackSwitch: false,
};
data.actionTypeHints = {};
data.actionTypes.forEach(type => {
data.actionTypeHints[type.value] = type.help;
});
return data;
},
computed: {
isAttack(){
return this.attackSwitch || !!this.model.attackRoll?.calculation
}
export default {
components: {
ResourcesForm,
IconColorMenu,
ResetSelector,
},
mixins: [propertyFormMixin],
data(){
let data = {
actionTypes: [
{
text: 'Action',
value: 'action',
}, {
text: 'Bonus action',
value: 'bonus',
}, {
text: 'Attack action',
value: 'attack',
help: 'Attack actions replace a single attack when you choose to use your Action to attack',
}, {
text: 'Reaction',
value: 'reaction',
}, {
text: 'Free action',
value: 'free',
help: 'You can take one free action on your turn without using an action or bonus action'
}, {
text: 'Long action',
value: 'long',
help: 'Long actions take longer than one turn to complete'
}, {
text: 'Event',
value: 'event',
help: 'Events are actions that happen to the character like rests or dawn'
},
],
targetOptions: [
{
text: 'Self',
value: 'self',
}, {
text: 'Single target',
value: 'singleTarget',
}, {
text: 'Multiple targets',
value: 'multipleTargets',
},
],
attackSwitch: false,
};
data.actionTypeHints = {};
data.actionTypes.forEach(type => {
data.actionTypeHints[type.value] = type.help;
});
return data;
},
computed: {
isAttack(){
return this.attackSwitch || !!this.model.attackRoll?.calculation
}
};
},
};
</script>
<style lang="css" scoped>

View File

@@ -106,10 +106,17 @@
/>
<smart-switch
label="Ignore damage"
class="mr-4"
:value="model.healthBarNoDamage"
:error-messages="errors.healthBarNoDamage"
@change="change('healthBarNoDamage', ...arguments)"
/>
<smart-switch
label="Prevent damage overflow"
:value="model.healthBarNoDamageOverflow"
:error-messages="errors.healthBarNoDamageOverflow"
@change="change('healthBarNoDamageOverflow', ...arguments)"
/>
</v-layout>
<v-layout wrap>
<text-field
@@ -125,14 +132,20 @@
/>
<smart-switch
label="Ignore healing"
class="mr-4"
:value="model.healthBarNoHealing"
:error-messages="errors.healthBarNoHealing"
@change="change('healthBarNoHealing', ...arguments)"
/>
<smart-switch
label="Prevent healing overflow"
:value="model.healthBarNoHealingOverflow"
:error-messages="errors.healthBarNoHealingOverflow"
@change="change('healthBarNoHealingOverflow', ...arguments)"
/>
</v-layout>
</form-section>
</v-expand-transition>
<form-section
v-if="$slots.children"
name="Children"
@@ -151,26 +164,74 @@
@change="change('tags', ...arguments)"
/>
<div class="layout column align-center">
<smart-switch
v-if="model.attributeType !== 'hitDice'"
label="Allow decimal values"
class="no-flex"
:value="model.decimal"
:error-messages="errors.decimal"
@change="change('decimal', ...arguments)"
/>
<smart-switch
label="Can be damaged into negative values"
:value="model.ignoreLowerLimit"
:error-messages="errors.ignoreLowerLimit"
@change="change('ignoreLowerLimit', ...arguments)"
/>
<smart-switch
label="Can be incremented above total"
:value="model.ignoreUpperLimit"
:error-messages="errors.ignoreUpperLimit"
@change="change('ignoreUpperLimit', ...arguments)"
/>
<v-row dense>
<v-col
cols="12"
sm="6"
md="4"
>
<smart-switch
v-if="model.attributeType !== 'hitDice'"
label="Allow decimal values"
class="mx-4"
:value="model.decimal"
:error-messages="errors.decimal"
@change="change('decimal', ...arguments)"
/>
</v-col>
<v-col
cols="12"
sm="6"
md="4"
>
<smart-switch
label="Can be damaged into negative values"
class="mx-4"
:value="model.ignoreLowerLimit"
:error-messages="errors.ignoreLowerLimit"
@change="change('ignoreLowerLimit', ...arguments)"
/>
</v-col>
<v-col
cols="12"
sm="6"
md="4"
>
<smart-switch
label="Can be incremented above total"
class="mx-4"
:value="model.ignoreUpperLimit"
:error-messages="errors.ignoreUpperLimit"
@change="change('ignoreUpperLimit', ...arguments)"
/>
</v-col>
<v-col
cols="12"
sm="6"
md="4"
>
<smart-switch
label="Hide when total is zero"
class="mx-4"
:value="model.hideWhenTotalZero"
:error-messages="errors.hideWhenTotalZero"
@change="change('hideWhenTotalZero', ...arguments)"
/>
</v-col>
<v-col
cols="12"
sm="6"
md="4"
>
<smart-switch
label="Hide when value is zero"
class="mx-4"
:value="model.hideWhenValueZero"
:error-messages="errors.hideWhenValueZero"
@change="change('hideWhenValueZero', ...arguments)"
/>
</v-col>
</v-row>
<div
class="layout justify-center"
style="align-self: stretch;"
@@ -189,16 +250,11 @@
</div>
</div>
<div class="layout wrap">
<smart-select
<reset-selector
v-if="model.attributeType !== 'hitDice'"
label="Reset"
clearable
style="flex-basis: 300px;"
hint="When damage should be reset to zero"
:items="resetOptions"
:value="model.reset"
:error-messages="errors.reset"
:menu-props="{auto: true, lazy: true}"
@change="change('reset', ...arguments)"
/>
</div>
@@ -212,12 +268,14 @@ import FormSection from '/imports/ui/properties/forms/shared/FormSection.vue';
import FormSections from '/imports/ui/properties/forms/shared/FormSections.vue';
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
import ColorPicker from '/imports/ui/components/ColorPicker.vue';
import ResetSelector from '/imports/ui/components/ResetSelector.vue';
export default {
components: {
FormSection,
FormSections,
ColorPicker,
ResetSelector,
},
mixins: [propertyFormMixin],
inject: {

View File

@@ -26,6 +26,12 @@
:value="model.tags"
@change="change('tags', ...arguments)"
/>
<smart-switch
label="Group children on stats tab"
:value="model.groupStats"
:error-messages="errors.groupStats"
@change="change('groupStats', ...arguments)"
/>
</form-section>
</form-sections>
</div>

View File

@@ -278,15 +278,10 @@
/>
</v-col>
</v-row>
<smart-select
label="Reset"
clearable
<reset-selector
hint="When number of uses used should be reset to zero"
style="flex-basis: 300px;"
:items="resetOptions"
:value="model.reset"
:error-messages="errors.reset"
:menu-props="{auto: true, lazy: true}"
@change="change('reset', ...arguments)"
/>
</form-section>
@@ -318,6 +313,7 @@ import FormSection, { FormSections } from '/imports/ui/properties/forms/shared/F
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
import IconColorMenu from '/imports/ui/properties/forms/shared/IconColorMenu.vue';
import ResourcesForm from '/imports/ui/properties/forms/ResourcesForm.vue';
import ResetSelector from '/imports/ui/components/ResetSelector.vue';
export default {
components: {
@@ -325,6 +321,7 @@ export default {
FormSection,
IconColorMenu,
ResourcesForm,
ResetSelector,
},
mixins: [propertyFormMixin],
data() {
@@ -401,15 +398,6 @@ export default {
value: 'multipleTargets',
},
],
resetOptions: [
{
text: 'Short rest',
value: 'shortRest',
}, {
text: 'Long rest',
value: 'longRest',
}
],
attackSwitch: false,
};
},

View File

@@ -27,6 +27,15 @@
$emit('change', {path: ['maxPrepared', ...path], value, ack})"
/>
<smart-combobox
label="Spellcasting ability"
:value="model.ability"
hint="Which ability is used to cast spells in this spell list"
:items="abilityScoreList"
:error-messages="errors.ability"
@change="changeAbility"
/>
<computed-field
label="Spell save DC"
hint="The spell save DC of spells in this list"
@@ -67,9 +76,46 @@
<script lang="js">
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
import createListOfProperties from '/imports/ui/properties/forms/shared/lists/createListOfProperties.js';
export default {
mixins: [propertyFormMixin],
meteor: {
abilityScoreList() {
return createListOfProperties({
type: 'attribute',
attributeType: 'ability',
});
},
},
methods: {
changeAbility(value, ack) {
this.$emit('change', { path: ['ability'], value, ack })
const oldValue = this.model.ability;
const attackRollBonus = this.model.attackRollBonus?.calculation;
if (
!attackRollBonus ||
attackRollBonus === `proficiencyBonus + ${oldValue}.modifier`
) {
this.$emit('change', {
path: ['attackRollBonus', 'calculation'],
value: `proficiencyBonus + ${value}.modifier`
});
}
const dc = this.model.dc?.calculation;
if (
!dc ||
dc === `8 + proficiencyBonus + ${oldValue}.modifier`
) {
this.$emit('change', {
path: ['dc', 'calculation'],
value: `8 + proficiencyBonus + ${value}.modifier`
});
}
}
}
};
</script>

View File

@@ -1,12 +1,12 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
export default function createListOfProperties(filter = {}){
filter.removed = {$ne: true};
export default function createListOfProperties(filter = {}, getNamesWithValues) {
filter.removed = { $ne: true };
let propertyList = [];
let variableNames = new Set();
function addUniquePropertys(property){
if (property.variableName && !variableNames.has(property.variableName)){
function addUniquePropertys(property) {
if (property.variableName && !variableNames.has(property.variableName)) {
variableNames.add(property.variableName);
propertyList.push({
text: property.name || property.variableName,
@@ -15,8 +15,9 @@ export default function createListOfProperties(filter = {}){
});
}
}
let options = {sort: {order: 1, variableName: 1}}
let options = { sort: { order: 1, variableName: 1 } }
CreatureProperties.find(filter, options).forEach(addUniquePropertys);
LibraryNodes.find(filter, options).forEach(addUniquePropertys);
if (getNamesWithValues) return propertyList;
return Array.from(variableNames);
}

View File

@@ -1,32 +1,32 @@
const ActionForm = () => import('/imports/ui/properties/forms/ActionForm.vue');
const AdjustmentForm = () => import('/imports/ui/properties/forms/AdjustmentForm.vue');
const AttributeForm = () => import('/imports/ui/properties/forms/AttributeForm.vue');
const BuffForm = () => import('/imports/ui/properties/forms/BuffForm.vue');
const BuffRemoverForm = () => import('/imports/ui/properties/forms/BuffRemoverForm.vue');
const BranchForm = () => import('/imports/ui/properties/forms/BranchForm.vue');
const ClassForm = () => import('/imports/ui/properties/forms/ClassForm.vue');
const ClassLevelForm = () => import('/imports/ui/properties/forms/ClassLevelForm.vue');
const ConstantForm = () => import('/imports/ui/properties/forms/ConstantForm.vue');
const ContainerForm = () => import('/imports/ui/properties/forms/ContainerForm.vue');
const DamageForm = () => import('/imports/ui/properties/forms/DamageForm.vue');
const DamageMultiplierForm = () => import('/imports/ui/properties/forms/DamageMultiplierForm.vue');
const EffectForm = () => import('/imports/ui/properties/forms/EffectForm.vue');
const FeatureForm = () => import('/imports/ui/properties/forms/FeatureForm.vue');
const FolderForm = () => import('/imports/ui/properties/forms/FolderForm.vue');
const ItemForm = () => import('/imports/ui/properties/forms/ItemForm.vue');
const NoteForm = () => import('/imports/ui/properties/forms/NoteForm.vue');
const PointBuyForm = () => import('/imports/ui/properties/forms/PointBuyForm.vue');
const ProficiencyForm = () => import('/imports/ui/properties/forms/ProficiencyForm.vue');
const ReferenceForm = () => import('/imports/ui/properties/forms/ReferenceForm.vue');
const RollForm = () => import('/imports/ui/properties/forms/RollForm.vue');
const SavingThrowForm = () => import('/imports/ui/properties/forms/SavingThrowForm.vue');
const SkillForm = () => import('/imports/ui/properties/forms/SkillForm.vue');
const SlotForm = () => import('/imports/ui/properties/forms/SlotForm.vue');
const SlotFillerForm = () => import('/imports/ui/properties/forms/SlotFillerForm.vue');
const SpellListForm = () => import('/imports/ui/properties/forms/SpellListForm.vue');
const SpellForm = () => import('/imports/ui/properties/forms/SpellForm.vue');
const ToggleForm = () => import('/imports/ui/properties/forms/ToggleForm.vue');
const TriggerForm = () => import('/imports/ui/properties/forms/TriggerForm.vue');
import ActionForm from '/imports/ui/properties/forms/ActionForm.vue';
import AdjustmentForm from '/imports/ui/properties/forms/AdjustmentForm.vue';
import AttributeForm from '/imports/ui/properties/forms/AttributeForm.vue';
import BuffForm from '/imports/ui/properties/forms/BuffForm.vue';
import BuffRemoverForm from '/imports/ui/properties/forms/BuffRemoverForm.vue';
import BranchForm from '/imports/ui/properties/forms/BranchForm.vue';
import ClassForm from '/imports/ui/properties/forms/ClassForm.vue';
import ClassLevelForm from '/imports/ui/properties/forms/ClassLevelForm.vue';
import ConstantForm from '/imports/ui/properties/forms/ConstantForm.vue';
import ContainerForm from '/imports/ui/properties/forms/ContainerForm.vue';
import DamageForm from '/imports/ui/properties/forms/DamageForm.vue';
import DamageMultiplierForm from '/imports/ui/properties/forms/DamageMultiplierForm.vue';
import EffectForm from '/imports/ui/properties/forms/EffectForm.vue';
import FeatureForm from '/imports/ui/properties/forms/FeatureForm.vue';
import FolderForm from '/imports/ui/properties/forms/FolderForm.vue';
import ItemForm from '/imports/ui/properties/forms/ItemForm.vue';
import NoteForm from '/imports/ui/properties/forms/NoteForm.vue';
import PointBuyForm from '/imports/ui/properties/forms/PointBuyForm.vue';
import ProficiencyForm from '/imports/ui/properties/forms/ProficiencyForm.vue';
import ReferenceForm from '/imports/ui/properties/forms/ReferenceForm.vue';
import RollForm from '/imports/ui/properties/forms/RollForm.vue';
import SavingThrowForm from '/imports/ui/properties/forms/SavingThrowForm.vue';
import SkillForm from '/imports/ui/properties/forms/SkillForm.vue';
import SlotForm from '/imports/ui/properties/forms/SlotForm.vue';
import SlotFillerForm from '/imports/ui/properties/forms/SlotFillerForm.vue';
import SpellListForm from '/imports/ui/properties/forms/SpellListForm.vue';
import SpellForm from '/imports/ui/properties/forms/SpellForm.vue';
import ToggleForm from '/imports/ui/properties/forms/ToggleForm.vue';
import TriggerForm from '/imports/ui/properties/forms/TriggerForm.vue';
export default {
action: ActionForm,

View File

@@ -13,6 +13,18 @@
center
:calculation="model.maxPrepared"
/>
<property-field
name="Spellcasting ability"
mono
:value="model.ability"
/>
<property-field
name="Spellcasting ability modifier"
large
center
signed
:value="model.abilityMod"
/>
<property-field
name="Spell Save DC"
large

View File

@@ -41,7 +41,7 @@
>
<slot>
<template v-if="value !== undefined">
{{ value }}
{{ valueText }}
</template>
<template v-else-if="calculation !== undefined">
{{ calculationText }}
@@ -117,6 +117,13 @@ export default {
if (!this.calculation) return;
return typeof this.calculation.value === 'string'
},
valueText() {
if (this.signed) {
return numberToSignedString(this.value);
} else {
return this.value;
}
},
calculationText(){
const calculation = this.calculation;
if (!calculation) {

View File

@@ -1,32 +1,32 @@
const ActionViewer = () => import ('/imports/ui/properties/viewers/ActionViewer.vue');
const AdjustmentViewer = () => import ('/imports/ui/properties/viewers/AdjustmentViewer.vue');
const AttributeViewer = () => import ('/imports/ui/properties/viewers/AttributeViewer.vue');
const BuffViewer = () => import ('/imports/ui/properties/viewers/BuffViewer.vue');
const BuffRemoverViewer = () => import ('/imports/ui/properties/viewers/BuffRemoverViewer.vue');
const BranchViewer = () => import ('/imports/ui/properties/viewers/BranchViewer.vue');
const ContainerViewer = () => import ('/imports/ui/properties/viewers/ContainerViewer.vue');
const ClassViewer = () => import ('/imports/ui/properties/viewers/ClassViewer.vue');
const ClassLevelViewer = () => import ('/imports/ui/properties/viewers/ClassLevelViewer.vue');
const ConstantViewer = () => import ('/imports/ui/properties/viewers/ConstantViewer.vue');
const DamageViewer = () => import ('/imports/ui/properties/viewers/DamageViewer.vue');
const DamageMultiplierViewer = () => import ('/imports/ui/properties/viewers/DamageMultiplierViewer.vue');
const EffectViewer = () => import ('/imports/ui/properties/viewers/EffectViewer.vue');
const FeatureViewer = () => import ('/imports/ui/properties/viewers/FeatureViewer.vue');
const FolderViewer = () => import ('/imports/ui/properties/viewers/FolderViewer.vue');
const ItemViewer = () => import ('/imports/ui/properties/viewers/ItemViewer.vue');
const NoteViewer = () => import ('/imports/ui/properties/viewers/NoteViewer.vue');
const PointBuyViewer = () => import ('/imports/ui/properties/viewers/PointBuyViewer.vue');
const ProficiencyViewer = () => import ('/imports/ui/properties/viewers/ProficiencyViewer.vue');
const ReferenceViewer = () => import ('/imports/ui/properties/viewers/ReferenceViewer.vue');
const RollViewer = () => import ('/imports/ui/properties/viewers/RollViewer.vue');
const SkillViewer = () => import ('/imports/ui/properties/viewers/SkillViewer.vue');
const SavingThrowViewer = () => import ('/imports/ui/properties/viewers/SavingThrowViewer.vue');
const SlotViewer = () => import ('/imports/ui/properties/viewers/SlotViewer.vue');
const SlotFillerViewer = () => import ('/imports/ui/properties/viewers/SlotFillerViewer.vue');
const SpellListViewer = () => import ('/imports/ui/properties/viewers/SpellListViewer.vue');
const SpellViewer = () => import ('/imports/ui/properties/viewers/SpellViewer.vue');
const ToggleViewer = () => import ('/imports/ui/properties/viewers/ToggleViewer.vue');
const TriggerViewer = () => import ('/imports/ui/properties/viewers/TriggerViewer.vue');
import ActionViewer from '/imports/ui/properties/viewers/ActionViewer.vue';
import AdjustmentViewer from '/imports/ui/properties/viewers/AdjustmentViewer.vue';
import AttributeViewer from '/imports/ui/properties/viewers/AttributeViewer.vue';
import BuffViewer from '/imports/ui/properties/viewers/BuffViewer.vue';
import BuffRemoverViewer from '/imports/ui/properties/viewers/BuffRemoverViewer.vue';
import BranchViewer from '/imports/ui/properties/viewers/BranchViewer.vue';
import ContainerViewer from '/imports/ui/properties/viewers/ContainerViewer.vue';
import ClassViewer from '/imports/ui/properties/viewers/ClassViewer.vue';
import ClassLevelViewer from '/imports/ui/properties/viewers/ClassLevelViewer.vue';
import ConstantViewer from '/imports/ui/properties/viewers/ConstantViewer.vue';
import DamageViewer from '/imports/ui/properties/viewers/DamageViewer.vue';
import DamageMultiplierViewer from '/imports/ui/properties/viewers/DamageMultiplierViewer.vue';
import EffectViewer from '/imports/ui/properties/viewers/EffectViewer.vue';
import FeatureViewer from '/imports/ui/properties/viewers/FeatureViewer.vue';
import FolderViewer from '/imports/ui/properties/viewers/FolderViewer.vue';
import ItemViewer from '/imports/ui/properties/viewers/ItemViewer.vue';
import NoteViewer from '/imports/ui/properties/viewers/NoteViewer.vue';
import PointBuyViewer from '/imports/ui/properties/viewers/PointBuyViewer.vue';
import ProficiencyViewer from '/imports/ui/properties/viewers/ProficiencyViewer.vue';
import ReferenceViewer from '/imports/ui/properties/viewers/ReferenceViewer.vue';
import RollViewer from '/imports/ui/properties/viewers/RollViewer.vue';
import SkillViewer from '/imports/ui/properties/viewers/SkillViewer.vue';
import SavingThrowViewer from '/imports/ui/properties/viewers/SavingThrowViewer.vue';
import SlotViewer from '/imports/ui/properties/viewers/SlotViewer.vue';
import SlotFillerViewer from '/imports/ui/properties/viewers/SlotFillerViewer.vue';
import SpellListViewer from '/imports/ui/properties/viewers/SpellListViewer.vue';
import SpellViewer from '/imports/ui/properties/viewers/SpellViewer.vue';
import ToggleViewer from '/imports/ui/properties/viewers/ToggleViewer.vue';
import TriggerViewer from '/imports/ui/properties/viewers/TriggerViewer.vue';
export default {
action: ActionViewer,

View File

@@ -12,6 +12,7 @@ const LibraryCollectionToolbar = () => import('/imports/ui/library/LibraryCollec
const CharacterSheetPage = () => import('/imports/ui/pages/CharacterSheetPage.vue');
const CharacterSheetToolbar = () => import('/imports/ui/creature/character/CharacterSheetToolbar.vue');
const CharacterSheetRightDrawer = () => import('/imports/ui/creature/character/CharacterSheetRightDrawer.vue');
const CharacterSheetPrinted = () => import('/imports/ui/creature/character/printedCharacterSheet/CharacterSheetPrinted.vue');
const SignIn = () => import('/imports/ui/pages/SignIn.vue');
const Register = () => import('/imports/ui/pages/Register.vue');
const IconAdmin = () => import('/imports/ui/icons/IconAdmin.vue');
@@ -177,6 +178,16 @@ RouterFactory.configure(router => {
meta: {
title: 'Character Sheet',
},
}, {
name: 'printCharacterSheet',
path: '/print-character/:id',
alias: '/print-character/:id/:urlName',
components: {
default: CharacterSheetPrinted,
},
meta: {
title: 'Print Character Sheet',
},
}, {
path: '/tabletops',
name: 'tabletops',

View File

@@ -13,6 +13,16 @@
:value="!!model.public + ''"
@change="(value, ack) => setSheetPublic({value, ack})"
/>
<smart-select
v-if="docRef.collection === 'libraries'"
label="Who can copy from this library"
:items="[
{text: 'Only people with edit permission', value: 'false'},
{text: 'Anyone with read permission', value: 'true'}
]"
:value="!!model.readersCanCopy + ''"
@change="(value, ack) => setReadersCanCopy({value, ack})"
/>
<text-field
v-if="model.public && docRef.collection === 'libraries'"
readonly
@@ -30,6 +40,7 @@
@change="(value, ack) => getUser({value, ack})"
/>
<v-btn
class="ml-2 mt-2"
:disabled="userFoundState !== 'found'"
@click="updateSharing(userId, 'reader')"
>
@@ -126,6 +137,7 @@
<script lang="js">
import {
setPublic,
setReadersCanCopy,
updateUserSharePermissions
} from '/imports/api/sharing/sharing.js';
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
@@ -157,6 +169,14 @@ export default {
ack(error && error.reason || error);
});
},
setReadersCanCopy({ value, ack }) {
setReadersCanCopy.call({
docRef: this.docRef,
readersCanCopy: value === 'true',
}, (error) => {
ack(error && error.reason || error);
});
},
getUser({ value, ack }) {
this.userSearched = value;
if (!value) {

756
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "dicecloud",
"version": "2.0.38",
"version": "2.0.44",
"description": "Unofficial Online Realtime D&D 5e App",
"license": "GPL-3.0",
"repository": {
@@ -19,57 +19,58 @@
"npm": "6.13.x"
},
"dependencies": {
"@babel/runtime": "^7.18.3",
"@babel/runtime": "^7.20.1",
"@chenfengyuan/vue-countdown": "^1.1.5",
"@tozd/vue-observer-utils": "^0.5.0",
"animejs": "^2.2.0",
"aws-sdk": "^2.1148.0",
"bcrypt": "^5.0.0",
"aws-sdk": "^2.1247.0",
"bcrypt": "^5.1.0",
"chroma-js": "^2.4.2",
"core-js": "^2.6.11",
"css-box-shadow": "^1.0.0-3",
"date-fns": "^1.30.1",
"ddp-rate-limiter-mixin": "^1.1.10",
"discord.js": "^12.5.3",
"dompurify": "^2.3.8",
"dompurify": "^2.4.0",
"ignore": "^5.2.0",
"ignore-styles": "^5.0.1",
"lodash": "^4.17.20",
"marked": "^4.0.16",
"meteor-node-stubs": "^1.2.3",
"marked": "^4.2.1",
"meteor-node-stubs": "^1.2.5",
"minify-css-string": "^1.0.0",
"moo": "^0.5.1",
"moo": "^0.5.2",
"nearley": "^2.19.1",
"ngraph.graph": "^19.1.0",
"ngraph.path": "^1.4.0",
"pretty-bytes": "^6.0.0",
"qrcode": "^1.5.0",
"qrcode": "^1.5.1",
"qrcode.vue": "^1.7.0",
"request": "^2.88.2",
"sharp": "^0.30.4",
"simpl-schema": "^1.12.2",
"sharp": "^0.30.7",
"simpl-schema": "^1.13.1",
"source-map-support": "^0.5.21",
"speakingurl": "^14.0.1",
"styles": "^0.2.1",
"underscore": "^1.13.4",
"underscore": "^1.13.6",
"vue": "2.6.10",
"vue-meteor-tracker": "^2.0.0-beta.5",
"vue-meteor-tracker": "^2.0.0",
"vue-reactive-provide": "^0.3.0",
"vue-router": "^3.5.4",
"vue-router": "^3.6.5",
"vuedraggable": "^2.23.2",
"vuetify": "^2.6.6",
"vuetify": "^2.6.12",
"vuetify-upload-button": "^2.0.2",
"vuex": "^3.1.3"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.39.0",
"@typescript-eslint/parser": "^5.39.0",
"@vue/compiler-dom": "^3.2.40",
"@typescript-eslint/eslint-plugin": "^5.42.0",
"@typescript-eslint/parser": "^5.42.0",
"@vue/compiler-dom": "^3.2.41",
"chai": "^4.3.6",
"eslint": "^7.32.0",
"eslint-plugin-vue": "^7.20.0",
"eslint-plugin-vuetify": "^1.1.0",
"mem": "^6.1.1",
"sass": "^1.52.2",
"sass": "^1.56.0",
"typescript": "^4.8.4"
},
"eslintConfig": {
@@ -120,8 +121,7 @@
"quotes": [
"error",
"single"
],
"vuetify/no-deprecated-classes": "error"
]
}
}
}
}

View File

@@ -6,6 +6,10 @@ Leveling up a class means choosing, or manually adding, class level properties t
The total level of the class can be accessed in calculations using `classVariableName.level`.
## Making your own class
See [Create a Class](/docs/walkthroughs/create-a-class)
---
### Name

View File

@@ -1,6 +1,6 @@
# Skills
Skills represent things the creature can be proficient in. Skills can have their values or behavior modifier by [effects](/docs/property/efffect), and their proficiencies modified by [proficiencies](/docs/property/proficiency).
Skills represent things the creature can be proficient in. Skills can have their values or behavior modifier by [effects](/docs/property/effect), and their proficiencies modified by [proficiencies](/docs/property/proficiency).
---

View File

@@ -18,6 +18,10 @@ Allows [inline calculations](/docs/inline-calculations).
A [computed field](/docs/computed-fields) that determines how many spells can be considered ready to cast in this spell list.
### Spell casting ability
The spellcasting ablity for this spell list. The variable name of the ability can be accessed using `#spellList.ability` and the ability modifier with `#spellList.abilityMod`. Setting this field will automatically update Spell save DC and Attack roll bonus if they aren't set manually.
### Spell save DC
A [computed field](/docs/computed-fields) that determines the DC of saving throws in this spell list. Spells can access the DC of their spell list using `#spellList.dc`

View File

@@ -0,0 +1,47 @@
# Create a Class
This is a guide on creating a custom class in a character sheet. If possible, it is always faster to use an existing library that contains the class you want to use. Before continuing, check the #libraries channel of the [official discord](https://discord.gg/qEvdfeB) to see if a library exists with the class you are creating.
This guide assumes you are using the ruleset provided in the [5e System Reference Document library](/library/qkv8aptJH2fCXARcJ). If you are using a different ruleset for your character, there may be some discrepancies.
## Adding the class property
On the build tab of your character, in the card labeled **Slots**, expand the rulset, then click the slot where you would like to place the custom class, if it is your starting class in an SRD character, this would be the Class slot. Be sure to click the name of the slot, not the **+** button.
![Screenshot of Build Tab > Slots > Ruleset > Class](/images/docs/walkthroughs/create-a-class-1.png)
This opens the slot in detail view, showing you how the slot expected to be filled from a library, instead of filling the slot, we will be manually adding a class to the slot that we create ourselves.
Click the **Edit** button in the top right of the slot detail dialog.
![Screenshot of slot detail dialog](/images/docs/walkthroughs/create-a-class-2.png)
Expand the children of the class slot, and click the plus button to add a child property.
![Screenshot of adding a child property](/images/docs/walkthroughs/create-a-class-3.png)
This brings up the create a property dialog, we are creating a class, so select the class property type.
![Screenshot of choosing a class property](/images/docs/walkthroughs/create-a-class-4.png)
Now that we have selected the class property type, the create tab is selected where we can enter the details of our class, fill in the form and click **Create**.
![Screenshot of the class form](/images/docs/walkthroughs/create-a-class-5.png)
Now that our custom class is created, we can close the class slot dialog.
On the Build tab, in the card with the title **Level**, you will see your new class, with a button to **Level Up**, clicking the level up button would usually search your libraries for class levels that match the variable name of the class, however, since it's a custom class, it will probably not find any levels.
Instead, as we did with the class slot, click on the class name to bring up the class detail dialog, click **Edit**, expand children and click the **+** button to add a child to the class. Here we will add all of the things our class gives the character.
Add an [Effect](/docs/property/effect) which targets `hitPoints` to add the starting hitpoints of the class. Add a [proficiencies](/docs/property/proficiency) for all the skill and saving throw proficiencies the class gives. Add [skills](/docs/property/skill) for all the tool and weapon proficiencies of the class, making sure to set the base proficiency of those skills to proficient. Add any text [features](/docs/property/feature) the class gives you, along with [actions](/docs/property/action) which may be children of those features, or direct children of the class.
Once you have added Everything the class gives you, it's time to add class levels. As a child of the class, add a [class level](/docs/property/class-level) property. Set the level to 1 and the name and variable name to match the variable name of the class.
Once the class level is created, open the class level and edit it. Use the **+** button in the children of the class level to add all the properties the class level gives your character.
Repeat this for every level of the class until your character is at the correct level.
You can use a separate character with levels in a class that is available in your libraries as an example of what properties you may want to add to your class and class levels.
![Example wizard class](/images/docs/walkthroughs/create-a-class-6.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB