Compare commits
27 Commits
2.0-beta.4
...
2.0-beta.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c55d572134 | ||
|
|
0a2b60990e | ||
|
|
a437ff5aef | ||
|
|
3d31d62860 | ||
|
|
8377231254 | ||
|
|
1ec29365cb | ||
|
|
60b21c1901 | ||
|
|
03f87b0afa | ||
|
|
48291d2c8f | ||
|
|
1cedf55fbf | ||
|
|
bed4d4b162 | ||
|
|
a1d992ec8d | ||
|
|
008ef62517 | ||
|
|
c436309ba8 | ||
|
|
0bfdb73b47 | ||
|
|
a462cc5ca2 | ||
|
|
5d57a74667 | ||
|
|
21b0029df7 | ||
|
|
c0ccafa787 | ||
|
|
d63ad9ea8f | ||
|
|
8f56a60fb1 | ||
|
|
358ae46627 | ||
|
|
0b1db3c40c | ||
|
|
0ad7e659d2 | ||
|
|
58c3875dc7 | ||
|
|
84f506f1fe | ||
|
|
d0a3ccc76a |
@@ -11,7 +11,7 @@ accounts-google@1.4.0
|
|||||||
email@2.2.1
|
email@2.2.1
|
||||||
meteor-base@1.5.1
|
meteor-base@1.5.1
|
||||||
mobile-experience@1.1.0
|
mobile-experience@1.1.0
|
||||||
mongo@1.16.0-beta280.7
|
mongo@1.16.0
|
||||||
session@1.2.0
|
session@1.2.0
|
||||||
tracker@1.2.0
|
tracker@1.2.0
|
||||||
logging@1.3.1
|
logging@1.3.1
|
||||||
@@ -48,4 +48,4 @@ simple:rest-bearer-token-parser
|
|||||||
simple:rest-json-error-handler
|
simple:rest-json-error-handler
|
||||||
littledata:synced-cron
|
littledata:synced-cron
|
||||||
mdg:meteor-apm-agent
|
mdg:meteor-apm-agent
|
||||||
typescript
|
typescript@4.5.4
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
METEOR@2.8-beta.7
|
METEOR@2.8.0
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ coffeescript@2.4.1
|
|||||||
coffeescript-compiler@2.4.1
|
coffeescript-compiler@2.4.1
|
||||||
dburles:mongo-collection-instances@0.3.6
|
dburles:mongo-collection-instances@0.3.6
|
||||||
ddp@1.4.0
|
ddp@1.4.0
|
||||||
ddp-client@2.5.0
|
ddp-client@2.6.0
|
||||||
ddp-common@1.4.0
|
ddp-common@1.4.0
|
||||||
ddp-rate-limiter@1.1.0
|
ddp-rate-limiter@1.1.0
|
||||||
ddp-server@2.5.0
|
ddp-server@2.6.0
|
||||||
diff-sequence@1.1.1
|
diff-sequence@1.1.1
|
||||||
dynamic-import@0.7.2
|
dynamic-import@0.7.2
|
||||||
ecmascript@0.16.2
|
ecmascript@0.16.2
|
||||||
@@ -57,7 +57,7 @@ localstorage@1.2.0
|
|||||||
logging@1.3.1
|
logging@1.3.1
|
||||||
mdg:meteor-apm-agent@3.5.1
|
mdg:meteor-apm-agent@3.5.1
|
||||||
mdg:validated-method@1.2.0
|
mdg:validated-method@1.2.0
|
||||||
meteor@1.10.1-beta280.7
|
meteor@1.10.1
|
||||||
meteor-base@1.5.1
|
meteor-base@1.5.1
|
||||||
meteortesting:browser-tests@1.3.5
|
meteortesting:browser-tests@1.3.5
|
||||||
meteortesting:mocha@2.0.3
|
meteortesting:mocha@2.0.3
|
||||||
@@ -65,18 +65,18 @@ meteortesting:mocha-core@8.1.2
|
|||||||
mikowals:batch-insert@1.3.0
|
mikowals:batch-insert@1.3.0
|
||||||
minifier-css@1.6.1
|
minifier-css@1.6.1
|
||||||
minifier-js@2.7.5
|
minifier-js@2.7.5
|
||||||
minimongo@1.9.0-beta280.7
|
minimongo@1.9.0
|
||||||
mobile-experience@1.1.0
|
mobile-experience@1.1.0
|
||||||
mobile-status-bar@1.1.0
|
mobile-status-bar@1.1.0
|
||||||
modern-browsers@0.1.8
|
modern-browsers@0.1.8
|
||||||
modules@0.19.0-beta280.7
|
modules@0.19.0
|
||||||
modules-runtime@0.13.0
|
modules-runtime@0.13.0
|
||||||
mongo@1.16.0-beta280.7
|
mongo@1.16.0
|
||||||
mongo-decimal@0.1.3
|
mongo-decimal@0.1.3
|
||||||
mongo-dev-server@1.1.0
|
mongo-dev-server@1.1.0
|
||||||
mongo-id@1.0.8
|
mongo-id@1.0.8
|
||||||
mongo-livedata@1.0.12
|
mongo-livedata@1.0.12
|
||||||
npm-mongo@4.9.0-beta280.7
|
npm-mongo@4.9.0
|
||||||
oauth@2.1.2
|
oauth@2.1.2
|
||||||
oauth2@1.3.1
|
oauth2@1.3.1
|
||||||
ordered-dict@1.1.0
|
ordered-dict@1.1.0
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ let CreatureSettingsSchema = new SimpleSchema({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
|
//hide rest buttons
|
||||||
|
hideRestButtons: {
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
// Swap around the modifier and stat
|
// Swap around the modifier and stat
|
||||||
swapStatAndModifier: {
|
swapStatAndModifier: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const restCreature = new ValidatedMethod({
|
|||||||
applyTriggers(afterTriggers, null, actionContext);
|
applyTriggers(afterTriggers, null, actionContext);
|
||||||
|
|
||||||
// Insert log
|
// Insert log
|
||||||
actionContext.writeLog();
|
actionContext.writeLog();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,88 +57,124 @@ function doRestWork(restType, actionContext) {
|
|||||||
const creatureId = actionContext.creature._id;
|
const creatureId = actionContext.creature._id;
|
||||||
// Long rests reset short rest properties as well
|
// Long rests reset short rest properties as well
|
||||||
let resetFilter;
|
let resetFilter;
|
||||||
if (restType === 'shortRest'){
|
if (restType === 'shortRest') {
|
||||||
resetFilter = 'shortRest'
|
resetFilter = 'shortRest'
|
||||||
} else {
|
} else {
|
||||||
resetFilter = {$in: ['shortRest', 'longRest']}
|
resetFilter = { $in: ['shortRest', 'longRest'] }
|
||||||
}
|
}
|
||||||
|
resetProperties(creatureId, resetFilter, actionContext);
|
||||||
|
|
||||||
|
// Reset half hit dice on a long rest, starting with the highest dice
|
||||||
|
if (restType === 'longRest') {
|
||||||
|
resetHitDice(creatureId, actionContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetProperties(creatureId, resetFilter, actionContext) {
|
||||||
// Only apply to active properties
|
// Only apply to active properties
|
||||||
let filter = {
|
const filter = {
|
||||||
'ancestors.id': creatureId,
|
'ancestors.id': creatureId,
|
||||||
reset: resetFilter,
|
reset: resetFilter,
|
||||||
removed: { $ne: true },
|
removed: { $ne: true },
|
||||||
inactive: { $ne: true },
|
inactive: { $ne: true },
|
||||||
};
|
};
|
||||||
// update all attribute's damage
|
// update all attribute's damage
|
||||||
filter.type = 'attribute';
|
const attributeFilter = {
|
||||||
CreatureProperties.update(filter, {
|
...filter,
|
||||||
|
type: 'attribute',
|
||||||
|
damage: { $ne: 0 },
|
||||||
|
}
|
||||||
|
CreatureProperties.find(attributeFilter, {
|
||||||
|
fields: { name: 1, damage: 1 }
|
||||||
|
}).forEach(prop => {
|
||||||
|
actionContext.addLog({
|
||||||
|
name: prop.name,
|
||||||
|
value: prop.damage >= 0 ? `Restored ${prop.damage}` : `Removed ${-prop.damage}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
CreatureProperties.update(attributeFilter, {
|
||||||
$set: {
|
$set: {
|
||||||
damage: 0,
|
damage: 0,
|
||||||
dirty: true,
|
dirty: true,
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
selector: {type: 'attribute'},
|
selector: { type: 'attribute' },
|
||||||
multi: true,
|
multi: true,
|
||||||
});
|
});
|
||||||
// Update all action-like properties' usesUsed
|
// Update all action-like properties' usesUsed
|
||||||
filter.type = {$in: [
|
const actionFilter = {
|
||||||
'action',
|
...filter,
|
||||||
'attack',
|
type: {
|
||||||
'spell'
|
$in: ['action', 'spell']
|
||||||
]};
|
},
|
||||||
CreatureProperties.update(filter, {
|
usesUsed: { $ne: 0 },
|
||||||
|
};
|
||||||
|
CreatureProperties.find(actionFilter, {
|
||||||
|
fields: { name: 1, usesUsed: 1 }
|
||||||
|
}).forEach(prop => {
|
||||||
|
actionContext.addLog({
|
||||||
|
name: prop.name,
|
||||||
|
value: prop.usesUsed >= 0 ? `Restored ${prop.usesUsed} uses` : `Removed ${-prop.usesUsed} uses`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
CreatureProperties.update(actionFilter, {
|
||||||
$set: {
|
$set: {
|
||||||
usesUsed: 0,
|
usesUsed: 0,
|
||||||
dirty: true,
|
dirty: true,
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
selector: {type: 'action'},
|
selector: { type: 'action' },
|
||||||
multi: true,
|
multi: true,
|
||||||
});
|
});
|
||||||
// Reset half hit dice on a long rest, starting with the highest dice
|
}
|
||||||
if (restType === 'longRest'){
|
|
||||||
let hitDice = CreatureProperties.find({
|
function resetHitDice(creatureId, actionContext) {
|
||||||
'ancestors.id': creatureId,
|
let hitDice = CreatureProperties.find({
|
||||||
type: 'attribute',
|
'ancestors.id': creatureId,
|
||||||
attributeType: 'hitDice',
|
type: 'attribute',
|
||||||
removed: {$ne: true},
|
attributeType: 'hitDice',
|
||||||
inactive: {$ne: true},
|
removed: { $ne: true },
|
||||||
}, {
|
inactive: { $ne: true },
|
||||||
fields: {
|
}, {
|
||||||
hitDiceSize: 1,
|
fields: {
|
||||||
damage: 1,
|
name: 1,
|
||||||
total: 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
|
selector: { type: 'attribute' },
|
||||||
let collator = new Intl.Collator('en', {
|
|
||||||
numeric: true, sensitivity: 'base'
|
|
||||||
});
|
});
|
||||||
// Get the hit dice in decending order of hitDiceSize
|
});
|
||||||
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
|
|
||||||
hitDice.sort(compare);
|
|
||||||
// Get the total number of hit dice that can be recovered this rest
|
|
||||||
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
|
|
||||||
let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
|
|
||||||
let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1);
|
|
||||||
// recover each hit dice in turn until the recoverable amount is used up
|
|
||||||
let amountToRecover, resultingDamage;
|
|
||||||
hitDice.forEach(hd => {
|
|
||||||
if (!recoverableHd) return;
|
|
||||||
amountToRecover = Math.min(recoverableHd, hd.damage || 0);
|
|
||||||
if (!amountToRecover) return;
|
|
||||||
recoverableHd -= amountToRecover;
|
|
||||||
resultingDamage = hd.damage - amountToRecover;
|
|
||||||
CreatureProperties.update(hd._id, {
|
|
||||||
$set: {
|
|
||||||
damage: resultingDamage,
|
|
||||||
dirty: true,
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
selector: {type: 'attribute'},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default restCreature;
|
export default restCreature;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/met
|
|||||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||||
|
import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js';
|
||||||
|
|
||||||
export default function applyAction(node, actionContext) {
|
export default function applyAction(node, actionContext) {
|
||||||
applyNodeTriggers(node, 'before', actionContext);
|
applyNodeTriggers(node, 'before', actionContext);
|
||||||
@@ -16,7 +17,7 @@ export default function applyAction(node, actionContext) {
|
|||||||
|
|
||||||
// Log the name and summary
|
// Log the name and summary
|
||||||
let content = { name: prop.name };
|
let content = { name: prop.name };
|
||||||
if (prop.summary?.text){
|
if (prop.summary?.text) {
|
||||||
recalculateInlineCalculations(prop.summary, actionContext);
|
recalculateInlineCalculations(prop.summary, actionContext);
|
||||||
content.value = prop.summary.value;
|
content.value = prop.summary.value;
|
||||||
}
|
}
|
||||||
@@ -29,24 +30,27 @@ export default function applyAction(node, actionContext) {
|
|||||||
const attack = prop.attackRoll || prop.attackRollBonus;
|
const attack = prop.attackRoll || prop.attackRollBonus;
|
||||||
|
|
||||||
// Attack if there is an attack roll
|
// Attack if there is an attack roll
|
||||||
if (attack && attack.calculation){
|
if (attack && attack.calculation) {
|
||||||
if (targets.length){
|
if (targets.length) {
|
||||||
targets.forEach(target => {
|
targets.forEach(target => {
|
||||||
applyAttackToTarget({attack, target, actionContext});
|
applyAttackToTarget({ attack, target, actionContext });
|
||||||
// Apply the children, but only to the current target
|
// Apply the children, but only to the current target
|
||||||
actionContext.targets = [target];
|
actionContext.targets = [target];
|
||||||
applyChildren(node, actionContext);
|
applyChildren(node, actionContext);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
applyAttackWithoutTarget({attack, actionContext});
|
applyAttackWithoutTarget({ attack, actionContext });
|
||||||
applyChildren(node, actionContext);
|
applyChildren(node, actionContext);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
applyChildren(node, actionContext);
|
applyChildren(node, actionContext);
|
||||||
}
|
}
|
||||||
|
if (prop.actionType === 'event' && prop.variableName) {
|
||||||
|
resetProperties(actionContext.creature._id, prop.variableName, actionContext);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyAttackWithoutTarget({attack, actionContext}){
|
function applyAttackWithoutTarget({ attack, actionContext }) {
|
||||||
delete actionContext.scope['$attackHit'];
|
delete actionContext.scope['$attackHit'];
|
||||||
delete actionContext.scope['$attackMiss'];
|
delete actionContext.scope['$attackMiss'];
|
||||||
delete actionContext.scope['$criticalHit'];
|
delete actionContext.scope['$criticalHit'];
|
||||||
@@ -62,16 +66,16 @@ function applyAttackWithoutTarget({attack, actionContext}){
|
|||||||
criticalMiss,
|
criticalMiss,
|
||||||
} = rollAttack(attack, scope);
|
} = rollAttack(attack, scope);
|
||||||
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
|
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
|
||||||
if (scope['$attackAdvantage'] === 1){
|
if (scope['$attackAdvantage'] === 1) {
|
||||||
name += ' (Advantage)';
|
name += ' (Advantage)';
|
||||||
} else if(scope['$attackAdvantage'] === -1){
|
} else if (scope['$attackAdvantage'] === -1) {
|
||||||
name += ' (Disadvantage)';
|
name += ' (Disadvantage)';
|
||||||
}
|
}
|
||||||
if (!criticalMiss){
|
if (!criticalMiss) {
|
||||||
scope['$attackHit'] = {value: true}
|
scope['$attackHit'] = { value: true }
|
||||||
}
|
}
|
||||||
if (!criticalHit){
|
if (!criticalHit) {
|
||||||
scope['$attackMiss'] = {value: true};
|
scope['$attackMiss'] = { value: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
actionContext.addLog({
|
actionContext.addLog({
|
||||||
@@ -81,7 +85,7 @@ function applyAttackWithoutTarget({attack, actionContext}){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyAttackToTarget({attack, target, actionContext}){
|
function applyAttackToTarget({ attack, target, actionContext }) {
|
||||||
const scope = actionContext.scope;
|
const scope = actionContext.scope;
|
||||||
delete scope['$attackHit'];
|
delete scope['$attackHit'];
|
||||||
delete scope['$attackMiss'];
|
delete scope['$attackMiss'];
|
||||||
@@ -99,15 +103,15 @@ function applyAttackToTarget({attack, target, actionContext}){
|
|||||||
criticalMiss,
|
criticalMiss,
|
||||||
} = rollAttack(attack, scope);
|
} = rollAttack(attack, scope);
|
||||||
|
|
||||||
if (target.variables.armor){
|
if (target.variables.armor) {
|
||||||
const armor = target.variables.armor.value;
|
const armor = target.variables.armor.value;
|
||||||
|
|
||||||
let name = criticalHit ? 'Critical Hit!' :
|
let name = criticalHit ? 'Critical Hit!' :
|
||||||
criticalMiss ? 'Critical Miss!' :
|
criticalMiss ? 'Critical Miss!' :
|
||||||
result > armor ? 'Hit!' : 'Miss!';
|
result > armor ? 'Hit!' : 'Miss!';
|
||||||
if (scope['$attackAdvantage'] === 1){
|
if (scope['$attackAdvantage'] === 1) {
|
||||||
name += ' (Advantage)';
|
name += ' (Advantage)';
|
||||||
} else if(scope['$attackAdvantage'] === -1){
|
} else if (scope['$attackAdvantage'] === -1) {
|
||||||
name += ' (Disadvantage)';
|
name += ' (Disadvantage)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,15 +120,15 @@ function applyAttackToTarget({attack, target, actionContext}){
|
|||||||
value: `${resultPrefix}\n**${result}**`,
|
value: `${resultPrefix}\n**${result}**`,
|
||||||
inline: true,
|
inline: true,
|
||||||
});
|
});
|
||||||
if (criticalMiss || result < armor){
|
if (criticalMiss || result < armor) {
|
||||||
scope['$attackMiss'] = {value: true};
|
scope['$attackMiss'] = { value: true };
|
||||||
} else {
|
} else {
|
||||||
scope['$attackHit'] = {value: true};
|
scope['$attackHit'] = { value: true };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
actionContext.addLog({
|
actionContext.addLog({
|
||||||
name: 'Error',
|
name: 'Error',
|
||||||
value:'Target has no `armor`',
|
value: 'Target has no `armor`',
|
||||||
});
|
});
|
||||||
actionContext.addLog({
|
actionContext.addLog({
|
||||||
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
|
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);
|
const rollModifierText = numberToSignedString(attack.value, true);
|
||||||
let value, resultPrefix;
|
let value, resultPrefix;
|
||||||
if (scope['$attackAdvantage'] === 1){
|
if (scope['$attackAdvantage'] === 1) {
|
||||||
const [a, b] = rollDice(2, 20);
|
const [a, b] = rollDice(2, 20);
|
||||||
if (a >= b) {
|
if (a >= b) {
|
||||||
value = a;
|
value = a;
|
||||||
@@ -146,7 +150,7 @@ function rollAttack(attack, scope){
|
|||||||
value = b;
|
value = b;
|
||||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
||||||
}
|
}
|
||||||
} else if (scope['$attackAdvantage'] === -1){
|
} else if (scope['$attackAdvantage'] === -1) {
|
||||||
const [a, b] = rollDice(2, 20);
|
const [a, b] = rollDice(2, 20);
|
||||||
if (a <= b) {
|
if (a <= b) {
|
||||||
value = a;
|
value = a;
|
||||||
@@ -159,25 +163,25 @@ function rollAttack(attack, scope){
|
|||||||
value = rollDice(1, 20)[0];
|
value = rollDice(1, 20)[0];
|
||||||
resultPrefix = `1d20 [${value}] ${rollModifierText}`
|
resultPrefix = `1d20 [${value}] ${rollModifierText}`
|
||||||
}
|
}
|
||||||
scope['$attackRoll'] = {value};
|
scope['$attackRoll'] = { value };
|
||||||
const result = value + attack.value;
|
const result = value + attack.value;
|
||||||
const {criticalHit, criticalMiss} = applyCrits(value, scope);
|
const { criticalHit, criticalMiss } = applyCrits(value, scope);
|
||||||
return {resultPrefix, result, value, criticalHit, criticalMiss};
|
return { resultPrefix, result, value, criticalHit, criticalMiss };
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyCrits(value, scope){
|
function applyCrits(value, scope) {
|
||||||
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
|
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
|
||||||
let criticalHit = value >= criticalHitTarget;
|
let criticalHit = value >= criticalHitTarget;
|
||||||
let criticalMiss;
|
let criticalMiss;
|
||||||
if (criticalHit){
|
if (criticalHit) {
|
||||||
scope['$criticalHit'] = {value: true};
|
scope['$criticalHit'] = { value: true };
|
||||||
} else {
|
} else {
|
||||||
criticalMiss = value === 1;
|
criticalMiss = value === 1;
|
||||||
if (criticalMiss){
|
if (criticalMiss) {
|
||||||
scope['$criticalMiss'] = {value: true};
|
scope['$criticalMiss'] = { value: true };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {criticalHit, criticalMiss};
|
return { criticalHit, criticalMiss };
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyChildren(node, actionContext) {
|
function applyChildren(node, actionContext) {
|
||||||
@@ -185,9 +189,9 @@ function applyChildren(node, actionContext) {
|
|||||||
node.children.forEach(child => applyProperty(child, actionContext));
|
node.children.forEach(child => applyProperty(child, actionContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
function spendResources(prop, actionContext){
|
function spendResources(prop, actionContext) {
|
||||||
// Check Uses
|
// Check Uses
|
||||||
if (prop.usesLeft <= 0){
|
if (prop.usesLeft <= 0) {
|
||||||
if (!prop.silent) actionContext.addLog({
|
if (!prop.silent) actionContext.addLog({
|
||||||
name: 'Error',
|
name: 'Error',
|
||||||
value: `${prop.name || 'action'} does not have enough uses left`,
|
value: `${prop.name || 'action'} does not have enough uses left`,
|
||||||
@@ -195,7 +199,7 @@ function spendResources(prop, actionContext){
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Resources
|
// Resources
|
||||||
if (prop.insufficientResources){
|
if (prop.insufficientResources) {
|
||||||
if (!prop.silent) actionContext.addLog({
|
if (!prop.silent) actionContext.addLog({
|
||||||
name: 'Error',
|
name: 'Error',
|
||||||
value: 'This creature doesn\'t have sufficient resources to perform this action',
|
value: 'This creature doesn\'t have sufficient resources to perform this action',
|
||||||
@@ -209,14 +213,14 @@ function spendResources(prop, actionContext){
|
|||||||
try {
|
try {
|
||||||
prop.resources.itemsConsumed.forEach(itemConsumed => {
|
prop.resources.itemsConsumed.forEach(itemConsumed => {
|
||||||
recalculateCalculation(itemConsumed.quantity, actionContext);
|
recalculateCalculation(itemConsumed.quantity, actionContext);
|
||||||
if (!itemConsumed.itemId){
|
if (!itemConsumed.itemId) {
|
||||||
throw 'No ammo was selected for this prop';
|
throw 'No ammo was selected for this prop';
|
||||||
}
|
}
|
||||||
let item = CreatureProperties.findOne(itemConsumed.itemId);
|
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';
|
throw 'The prop\'s ammo was not found on the creature';
|
||||||
}
|
}
|
||||||
if (!item.equipped){
|
if (!item.equipped) {
|
||||||
throw 'The selected ammo is not equipped';
|
throw 'The selected ammo is not equipped';
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -229,16 +233,16 @@ function spendResources(prop, actionContext){
|
|||||||
value: itemConsumed.quantity.value,
|
value: itemConsumed.quantity.value,
|
||||||
});
|
});
|
||||||
let logName = item.name;
|
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;
|
logName = item.plural || logName;
|
||||||
}
|
}
|
||||||
if (itemConsumed.quantity.value > 0){
|
if (itemConsumed.quantity.value > 0) {
|
||||||
spendLog.push(logName + ': ' + itemConsumed.quantity.value);
|
spendLog.push(logName + ': ' + itemConsumed.quantity.value);
|
||||||
} else if (itemConsumed.quantity.value < 0){
|
} else if (itemConsumed.quantity.value < 0) {
|
||||||
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
|
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e){
|
} catch (e) {
|
||||||
actionContext.addLog({
|
actionContext.addLog({
|
||||||
name: 'Error',
|
name: 'Error',
|
||||||
value: e,
|
value: e,
|
||||||
@@ -251,9 +255,9 @@ function spendResources(prop, actionContext){
|
|||||||
itemQuantityAdjustments.forEach(adjustQuantityWork);
|
itemQuantityAdjustments.forEach(adjustQuantityWork);
|
||||||
|
|
||||||
// Use uses
|
// Use uses
|
||||||
if (prop.usesLeft){
|
if (prop.usesLeft) {
|
||||||
CreatureProperties.update(prop._id, {
|
CreatureProperties.update(prop._id, {
|
||||||
$inc: {usesUsed: 1}
|
$inc: { usesUsed: 1 }
|
||||||
}, {
|
}, {
|
||||||
selector: prop
|
selector: prop
|
||||||
});
|
});
|
||||||
@@ -270,7 +274,7 @@ function spendResources(prop, actionContext){
|
|||||||
|
|
||||||
if (!attConsumed.quantity?.value) return;
|
if (!attConsumed.quantity?.value) return;
|
||||||
let stat = actionContext.scope[attConsumed.variableName];
|
let stat = actionContext.scope[attConsumed.variableName];
|
||||||
if (!stat){
|
if (!stat) {
|
||||||
spendLog.push(stat.name + ': ' + ' not found');
|
spendLog.push(stat.name + ': ' + ' not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -280,9 +284,9 @@ function spendResources(prop, actionContext){
|
|||||||
value: attConsumed.quantity.value,
|
value: attConsumed.quantity.value,
|
||||||
actionContext,
|
actionContext,
|
||||||
});
|
});
|
||||||
if (attConsumed.quantity.value > 0){
|
if (attConsumed.quantity.value > 0) {
|
||||||
spendLog.push(stat.name + ': ' + attConsumed.quantity.value);
|
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);
|
gainLog.push(stat.name + ': ' + -attConsumed.quantity.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { some, intersection, difference, remove, includes } from 'lodash';
|
import { some, intersection, difference, remove, includes } from 'lodash';
|
||||||
import applyProperty from '../applyProperty.js';
|
import applyProperty from '../applyProperty.js';
|
||||||
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
|
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
|
||||||
import resolve, { Context, toString } from '/imports/parser/resolve.js';
|
import resolve, { Context, toString } from '/imports/parser/resolve.js';
|
||||||
import logErrors from './shared/logErrors.js';
|
import logErrors from './shared/logErrors.js';
|
||||||
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
|
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
|
||||||
@@ -10,9 +10,9 @@ import {
|
|||||||
} from '/imports/api/engine/loadCreatures.js';
|
} from '/imports/api/engine/loadCreatures.js';
|
||||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.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);
|
applyNodeTriggers(node, 'before', actionContext);
|
||||||
const applyChildren = function(){
|
const applyChildren = function () {
|
||||||
applyNodeTriggers(node, 'after', actionContext);
|
applyNodeTriggers(node, 'after', actionContext);
|
||||||
node.children.forEach(child => applyProperty(child, actionContext));
|
node.children.forEach(child => applyProperty(child, actionContext));
|
||||||
};
|
};
|
||||||
@@ -28,10 +28,10 @@ export default function applyDamage(node, actionContext){
|
|||||||
// Determine if the hit is critical
|
// Determine if the hit is critical
|
||||||
let criticalHit = scope['$criticalHit']?.value &&
|
let criticalHit = scope['$criticalHit']?.value &&
|
||||||
prop.damageType !== 'healing' // Can't critically heal
|
prop.damageType !== 'healing' // Can't critically heal
|
||||||
;
|
;
|
||||||
// Double the damage rolls if the hit is critical
|
// Double the damage rolls if the hit is critical
|
||||||
let context = new Context({
|
let context = new Context({
|
||||||
options: {doubleRolls: criticalHit},
|
options: { doubleRolls: criticalHit },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gather all the lines we need to log into an array
|
// 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
|
// roll the dice only and store that string
|
||||||
applyEffectsToCalculationParseNode(prop.amount, actionContext.log);
|
applyEffectsToCalculationParseNode(prop.amount, actionContext.log);
|
||||||
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
|
const { result: rolled } = resolve('roll', prop.amount.parseNode, scope, context);
|
||||||
if (rolled.parseType !== 'constant'){
|
if (rolled.parseType !== 'constant') {
|
||||||
logValue.push(toString(rolled));
|
logValue.push(toString(rolled));
|
||||||
}
|
}
|
||||||
logErrors(context.errors, actionContext);
|
logErrors(context.errors, actionContext);
|
||||||
@@ -50,13 +50,13 @@ export default function applyDamage(node, actionContext){
|
|||||||
context.errors = [];
|
context.errors = [];
|
||||||
|
|
||||||
// Resolve the roll to a final value
|
// 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);
|
logErrors(context.errors, actionContext);
|
||||||
|
|
||||||
// Store the result
|
// Store the result
|
||||||
if (reduced.parseType === 'constant'){
|
if (reduced.parseType === 'constant') {
|
||||||
prop.amount.value = reduced.value;
|
prop.amount.value = reduced.value;
|
||||||
} else if (reduced.parseType === 'error'){
|
} else if (reduced.parseType === 'error') {
|
||||||
prop.amount.value = null;
|
prop.amount.value = null;
|
||||||
} else {
|
} else {
|
||||||
prop.amount.value = toString(reduced);
|
prop.amount.value = toString(reduced);
|
||||||
@@ -64,7 +64,7 @@ export default function applyDamage(node, actionContext){
|
|||||||
let damage = +reduced.value;
|
let damage = +reduced.value;
|
||||||
|
|
||||||
// If we didn't end up with a constant of finite amount, give up
|
// 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();
|
return applyChildren();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ export default function applyDamage(node, actionContext){
|
|||||||
// Memoise the damage suffix for the log
|
// Memoise the damage suffix for the log
|
||||||
let suffix = (criticalHit ? ' critical ' : ' ') +
|
let suffix = (criticalHit ? ' critical ' : ' ') +
|
||||||
prop.damageType +
|
prop.damageType +
|
||||||
(prop.damageType !== 'healing' ? ' damage ': '');
|
(prop.damageType !== 'healing' ? ' damage ' : '');
|
||||||
|
|
||||||
if (damageTargets && damageTargets.length) {
|
if (damageTargets && damageTargets.length) {
|
||||||
// Iterate through all the targets
|
// Iterate through all the targets
|
||||||
@@ -107,7 +107,7 @@ export default function applyDamage(node, actionContext){
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Log the damage done
|
// 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
|
// Target is same as self, log damage as such
|
||||||
logValue.push(`**${damageDealt}** ${suffix} to self`);
|
logValue.push(`**${damageDealt}** ${suffix} to self`);
|
||||||
} else {
|
} else {
|
||||||
@@ -136,33 +136,33 @@ export default function applyDamage(node, actionContext){
|
|||||||
return applyChildren();
|
return applyChildren();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyDamageMultipliers({target, damage, damageProp, logValue}){
|
function applyDamageMultipliers({ target, damage, damageProp, logValue }) {
|
||||||
const damageType = damageProp?.damageType;
|
const damageType = damageProp?.damageType;
|
||||||
if (!damageType) return damage;
|
if (!damageType) return damage;
|
||||||
|
|
||||||
const multiplier = target?.variables?.[damageType];
|
const multiplier = target?.variables?.[damageType];
|
||||||
if (!multiplier) return damage;
|
if (!multiplier) return damage;
|
||||||
|
|
||||||
const damageTypeText = damageType == 'healing' ? 'healing': `${damageType} damage`;
|
const damageTypeText = damageType == 'healing' ? 'healing' : `${damageType} damage`;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
multiplier.immunity &&
|
multiplier.immunity &&
|
||||||
some(multiplier.immunities, multiplierAppliesTo(damageProp, 'immunity'))
|
some(multiplier.immunities, multiplierAppliesTo(damageProp, 'immunity'))
|
||||||
){
|
) {
|
||||||
logValue.push(`Immune to ${damageTypeText}`);
|
logValue.push(`Immune to ${damageTypeText}`);
|
||||||
return 0;
|
return 0;
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (
|
||||||
multiplier.resistance &&
|
multiplier.resistance &&
|
||||||
some(multiplier.resistances, multiplierAppliesTo(damageProp, 'resistance'))
|
some(multiplier.resistances, multiplierAppliesTo(damageProp, 'resistance'))
|
||||||
){
|
) {
|
||||||
logValue.push(`Resistant to ${damageTypeText}`);
|
logValue.push(`Resistant to ${damageTypeText}`);
|
||||||
damage = Math.floor(damage / 2);
|
damage = Math.floor(damage / 2);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
multiplier.vulnerability &&
|
multiplier.vulnerability &&
|
||||||
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp, 'vulnerability'))
|
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp, 'vulnerability'))
|
||||||
){
|
) {
|
||||||
logValue.push(`Vulnerable to ${damageTypeText}`);
|
logValue.push(`Vulnerable to ${damageTypeText}`);
|
||||||
damage = Math.floor(damage * 2);
|
damage = Math.floor(damage * 2);
|
||||||
}
|
}
|
||||||
@@ -170,7 +170,7 @@ function applyDamageMultipliers({target, damage, damageProp, logValue}){
|
|||||||
return damage;
|
return damage;
|
||||||
}
|
}
|
||||||
|
|
||||||
function multiplierAppliesTo(damageProp, multiplierType){
|
function multiplierAppliesTo(damageProp, multiplierType) {
|
||||||
return multiplier => {
|
return multiplier => {
|
||||||
// Apply the default 'ignore x' tags
|
// Apply the default 'ignore x' tags
|
||||||
if (includes(damageProp.tags, `ignore ${multiplierType}`)) return false;
|
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
|
// Get all the health bars and do damage to them
|
||||||
let healthBars = getPropertiesOfType(target._id, 'attribute');
|
let healthBars = getPropertiesOfType(target._id, 'attribute');
|
||||||
|
|
||||||
@@ -239,6 +239,14 @@ function dealDamage({target, damageType, amount, actionContext}){
|
|||||||
actionContext
|
actionContext
|
||||||
});
|
});
|
||||||
damageLeft -= damageAdded;
|
damageLeft -= damageAdded;
|
||||||
|
// Prevent overflow
|
||||||
|
if (
|
||||||
|
damageType === 'healing' ?
|
||||||
|
healthBar.healthBarNoHealingOverflow :
|
||||||
|
healthBar.healthBarNoDamageOverflow
|
||||||
|
) {
|
||||||
|
damageLeft = 0;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return totalDamage;
|
return totalDamage;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const doAction = new ValidatedMethod({
|
|||||||
let slot;
|
let slot;
|
||||||
|
|
||||||
// If a spell requires a slot, make sure a slot is spent
|
// 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);
|
slot = CreatureProperties.findOne(slotId);
|
||||||
if (!slot) {
|
if (!slot) {
|
||||||
throw new Meteor.Error('No slot',
|
throw new Meteor.Error('No slot',
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const doCheck = new ValidatedMethod({
|
|||||||
numRequests: 10,
|
numRequests: 10,
|
||||||
timeInterval: 5000,
|
timeInterval: 5000,
|
||||||
},
|
},
|
||||||
run({propId, scope}) {
|
run({ propId, scope }) {
|
||||||
const prop = CreatureProperties.findOne(propId);
|
const prop = CreatureProperties.findOne(propId);
|
||||||
const creatureId = prop.ancestors[0].id;
|
const creatureId = prop.ancestors[0].id;
|
||||||
const actionContext = new ActionContext(creatureId, [creatureId], this);
|
const actionContext = new ActionContext(creatureId, [creatureId], this);
|
||||||
@@ -33,13 +33,13 @@ const doCheck = new ValidatedMethod({
|
|||||||
assertEditPermission(actionContext.creature, this.userId);
|
assertEditPermission(actionContext.creature, this.userId);
|
||||||
|
|
||||||
// Do the check
|
// Do the check
|
||||||
doCheckWork({prop, actionContext});
|
doCheckWork({ prop, actionContext });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default doCheck;
|
export default doCheck;
|
||||||
|
|
||||||
export function doCheckWork({prop, actionContext}){
|
export function doCheckWork({ prop, actionContext }) {
|
||||||
|
|
||||||
applyTriggers(actionContext.triggers.check?.before, prop, actionContext);
|
applyTriggers(actionContext.triggers.check?.before, prop, actionContext);
|
||||||
rollCheck(prop, actionContext);
|
rollCheck(prop, actionContext);
|
||||||
@@ -54,17 +54,17 @@ function rollCheck(prop, actionContext) {
|
|||||||
// get the modifier for the roll
|
// get the modifier for the roll
|
||||||
let rollModifier;
|
let rollModifier;
|
||||||
let logName = `${prop.name} check`;
|
let logName = `${prop.name} check`;
|
||||||
if (prop.type === 'skill'){
|
if (prop.type === 'skill') {
|
||||||
rollModifier = prop.value;
|
rollModifier = prop.value;
|
||||||
if (prop.skillType === 'save'){
|
if (prop.skillType === 'save') {
|
||||||
if (prop.name.match(/save/i)){
|
if (prop.name.match(/save/i)) {
|
||||||
logName = prop.name;
|
logName = prop.name;
|
||||||
} else {
|
} else {
|
||||||
logName = prop.name ? `${prop.name} save` : 'Saving Throw';
|
logName = prop.name ? `${prop.name} save` : 'Saving Throw';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (prop.type === 'attribute'){
|
} else if (prop.type === 'attribute') {
|
||||||
if (prop.attributeType === 'ability'){
|
if (prop.attributeType === 'ability') {
|
||||||
rollModifier = prop.modifier;
|
rollModifier = prop.modifier;
|
||||||
} else {
|
} else {
|
||||||
rollModifier = prop.value;
|
rollModifier = prop.value;
|
||||||
@@ -80,7 +80,7 @@ function rollCheck(prop, actionContext) {
|
|||||||
rollModifier += effectBonus;
|
rollModifier += effectBonus;
|
||||||
|
|
||||||
let value, values, resultPrefix;
|
let value, values, resultPrefix;
|
||||||
if (scope['$checkAdvantage'] === 1){
|
if (scope['$checkAdvantage'] === 1) {
|
||||||
logName += ' (Advantage)';
|
logName += ' (Advantage)';
|
||||||
const [a, b] = rollDice(2, 20);
|
const [a, b] = rollDice(2, 20);
|
||||||
if (a >= b) {
|
if (a >= b) {
|
||||||
@@ -90,7 +90,7 @@ function rollCheck(prop, actionContext) {
|
|||||||
value = b;
|
value = b;
|
||||||
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||||
}
|
}
|
||||||
} else if (scope['$checkAdvantage'] === -1){
|
} else if (scope['$checkAdvantage'] === -1) {
|
||||||
logName += ' (Disadvantage)';
|
logName += ' (Disadvantage)';
|
||||||
const [a, b] = rollDice(2, 20);
|
const [a, b] = rollDice(2, 20);
|
||||||
if (a <= b) {
|
if (a <= b) {
|
||||||
@@ -106,6 +106,9 @@ function rollCheck(prop, actionContext) {
|
|||||||
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
|
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
|
||||||
}
|
}
|
||||||
const result = (value + rollModifier) || 0;
|
const result = (value + rollModifier) || 0;
|
||||||
|
scope['$checkDiceRoll'] = value;
|
||||||
|
scope['$checkRoll'] = result;
|
||||||
|
scope['$checkModifier'] = rollModifier;
|
||||||
actionContext.addLog({
|
actionContext.addLog({
|
||||||
name: logName,
|
name: logName,
|
||||||
value: `${resultPrefix} **${result}**`,
|
value: `${resultPrefix} **${result}**`,
|
||||||
@@ -116,7 +119,7 @@ function applyUnresolvedEffects(prop, scope) {
|
|||||||
let effectBonus = 0;
|
let effectBonus = 0;
|
||||||
let effectString = '';
|
let effectString = '';
|
||||||
if (!prop.effects) {
|
if (!prop.effects) {
|
||||||
return { effectBonus, effectString};
|
return { effectBonus, effectString };
|
||||||
}
|
}
|
||||||
prop.effects.forEach(effect => {
|
prop.effects.forEach(effect => {
|
||||||
if (!effect.amount?.parseNode) return;
|
if (!effect.amount?.parseNode) return;
|
||||||
@@ -127,5 +130,5 @@ function applyUnresolvedEffects(prop, scope) {
|
|||||||
effectBonus += effect.amount.value;
|
effectBonus += effect.amount.value;
|
||||||
effectString += ` ${effect.amount.value < 0 ? '-' : '+'} [${effect.amount.calculation}] ${Math.abs(effect.amount.value)}`
|
effectString += ` ${effect.amount.value < 0 ? '-' : '+'} [${effect.amount.calculation}] ${Math.abs(effect.amount.value)}`
|
||||||
});
|
});
|
||||||
return { effectBonus, effectString};
|
return { effectBonus, effectString };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import skill from './computeByType/computeSkill.js';
|
|||||||
import pointBuy from './computeByType/computePointBuy.js';
|
import pointBuy from './computeByType/computePointBuy.js';
|
||||||
import propertySlot from './computeByType/computeSlot.js';
|
import propertySlot from './computeByType/computeSlot.js';
|
||||||
import container from './computeByType/computeContainer.js';
|
import container from './computeByType/computeContainer.js';
|
||||||
|
import spellList from './computeByType/computeSpellList.js';
|
||||||
import _calculation from './computeByType/computeCalculation.js';
|
import _calculation from './computeByType/computeCalculation.js';
|
||||||
|
|
||||||
export default Object.freeze({
|
export default Object.freeze({
|
||||||
@@ -17,4 +18,5 @@ export default Object.freeze({
|
|||||||
pointBuy,
|
pointBuy,
|
||||||
propertySlot,
|
propertySlot,
|
||||||
spell: action,
|
spell: action,
|
||||||
|
spellList,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
|
|
||||||
export default function aggregateEffect({node, linkedNode, link}){
|
export default function aggregateEffect({ node, linkedNode, link }) {
|
||||||
if (link.data !== 'effect') return;
|
if (link.data !== 'effect') return;
|
||||||
// store the effect aggregator, its presence indicates that the variable is
|
// store the effect aggregator, its presence indicates that the variable is
|
||||||
// targeted by effects
|
// targeted by effects
|
||||||
@@ -38,6 +38,7 @@ export default function aggregateEffect({node, linkedNode, link}){
|
|||||||
operation: linkedNode.data.operation,
|
operation: linkedNode.data.operation,
|
||||||
amount: effectAmount,
|
amount: effectAmount,
|
||||||
type: linkedNode.data.type,
|
type: linkedNode.data.type,
|
||||||
|
text: linkedNode.data.text,
|
||||||
// ancestors: linkedNode.data.ancestors,
|
// ancestors: linkedNode.data.ancestors,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,14 +46,13 @@ export default function aggregateEffect({node, linkedNode, link}){
|
|||||||
const aggregator = node.data.effectAggregator;
|
const aggregator = node.data.effectAggregator;
|
||||||
// Get the result of the effect
|
// Get the result of the effect
|
||||||
const result = linkedNode.data.amount?.value;
|
const result = linkedNode.data.amount?.value;
|
||||||
// Skip aggregating if the result is not resolved completely
|
|
||||||
if (typeof result === 'string' || result === undefined) return;
|
|
||||||
// Aggregate the effect based on its operation
|
// Aggregate the effect based on its operation
|
||||||
switch(linkedNode.data.operation){
|
switch (linkedNode.data.operation) {
|
||||||
case 'base':
|
case 'base':
|
||||||
// Take the largest base value
|
// Take the largest base value
|
||||||
if (Number.isFinite(result)){
|
if (Number.isFinite(result)) {
|
||||||
if(Number.isFinite(aggregator.base)){
|
if (Number.isFinite(aggregator.base)) {
|
||||||
aggregator.base = Math.max(aggregator.base, result);
|
aggregator.base = Math.max(aggregator.base, result);
|
||||||
} else {
|
} else {
|
||||||
aggregator.base = result;
|
aggregator.base = result;
|
||||||
|
|||||||
97
app/imports/api/library/methods/copyLibraryNodeTo.js
Normal 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;
|
||||||
@@ -16,7 +16,7 @@ if (Meteor.isClient) {
|
|||||||
).snackbar
|
).snackbar
|
||||||
}
|
}
|
||||||
|
|
||||||
const DUPLICATE_CHILDREN_LIMIT = 50;
|
const DUPLICATE_CHILDREN_LIMIT = 500;
|
||||||
|
|
||||||
const duplicateLibraryNode = new ValidatedMethod({
|
const duplicateLibraryNode = new ValidatedMethod({
|
||||||
name: 'libraryNodes.duplicate',
|
name: 'libraryNodes.duplicate',
|
||||||
@@ -28,7 +28,7 @@ const duplicateLibraryNode = new ValidatedMethod({
|
|||||||
}).validator(),
|
}).validator(),
|
||||||
mixins: [RateLimiterMixin],
|
mixins: [RateLimiterMixin],
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
numRequests: 5,
|
numRequests: 1,
|
||||||
timeInterval: 5000,
|
timeInterval: 5000,
|
||||||
},
|
},
|
||||||
run({ _id }) {
|
run({ _id }) {
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
import '/imports/api/library/methods/copyLibraryNodeTo.js';
|
||||||
import '/imports/api/library/methods/duplicateLibraryNode.js';
|
import '/imports/api/library/methods/duplicateLibraryNode.js';
|
||||||
import '/imports/api/library/methods/updateReferenceNode.js';
|
import '/imports/api/library/methods/updateReferenceNode.js';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
|
|||||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
|
||||||
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
|
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
|
||||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
|
||||||
|
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Actions are things a character can do
|
* Actions are things a character can do
|
||||||
@@ -24,9 +25,17 @@ let ActionSchema = createPropertySchema({
|
|||||||
// long actions take longer than 1 round to cast
|
// long actions take longer than 1 round to cast
|
||||||
actionType: {
|
actionType: {
|
||||||
type: String,
|
type: String,
|
||||||
allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long'],
|
allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long', 'event'],
|
||||||
defaultValue: 'action',
|
defaultValue: 'action',
|
||||||
},
|
},
|
||||||
|
// If the action type is an event, what is the variable name of that event?
|
||||||
|
variableName: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
regEx: VARIABLE_NAME_REGEX,
|
||||||
|
min: 2,
|
||||||
|
max: STORAGE_LIMITS.variableName,
|
||||||
|
},
|
||||||
// Who is the action directed at
|
// Who is the action directed at
|
||||||
target: {
|
target: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -56,8 +65,10 @@ let ActionSchema = createPropertySchema({
|
|||||||
// How this action's uses are reset automatically
|
// How this action's uses are reset automatically
|
||||||
reset: {
|
reset: {
|
||||||
type: String,
|
type: String,
|
||||||
allowedValues: ['longRest', 'shortRest'],
|
|
||||||
optional: true,
|
optional: true,
|
||||||
|
regEx: VARIABLE_NAME_REGEX,
|
||||||
|
min: 2,
|
||||||
|
max: STORAGE_LIMITS.variableName,
|
||||||
},
|
},
|
||||||
// Resources
|
// Resources
|
||||||
resources: {
|
resources: {
|
||||||
@@ -74,7 +85,7 @@ let ActionSchema = createPropertySchema({
|
|||||||
'resources.itemsConsumed.$._id': {
|
'resources.itemsConsumed.$._id': {
|
||||||
type: String,
|
type: String,
|
||||||
regEx: SimpleSchema.RegEx.Id,
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
autoValue(){
|
autoValue() {
|
||||||
if (!this.isSet) return Random.id();
|
if (!this.isSet) return Random.id();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -101,7 +112,7 @@ let ActionSchema = createPropertySchema({
|
|||||||
'resources.attributesConsumed.$._id': {
|
'resources.attributesConsumed.$._id': {
|
||||||
type: String,
|
type: String,
|
||||||
regEx: SimpleSchema.RegEx.Id,
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
autoValue(){
|
autoValue() {
|
||||||
if (!this.isSet) return Random.id();
|
if (!this.isSet) return Random.id();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -218,4 +229,4 @@ const ComputedActionSchema = new SimpleSchema()
|
|||||||
.extend(ActionSchema)
|
.extend(ActionSchema)
|
||||||
.extend(ComputedOnlyActionSchema);
|
.extend(ComputedOnlyActionSchema);
|
||||||
|
|
||||||
export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema};
|
export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema };
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ let AttributeSchema = createPropertySchema({
|
|||||||
'stat', // Speed, Armor Class
|
'stat', // Speed, Armor Class
|
||||||
'modifier', // Proficiency Bonus, displayed as +x
|
'modifier', // Proficiency Bonus, displayed as +x
|
||||||
'hitDice', // d12 hit dice
|
'hitDice', // d12 hit dice
|
||||||
'healthBar', // Hitpoints, Temporary Hitpoints, can take damage
|
'healthBar', // Hitpoints, Temporary Hitpoints
|
||||||
'bar', // Displayed as a health bar, can't take damage
|
|
||||||
'resource', // Rages, sorcery points
|
'resource', // Rages, sorcery points
|
||||||
'spellSlot', // Level 1, 2, 3... spell slots
|
'spellSlot', // Level 1, 2, 3... spell slots
|
||||||
'utility', // Aren't displayed, Jump height, Carry capacity
|
'utility', // Aren't displayed, Jump height, Carry capacity
|
||||||
@@ -69,6 +68,16 @@ let AttributeSchema = createPropertySchema({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
|
// Control how the health bar handles overflow
|
||||||
|
healthBarNoDamageOverflow: {
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
healthBarNoHealingOverflow: {
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
// Control when the health bar takes damage or healing
|
||||||
healthBarDamageOrder: {
|
healthBarDamageOrder: {
|
||||||
type: SimpleSchema.Integer,
|
type: SimpleSchema.Integer,
|
||||||
optional: true,
|
optional: true,
|
||||||
@@ -107,11 +116,21 @@ let AttributeSchema = createPropertySchema({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
|
hideWhenTotalZero: {
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
hideWhenValueZero: {
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
// Automatically zero the adjustment on these conditions
|
// Automatically zero the adjustment on these conditions
|
||||||
reset: {
|
reset: {
|
||||||
type: String,
|
type: String,
|
||||||
optional: true,
|
optional: true,
|
||||||
allowedValues: ['shortRest', 'longRest'],
|
regEx: VARIABLE_NAME_REGEX,
|
||||||
|
min: 2,
|
||||||
|
max: STORAGE_LIMITS.variableName,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ let FolderSchema = new createPropertySchema({
|
|||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
max: STORAGE_LIMITS.name,
|
max: STORAGE_LIMITS.name,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
groupStats: {
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ let SpellListSchema = createPropertySchema({
|
|||||||
type: 'fieldToCompute',
|
type: 'fieldToCompute',
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
|
// The variable name of the ability this spell relies on
|
||||||
|
ability: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
max: STORAGE_LIMITS.variableName,
|
||||||
|
},
|
||||||
// Calculation of The attack roll bonus used by spell attacks in this list
|
// Calculation of The attack roll bonus used by spell attacks in this list
|
||||||
attackRollBonus: {
|
attackRollBonus: {
|
||||||
type: 'fieldToCompute',
|
type: 'fieldToCompute',
|
||||||
@@ -38,6 +44,12 @@ const ComputedOnlySpellListSchema = createPropertySchema({
|
|||||||
type: 'computedOnlyField',
|
type: 'computedOnlyField',
|
||||||
optional: true,
|
optional: true,
|
||||||
},
|
},
|
||||||
|
// Computed value determined by the ability
|
||||||
|
abilityMod: {
|
||||||
|
type: SimpleSchema.Integer,
|
||||||
|
optional: true,
|
||||||
|
removeBeforeCompute: true,
|
||||||
|
},
|
||||||
attackRollBonus: {
|
attackRollBonus: {
|
||||||
type: 'computedOnlyField',
|
type: 'computedOnlyField',
|
||||||
optional: true,
|
optional: true,
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ let SharingSchema = new SimpleSchema({
|
|||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
index: 1,
|
index: 1,
|
||||||
},
|
},
|
||||||
|
readersCanCopy: {
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default SharingSchema;
|
export default SharingSchema;
|
||||||
|
|||||||
@@ -27,6 +27,26 @@ const setPublic = new ValidatedMethod({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setReadersCanCopy = new ValidatedMethod({
|
||||||
|
name: 'sharing.setReadersCanCopy',
|
||||||
|
validate: new SimpleSchema({
|
||||||
|
docRef: RefSchema,
|
||||||
|
readersCanCopy: { type: Boolean },
|
||||||
|
}).validator(),
|
||||||
|
mixins: [RateLimiterMixin],
|
||||||
|
rateLimit: {
|
||||||
|
numRequests: 5,
|
||||||
|
timeInterval: 5000,
|
||||||
|
},
|
||||||
|
run({ docRef, readersCanCopy }) {
|
||||||
|
let doc = fetchDocByRef(docRef);
|
||||||
|
assertOwnership(doc, this.userId);
|
||||||
|
return getCollectionByName(docRef.collection).update(docRef.id, {
|
||||||
|
$set: { readersCanCopy },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const updateUserSharePermissions = new ValidatedMethod({
|
const updateUserSharePermissions = new ValidatedMethod({
|
||||||
name: 'sharing.updateUserSharePermissions',
|
name: 'sharing.updateUserSharePermissions',
|
||||||
validate: new SimpleSchema({
|
validate: new SimpleSchema({
|
||||||
@@ -129,4 +149,4 @@ const transferOwnership = new ValidatedMethod({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export { setPublic, updateUserSharePermissions, transferOwnership };
|
export { setPublic, setReadersCanCopy, updateUserSharePermissions, transferOwnership };
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import { _ } from 'meteor/underscore';
|
import { _ } from 'meteor/underscore';
|
||||||
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||||
|
|
||||||
function assertIdValid(userId){
|
function assertIdValid(userId) {
|
||||||
if (!userId || typeof userId !== 'string'){
|
if (!userId || typeof userId !== 'string') {
|
||||||
throw new Meteor.Error('Permission denied',
|
throw new Meteor.Error('Permission denied',
|
||||||
'No user ID. Are you logged in?');
|
'No user ID. Are you logged in?');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertdocExists(doc){
|
function assertdocExists(doc) {
|
||||||
if (!doc){
|
if (!doc) {
|
||||||
throw new Meteor.Error('Permission denied',
|
throw new Meteor.Error('Permission denied',
|
||||||
'Permission denied: No such document exists');
|
'Permission denied: No such document exists');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertOwnership(doc, userId){
|
export function assertOwnership(doc, userId) {
|
||||||
assertIdValid(userId);
|
assertIdValid(userId);
|
||||||
assertdocExists(doc);
|
assertdocExists(doc);
|
||||||
if (doc.owner === userId ){
|
|
||||||
|
if (doc.owner === userId) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Meteor.Error('Permission denied',
|
throw new Meteor.Error('Permission denied',
|
||||||
@@ -37,13 +38,12 @@ export function assertEditPermission(doc, userId) {
|
|||||||
assertdocExists(doc);
|
assertdocExists(doc);
|
||||||
const user = Meteor.users.findOne(userId, {
|
const user = Meteor.users.findOne(userId, {
|
||||||
fields: {
|
fields: {
|
||||||
'services.patreon': 1,
|
|
||||||
'roles': 1,
|
'roles': 1,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Admin override
|
// Admin override
|
||||||
if (user.roles && user.roles.includes('admin')){
|
if (user.roles && user.roles.includes('admin')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ export function assertEditPermission(doc, userId) {
|
|||||||
if (
|
if (
|
||||||
doc.owner === userId ||
|
doc.owner === userId ||
|
||||||
_.contains(doc.writers, userId)
|
_.contains(doc.writers, userId)
|
||||||
){
|
) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
throw new Meteor.Error('Edit permission denied',
|
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);
|
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]);
|
return fetchDocByRef(doc.ancestors[0]);
|
||||||
} else {
|
} else {
|
||||||
return doc;
|
return doc;
|
||||||
@@ -74,11 +111,22 @@ function getRoot(doc){
|
|||||||
*
|
*
|
||||||
* Warning: the doc and userId must be set by a trusted source
|
* 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);
|
let root = getRoot(doc);
|
||||||
assertEditPermission(root, userId);
|
assertEditPermission(root, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that the user can copy a descendant document whose root ancestor
|
||||||
|
* implements sharing permissions.
|
||||||
|
*
|
||||||
|
* Warning: the doc and userId must be set by a trusted source
|
||||||
|
*/
|
||||||
|
export function assertDocCopyPermission(doc, userId) {
|
||||||
|
let root = getRoot(doc);
|
||||||
|
assertCopyPermission(root, userId);
|
||||||
|
}
|
||||||
|
|
||||||
export function assertViewPermission(doc, userId) {
|
export function assertViewPermission(doc, userId) {
|
||||||
assertdocExists(doc);
|
assertdocExists(doc);
|
||||||
if (doc.public) return true;
|
if (doc.public) return true;
|
||||||
@@ -88,17 +136,17 @@ export function assertViewPermission(doc, userId) {
|
|||||||
doc.owner === userId ||
|
doc.owner === userId ||
|
||||||
_.contains(doc.readers, userId) ||
|
_.contains(doc.readers, userId) ||
|
||||||
_.contains(doc.writers, userId)
|
_.contains(doc.writers, userId)
|
||||||
){
|
) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Admin override
|
// Admin override
|
||||||
const user = Meteor.users.findOne(userId, {
|
const user = Meteor.users.findOne(userId, {
|
||||||
fields: {
|
fields: {
|
||||||
'roles': 1,
|
'roles': 1,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (user.roles && user.roles.includes('admin')){
|
if (user.roles && user.roles.includes('admin')) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,20 +161,20 @@ export function assertViewPermission(doc, userId) {
|
|||||||
*
|
*
|
||||||
* Warning: the doc and userId must be set by a trusted source
|
* 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);
|
let root = getRoot(doc);
|
||||||
assertViewPermission(root, userId);
|
assertViewPermission(root, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertAdmin(userId){
|
export function assertAdmin(userId) {
|
||||||
assertIdValid(userId);
|
assertIdValid(userId);
|
||||||
let user = Meteor.users.findOne(userId, {fields: {roles: 1}});
|
let user = Meteor.users.findOne(userId, { fields: { roles: 1 } });
|
||||||
if (!user){
|
if (!user) {
|
||||||
throw new Meteor.Error('Permission denied',
|
throw new Meteor.Error('Permission denied',
|
||||||
'UserId does not match any existing user');
|
'UserId does not match any existing user');
|
||||||
}
|
}
|
||||||
let isAdmin = user.roles && user.roles.includes('admin')
|
let isAdmin = user.roles && user.roles.includes('admin')
|
||||||
if (!isAdmin){
|
if (!isAdmin) {
|
||||||
throw new Meteor.Error('Permission denied',
|
throw new Meteor.Error('Permission denied',
|
||||||
'User does not have the admin role');
|
'User does not have the admin role');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
import { Migrations } from 'meteor/percolate:migrations';
|
import { Migrations } from 'meteor/percolate:migrations';
|
||||||
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
|
import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js';
|
||||||
|
|
||||||
if (Meteor.isServer){
|
if (Meteor.isServer) {
|
||||||
Meteor.startup(()=>{
|
Meteor.startup(() => {
|
||||||
const dbVersion = Migrations.getVersion();
|
const dbVersion = Migrations.getVersion();
|
||||||
|
// If there are no users, this is a new DB, set the version to latest
|
||||||
|
const aUser = Meteor.users.findOne({});
|
||||||
|
const latestVersion = Migrations._list[Migrations._list.length - 1].version
|
||||||
|
if (!aUser && dbVersion !== latestVersion) {
|
||||||
|
Migrations._collection.update({ _id: 'control' }, { version: latestVersion });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Otherwise put the app in maintenance mode if it's not the right version
|
||||||
if (
|
if (
|
||||||
!Meteor.settings.public.maintenanceMode &&
|
!Meteor.settings.public.maintenanceMode &&
|
||||||
dbVersion !== undefined &&
|
dbVersion !== undefined &&
|
||||||
SCHEMA_VERSION !== dbVersion
|
SCHEMA_VERSION !== dbVersion
|
||||||
){
|
) {
|
||||||
Meteor.settings.public.maintenanceMode = {
|
Meteor.settings.public.maintenanceMode = {
|
||||||
reason: 'App data needs to be migrated to the latest version'
|
reason: 'App data needs to be migrated to the latest version'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const docPaths = [
|
|||||||
'dependency-loops',
|
'dependency-loops',
|
||||||
'docs',
|
'docs',
|
||||||
'tags',
|
'tags',
|
||||||
|
'walkthroughs/create-a-class',
|
||||||
];
|
];
|
||||||
const docs = new Map();
|
const docs = new Map();
|
||||||
docPaths.forEach(path => {
|
docPaths.forEach(path => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
:outlined="!!label"
|
:outlined="!!label"
|
||||||
:icon="!label"
|
:icon="!label"
|
||||||
:min-width="label && 108"
|
:min-width="label && 108"
|
||||||
|
:disabled="context.editPermission === false"
|
||||||
v-on="on"
|
v-on="on"
|
||||||
>
|
>
|
||||||
{{ label }}
|
{{ label }}
|
||||||
@@ -124,6 +125,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
inject: {
|
||||||
|
context: { default: {} }
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
//hex string
|
//hex string
|
||||||
value: {
|
value: {
|
||||||
|
|||||||
46
app/imports/ui/components/ResetSelector.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<smart-select
|
||||||
|
label="Reset"
|
||||||
|
clearable
|
||||||
|
style="flex-basis: 300px;"
|
||||||
|
:hint="hint"
|
||||||
|
:items="resetOptions"
|
||||||
|
:value="value"
|
||||||
|
:error-messages="errorMessages"
|
||||||
|
:menu-props="{auto: true, lazy: true}"
|
||||||
|
@change="(value, ack) => $emit('change', value, ack)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="js">
|
||||||
|
import createListOfProperties from '/imports/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>
|
||||||
@@ -69,6 +69,7 @@
|
|||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-if="$listeners && $listeners.duplicate"
|
v-if="$listeners && $listeners.duplicate"
|
||||||
|
:disabled="context.editPermission === false"
|
||||||
@click="$emit('duplicate')"
|
@click="$emit('duplicate')"
|
||||||
>
|
>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
@@ -80,8 +81,23 @@
|
|||||||
<v-icon>mdi-content-copy</v-icon>
|
<v-icon>mdi-content-copy</v-icon>
|
||||||
</v-list-item-action>
|
</v-list-item-action>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
<v-list-item
|
||||||
|
v-if="$listeners && $listeners.copy"
|
||||||
|
:disabled="context.copyPermission === false"
|
||||||
|
@click="$emit('copy')"
|
||||||
|
>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>
|
||||||
|
Copy To
|
||||||
|
</v-list-item-title>
|
||||||
|
</v-list-item-content>
|
||||||
|
<v-list-item-action>
|
||||||
|
<v-icon>mdi-content-duplicate</v-icon>
|
||||||
|
</v-list-item-action>
|
||||||
|
</v-list-item>
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-if="$listeners && $listeners.move"
|
v-if="$listeners && $listeners.move"
|
||||||
|
:disabled="context.editPermission === false"
|
||||||
@click="$emit('move')"
|
@click="$emit('move')"
|
||||||
>
|
>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
@@ -95,6 +111,7 @@
|
|||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-if="$listeners && $listeners.remove"
|
v-if="$listeners && $listeners.remove"
|
||||||
|
:disabled="context.editPermission === false"
|
||||||
@click="$emit('remove')"
|
@click="$emit('remove')"
|
||||||
>
|
>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
@@ -157,6 +174,9 @@ export default {
|
|||||||
PropertyIcon,
|
PropertyIcon,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
},
|
},
|
||||||
|
inject: {
|
||||||
|
context: { default: {} }
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
model: {
|
model: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -39,6 +39,11 @@
|
|||||||
:input-value="model.settings.hideUnusedStats"
|
:input-value="model.settings.hideUnusedStats"
|
||||||
@change="value => $emit('change', {path: ['settings','hideUnusedStats'], value: !!value})"
|
@change="value => $emit('change', {path: ['settings','hideUnusedStats'], value: !!value})"
|
||||||
/>
|
/>
|
||||||
|
<v-switch
|
||||||
|
label="Hide rest buttons"
|
||||||
|
:input-value="model.settings.hideRestButtons"
|
||||||
|
@change="value => $emit('change', {path: ['settings','hideRestButtons'], value: !!value})"
|
||||||
|
/>
|
||||||
<v-switch
|
<v-switch
|
||||||
label="Show spells tab"
|
label="Show spells tab"
|
||||||
:input-value="!model.settings.hideSpellsTab"
|
:input-value="!model.settings.hideSpellsTab"
|
||||||
|
|||||||
@@ -1,21 +1,53 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<div class="stats-tab ma-2">
|
<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>
|
<column-layout>
|
||||||
<div class="character-buttons">
|
<folder-group-card
|
||||||
|
v-for="folder in folders"
|
||||||
|
:key="folder._id"
|
||||||
|
:model="folder"
|
||||||
|
@click-property="clickProperty"
|
||||||
|
@sub-click="_id => clickTreeProperty({_id})"
|
||||||
|
@remove="softRemove"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="!creature.settings.hideRestButtons || (events && events.length)"
|
||||||
|
class="character-buttons"
|
||||||
|
>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-text class="layout column align-center">
|
<v-card-text class="layout column align-center">
|
||||||
<rest-button
|
<rest-button
|
||||||
|
v-if="!creature.settings.hideRestButtons"
|
||||||
:creature-id="creatureId"
|
:creature-id="creatureId"
|
||||||
type="shortRest"
|
type="shortRest"
|
||||||
class="ma-1"
|
class="ma-1"
|
||||||
/>
|
/>
|
||||||
<rest-button
|
<rest-button
|
||||||
|
v-if="!creature.settings.hideRestButtons"
|
||||||
:creature-id="creatureId"
|
:creature-id="creatureId"
|
||||||
type="longRest"
|
type="longRest"
|
||||||
class="ma-1"
|
class="ma-1"
|
||||||
/>
|
/>
|
||||||
|
<event-button
|
||||||
|
v-for="event in events"
|
||||||
|
:key="event._id"
|
||||||
|
:model="event"
|
||||||
|
class="ma-1"
|
||||||
|
/>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -33,26 +65,14 @@
|
|||||||
<v-card>
|
<v-card>
|
||||||
<v-list>
|
<v-list>
|
||||||
<v-subheader>Buffs and conditions</v-subheader>
|
<v-subheader>Buffs and conditions</v-subheader>
|
||||||
<v-list-item
|
<buff-list-item
|
||||||
v-for="buff in appliedBuffs"
|
v-for="buff in appliedBuffs"
|
||||||
:key="buff._id"
|
:key="buff._id"
|
||||||
:data-id="buff._id"
|
:data-id="buff._id"
|
||||||
|
:model="buff"
|
||||||
@click="clickProperty({_id: buff._id})"
|
@click="clickProperty({_id: buff._id})"
|
||||||
>
|
@remove="softRemove(buff._id)"
|
||||||
<v-list-item-content>
|
/>
|
||||||
<v-list-item-title>
|
|
||||||
{{ buff.name }}
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item-content>
|
|
||||||
<v-list-item-action v-if="!buff.hideRemoveButton">
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
@click.stop="softRemove(buff._id)"
|
|
||||||
>
|
|
||||||
<v-icon>mdi-delete</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-list-item-action>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,7 +202,6 @@
|
|||||||
:model="spellSlot"
|
:model="spellSlot"
|
||||||
:data-id="spellSlot._id"
|
:data-id="spellSlot._id"
|
||||||
@click="clickProperty({_id: spellSlot._id})"
|
@click="clickProperty({_id: spellSlot._id})"
|
||||||
@cast="castSpellWithSlot(spellSlot._id)"
|
|
||||||
/>
|
/>
|
||||||
</v-list>
|
</v-list>
|
||||||
<div
|
<div
|
||||||
@@ -338,11 +357,11 @@
|
|||||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
|
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty.js';
|
||||||
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||||
|
import HealthBar from '/imports/ui/properties/components/attributes/HealthBar.vue';
|
||||||
import AttributeCard from '/imports/ui/properties/components/attributes/AttributeCard.vue';
|
import AttributeCard from '/imports/ui/properties/components/attributes/AttributeCard.vue';
|
||||||
import AbilityListTile from '/imports/ui/properties/components/attributes/AbilityListTile.vue';
|
import AbilityListTile from '/imports/ui/properties/components/attributes/AbilityListTile.vue';
|
||||||
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
|
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
|
||||||
import DamageMultiplierCard from '/imports/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue';
|
import DamageMultiplierCard from '/imports/ui/properties/components/damageMultipliers/DamageMultiplierCard.vue';
|
||||||
import HealthBarCardContainer from '/imports/ui/properties/components/attributes/HealthBarCardContainer.vue';
|
|
||||||
import HitDiceListTile from '/imports/ui/properties/components/attributes/HitDiceListTile.vue';
|
import HitDiceListTile from '/imports/ui/properties/components/attributes/HitDiceListTile.vue';
|
||||||
import SkillListTile from '/imports/ui/properties/components/skills/SkillListTile.vue';
|
import SkillListTile from '/imports/ui/properties/components/skills/SkillListTile.vue';
|
||||||
import ResourceCard from '/imports/ui/properties/components/attributes/ResourceCard.vue';
|
import ResourceCard from '/imports/ui/properties/components/attributes/ResourceCard.vue';
|
||||||
@@ -351,10 +370,14 @@ import ActionCard from '/imports/ui/properties/components/actions/ActionCard.vue
|
|||||||
import RestButton from '/imports/ui/creature/RestButton.vue';
|
import RestButton from '/imports/ui/creature/RestButton.vue';
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import ToggleCard from '/imports/ui/properties/components/toggles/ToggleCard.vue';
|
import ToggleCard from '/imports/ui/properties/components/toggles/ToggleCard.vue';
|
||||||
|
import BuffListItem from '/imports/ui/properties/components/buffs/BuffListItem.vue';
|
||||||
import doCastSpell from '/imports/api/engine/actions/doCastSpell.js';
|
import doCastSpell from '/imports/api/engine/actions/doCastSpell.js';
|
||||||
|
import EventButton from '/imports/ui/properties/components/actions/EventButton.vue';
|
||||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||||
|
import FolderGroupCard from '/imports/ui/properties/components/folders/FolderGroupCard.vue';
|
||||||
|
import { uniqBy } from 'lodash';
|
||||||
|
|
||||||
const getProperties = function (creature, filter, options = {
|
const getProperties = function (creature, folderIds, filter, options = {
|
||||||
sort: { order: 1 }
|
sort: { order: 1 }
|
||||||
}) {
|
}) {
|
||||||
if (!creature) return;
|
if (!creature) return;
|
||||||
@@ -362,22 +385,27 @@ const getProperties = function (creature, filter, options = {
|
|||||||
filter.hide = { $ne: true };
|
filter.hide = { $ne: true };
|
||||||
}
|
}
|
||||||
filter['ancestors.id'] = creature._id;
|
filter['ancestors.id'] = creature._id;
|
||||||
|
filter['parent.id'] = {$nin: folderIds},
|
||||||
filter.removed = { $ne: true };
|
filter.removed = { $ne: true };
|
||||||
filter.inactive = { $ne: true };
|
filter.inactive = { $ne: true };
|
||||||
filter.overridden = { $ne: true };
|
filter.overridden = { $ne: true };
|
||||||
|
filter.$nor = [
|
||||||
|
{ hideWhenTotalZero: true, total: 0 },
|
||||||
|
{ hideWhenValueZero: true, value: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
return CreatureProperties.find(filter, options);
|
return CreatureProperties.find(filter, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAttributeOfType = function (creature, type) {
|
const getAttributeOfType = function (creature, folderIds, type) {
|
||||||
return getProperties(creature, {
|
return getProperties(creature, folderIds, {
|
||||||
type: 'attribute',
|
type: 'attribute',
|
||||||
attributeType: type,
|
attributeType: type,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSkillOfType = function (creature, type) {
|
const getSkillOfType = function (creature, folderIds, type) {
|
||||||
return getProperties(creature, {
|
return getProperties(creature, folderIds, {
|
||||||
type: 'skill',
|
type: 'skill',
|
||||||
skillType: type,
|
skillType: type,
|
||||||
});
|
});
|
||||||
@@ -385,18 +413,21 @@ const getSkillOfType = function (creature, type) {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
HealthBar,
|
||||||
RestButton,
|
RestButton,
|
||||||
|
BuffListItem,
|
||||||
AbilityListTile,
|
AbilityListTile,
|
||||||
AttributeCard,
|
AttributeCard,
|
||||||
ColumnLayout,
|
ColumnLayout,
|
||||||
DamageMultiplierCard,
|
DamageMultiplierCard,
|
||||||
HealthBarCardContainer,
|
|
||||||
HitDiceListTile,
|
HitDiceListTile,
|
||||||
SkillListTile,
|
SkillListTile,
|
||||||
ResourceCard,
|
ResourceCard,
|
||||||
SpellSlotListTile,
|
SpellSlotListTile,
|
||||||
ActionCard,
|
ActionCard,
|
||||||
ToggleCard,
|
ToggleCard,
|
||||||
|
EventButton,
|
||||||
|
FolderGroupCard,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
creatureId: {
|
creatureId: {
|
||||||
@@ -413,16 +444,27 @@ export default {
|
|||||||
creature() {
|
creature() {
|
||||||
return Creatures.findOne(this.creatureId, { fields: { settings: 1 } });
|
return Creatures.findOne(this.creatureId, { fields: { settings: 1 } });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
folders() {
|
||||||
|
return getProperties(this.creature, [], { type: 'folder', groupStats: true });
|
||||||
|
},
|
||||||
|
folderIds() {
|
||||||
|
return this.folders.map(f => f._id);
|
||||||
|
},
|
||||||
|
healthBars() {
|
||||||
|
return getAttributeOfType(this.creature, this.folderIds, 'healthBar');
|
||||||
|
},
|
||||||
abilities() {
|
abilities() {
|
||||||
return getAttributeOfType(this.creature, 'ability');
|
return getAttributeOfType(this.creature, this.folderIds, 'ability');
|
||||||
},
|
},
|
||||||
stats() {
|
stats() {
|
||||||
return getAttributeOfType(this.creature, 'stat');
|
return getAttributeOfType(this.creature, this.folderIds, 'stat');
|
||||||
},
|
},
|
||||||
toggles() {
|
toggles() {
|
||||||
return CreatureProperties.find({
|
return CreatureProperties.find({
|
||||||
'ancestors.id': this.creatureId,
|
|
||||||
type: 'toggle',
|
type: 'toggle',
|
||||||
|
'ancestors.id': this.creatureId,
|
||||||
|
'parent.id': { $nin: this.folderIds },
|
||||||
removed: { $ne: true },
|
removed: { $ne: true },
|
||||||
deactivatedByAncestor: { $ne: true },
|
deactivatedByAncestor: { $ne: true },
|
||||||
showUI: true,
|
showUI: true,
|
||||||
@@ -431,52 +473,56 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
modifiers() {
|
modifiers() {
|
||||||
return getAttributeOfType(this.creature, 'modifier');
|
return getAttributeOfType(this.creature, this.folderIds, 'modifier');
|
||||||
},
|
},
|
||||||
resources() {
|
resources() {
|
||||||
return getAttributeOfType(this.creature, 'resource');
|
return getAttributeOfType(this.creature, this.folderIds, 'resource');
|
||||||
},
|
},
|
||||||
spellSlots() {
|
spellSlots() {
|
||||||
return getAttributeOfType(this.creature, 'spellSlot');
|
return getAttributeOfType(this.creature, this.folderIds, 'spellSlot');
|
||||||
},
|
},
|
||||||
hasSpells() {
|
hasSpells() {
|
||||||
const cursor = getProperties(this.creature, {
|
const cursor = getProperties(this.creature, this.folderIds, {
|
||||||
type: 'spell',
|
type: 'spell',
|
||||||
})
|
})
|
||||||
return cursor && cursor.count();
|
return cursor && cursor.count();
|
||||||
},
|
},
|
||||||
hitDice() {
|
hitDice() {
|
||||||
return getAttributeOfType(this.creature, 'hitDice');
|
return getAttributeOfType(this.creature, this.folderIds, 'hitDice');
|
||||||
},
|
},
|
||||||
checks() {
|
checks() {
|
||||||
return getSkillOfType(this.creature, 'check');
|
return getSkillOfType(this.creature, this.folderIds, 'check');
|
||||||
},
|
},
|
||||||
savingThrows() {
|
savingThrows() {
|
||||||
return getSkillOfType(this.creature, 'save');
|
return getSkillOfType(this.creature, this.folderIds, 'save');
|
||||||
},
|
},
|
||||||
skills() {
|
skills() {
|
||||||
return getSkillOfType(this.creature, 'skill');
|
return getSkillOfType(this.creature, this.folderIds, 'skill');
|
||||||
},
|
},
|
||||||
tools() {
|
tools() {
|
||||||
return getSkillOfType(this.creature, 'tool');
|
return getSkillOfType(this.creature, this.folderIds, 'tool');
|
||||||
},
|
},
|
||||||
weapons() {
|
weapons() {
|
||||||
return getSkillOfType(this.creature, 'weapon');
|
return getSkillOfType(this.creature, this.folderIds, 'weapon');
|
||||||
},
|
},
|
||||||
armors() {
|
armors() {
|
||||||
return getSkillOfType(this.creature, 'armor');
|
return getSkillOfType(this.creature, this.folderIds, 'armor');
|
||||||
},
|
},
|
||||||
languages() {
|
languages() {
|
||||||
return getSkillOfType(this.creature, 'language');
|
return getSkillOfType(this.creature, this.folderIds, 'language');
|
||||||
|
},
|
||||||
|
events() {
|
||||||
|
const events = getProperties(this.creature, this.folderIds, { type: 'action', actionType: 'event' });
|
||||||
|
return uniqBy(events.fetch(), e => e.variableName);
|
||||||
},
|
},
|
||||||
actions() {
|
actions() {
|
||||||
return getProperties(this.creature, { type: 'action' });
|
return getProperties(this.creature, this.folderIds, { type: 'action', actionType: { $ne: 'event' } });
|
||||||
},
|
},
|
||||||
appliedBuffs() {
|
appliedBuffs() {
|
||||||
return getProperties(this.creature, { type: 'buff' });
|
return getProperties(this.creature, this.folderIds, { type: 'buff' });
|
||||||
},
|
},
|
||||||
multipliers() {
|
multipliers() {
|
||||||
return getProperties(this.creature, {
|
return getProperties(this.creature, this.folderIds, {
|
||||||
type: 'damageMultiplier'
|
type: 'damageMultiplier'
|
||||||
}, {
|
}, {
|
||||||
sort: { value: 1, order: 1 }
|
sort: { value: 1, order: 1 }
|
||||||
@@ -499,13 +545,23 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
incrementChange(_id, { type, value }) {
|
incrementChange(_id, { type, value }) {
|
||||||
if (type === 'increment') {
|
damageProperty.call({
|
||||||
damageProperty.call({ _id, operation: 'increment', value: -value });
|
_id,
|
||||||
}
|
operation: type,
|
||||||
|
value: -value
|
||||||
|
}, error => {
|
||||||
|
if (error) {
|
||||||
|
snackbar({ text: error.reason || error.message || error.toString() });
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
softRemove(_id) {
|
softRemove(_id) {
|
||||||
softRemoveProperty.call({ _id }, error => {
|
softRemoveProperty.call({ _id }, error => {
|
||||||
if (error) console.error(error);
|
if (error) {
|
||||||
|
snackbar({ text: error.reason || error.message || error.toString() });
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
castSpell() {
|
castSpell() {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -16,6 +16,14 @@
|
|||||||
flat
|
flat
|
||||||
@change="propertyHelpChanged"
|
@change="propertyHelpChanged"
|
||||||
/>
|
/>
|
||||||
|
<v-btn
|
||||||
|
v-if="tab === 1"
|
||||||
|
icon
|
||||||
|
data-id="help-button"
|
||||||
|
@click="helpDialog"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-help</v-icon>
|
||||||
|
</v-btn>
|
||||||
<text-field
|
<text-field
|
||||||
v-if="tab === 2"
|
v-if="tab === 2"
|
||||||
prepend-inner-icon="mdi-magnify"
|
prepend-inner-icon="mdi-magnify"
|
||||||
@@ -173,7 +181,7 @@
|
|||||||
<script lang="js">
|
<script lang="js">
|
||||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
import PROPERTIES, { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||||
import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue';
|
import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue';
|
||||||
import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js';
|
import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js';
|
||||||
@@ -235,7 +243,11 @@ export default {
|
|||||||
},
|
},
|
||||||
toolbarColor(){
|
toolbarColor(){
|
||||||
return getThemeColor('secondary');
|
return getThemeColor('secondary');
|
||||||
}
|
},
|
||||||
|
docsPath() {
|
||||||
|
const propDef = PROPERTIES[this.type];
|
||||||
|
return propDef && propDef.docsPath;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
type(newType){
|
type(newType){
|
||||||
@@ -259,6 +271,15 @@ export default {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
helpDialog() {
|
||||||
|
this.$store.commit('pushDialogStack', {
|
||||||
|
component: 'help-dialog',
|
||||||
|
elementId: 'help-button',
|
||||||
|
data: {
|
||||||
|
path: this.docsPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
searchChanged(val, ack){
|
searchChanged(val, ack){
|
||||||
this._subs.searchLibraryNodes.setData('searchTerm', val);
|
this._subs.searchLibraryNodes.setData('searchTerm', val);
|
||||||
this._subs.searchLibraryNodes.setData('limit', undefined);
|
this._subs.searchLibraryNodes.setData('limit', undefined);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
:embedded="embedded"
|
:embedded="embedded"
|
||||||
@duplicate="duplicate"
|
@duplicate="duplicate"
|
||||||
@move="move"
|
@move="move"
|
||||||
|
@copy="copy"
|
||||||
@remove="remove"
|
@remove="remove"
|
||||||
@toggle-editing="editing = !editing"
|
@toggle-editing="editing = !editing"
|
||||||
@color-changed="value => change({path: ['color'], value})"
|
@color-changed="value => change({path: ['color'], value})"
|
||||||
@@ -95,10 +96,13 @@
|
|||||||
import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormIndex.js';
|
import propertyFormIndex from '/imports/ui/properties/forms/shared/propertyFormIndex.js';
|
||||||
import propertyViewerIndex from '/imports/ui/properties/viewers/shared/propertyViewerIndex.js';
|
import propertyViewerIndex from '/imports/ui/properties/viewers/shared/propertyViewerIndex.js';
|
||||||
import { get } from 'lodash';
|
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 { organizeDoc } from '/imports/api/parenting/organizeMethods.js';
|
||||||
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||||
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
|
import getPropertyTitle from '/imports/ui/properties/shared/getPropertyTitle.js';
|
||||||
|
import copyLibraryNodeTo from '/imports/api/library/methods/copyLibraryNodeTo.js';
|
||||||
|
|
||||||
let formIndex = {};
|
let formIndex = {};
|
||||||
for (let key in propertyFormIndex){
|
for (let key in propertyFormIndex){
|
||||||
@@ -126,7 +130,7 @@
|
|||||||
},
|
},
|
||||||
reactiveProvide: {
|
reactiveProvide: {
|
||||||
name: 'context',
|
name: 'context',
|
||||||
include: ['editPermission', 'isLibraryForm'],
|
include: ['editPermission', 'copyPermission', 'isLibraryForm'],
|
||||||
},
|
},
|
||||||
data(){return {
|
data(){return {
|
||||||
editing: !!this.startInEditTab,
|
editing: !!this.startInEditTab,
|
||||||
@@ -162,6 +166,14 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
copyPermission(){
|
||||||
|
try {
|
||||||
|
assertDocCopyPermission(this.model, Meteor.userId());
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
getPropertyName,
|
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}){
|
change({path, value, ack}){
|
||||||
updateLibraryNode.call({_id: this.currentId, path, value}, (error) =>{
|
updateLibraryNode.call({_id: this.currentId, path, value}, (error) =>{
|
||||||
if (ack){
|
if (ack){
|
||||||
|
|||||||
@@ -12,6 +12,13 @@
|
|||||||
:value="model.color"
|
:value="model.color"
|
||||||
@input="value => change({path: ['color'], value})"
|
@input="value => change({path: ['color'], value})"
|
||||||
/>
|
/>
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
data-id="help-button"
|
||||||
|
@click="helpDialog"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-help</v-icon>
|
||||||
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<component
|
<component
|
||||||
:is="type"
|
: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 schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js';
|
||||||
import ColorPicker from '/imports/ui/components/ColorPicker.vue';
|
import ColorPicker from '/imports/ui/components/ColorPicker.vue';
|
||||||
import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex.js';
|
import propertySchemasIndex from '/imports/api/properties/propertySchemasIndex.js';
|
||||||
|
import PROPERTIES from '/imports/constants/PROPERTIES.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -77,6 +85,12 @@ export default {
|
|||||||
isLibraryForm: true,
|
isLibraryForm: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
docsPath() {
|
||||||
|
const propDef = PROPERTIES[this.type];
|
||||||
|
return propDef && propDef.docsPath;
|
||||||
|
},
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
type(newType) {
|
type(newType) {
|
||||||
if (!newType) return;
|
if (!newType) return;
|
||||||
@@ -87,6 +101,17 @@ export default {
|
|||||||
this.model = model;
|
this.model = model;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
helpDialog() {
|
||||||
|
this.$store.commit('pushDialogStack', {
|
||||||
|
component: 'help-dialog',
|
||||||
|
elementId: 'help-button',
|
||||||
|
data: {
|
||||||
|
path: this.docsPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
@click="$store.dispatch('popDialogStack', node._id)"
|
@click="$store.dispatch('popDialogStack', node._id)"
|
||||||
>
|
>
|
||||||
Move
|
{{ action || 'Move' }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
</dialog-base>
|
</dialog-base>
|
||||||
@@ -30,6 +30,12 @@ export default {
|
|||||||
DialogBase,
|
DialogBase,
|
||||||
LibraryAndNode,
|
LibraryAndNode,
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
action: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
node: undefined,
|
node: undefined,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container class="documentation">
|
<v-container class="documentation">
|
||||||
<v-row>
|
<v-row justify="center">
|
||||||
<v-col cols="12">
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
lg="8"
|
||||||
|
>
|
||||||
<v-fade-transition mode="out-in">
|
<v-fade-transition mode="out-in">
|
||||||
<v-card
|
<v-card
|
||||||
v-if="doc"
|
v-if="doc"
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container class="documentation">
|
<v-container class="documentation">
|
||||||
<v-row>
|
<v-row justify="center">
|
||||||
<v-col cols="12">
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
lg="8"
|
||||||
|
>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-text class="markdown">
|
<v-card-text class="markdown">
|
||||||
<h1>Functions</h1>
|
<h1>Functions</h1>
|
||||||
|
|||||||
80
app/imports/ui/properties/components/actions/EventButton.vue
Normal 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>
|
||||||
@@ -5,54 +5,19 @@
|
|||||||
@mouseover="hasClickListener ? hovering = true : undefined"
|
@mouseover="hasClickListener ? hovering = true : undefined"
|
||||||
@mouseleave="hasClickListener ? hovering = false : undefined"
|
@mouseleave="hasClickListener ? hovering = false : undefined"
|
||||||
>
|
>
|
||||||
<div class="layout align-center">
|
<attribute-card-content :model="model" />
|
||||||
<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>
|
|
||||||
<card-highlight :active="hasClickListener && hovering" />
|
<card-highlight :active="hasClickListener && hovering" />
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<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 CardHighlight from '/imports/ui/components/CardHighlight.vue';
|
||||||
|
import AttributeCardContent from '/imports/ui/properties/components/attributes/AttributeCardContent.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
RollPopup,
|
|
||||||
CardHighlight,
|
CardHighlight,
|
||||||
},
|
AttributeCardContent,
|
||||||
inject: {
|
|
||||||
context: {
|
|
||||||
default: {},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
model: {
|
model: {
|
||||||
@@ -68,41 +33,11 @@
|
|||||||
hasClickListener(){
|
hasClickListener(){
|
||||||
return this.$listeners && !!this.$listeners.click
|
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: {
|
methods: {
|
||||||
signed: numberToSignedString,
|
|
||||||
click(e){
|
click(e){
|
||||||
this.$emit('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>
|
</script>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
|
||||||
.value {
|
|
||||||
min-width: 72px;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
style="min-height: 42px;"
|
style="min-height: 42px;"
|
||||||
:class="{ hover }"
|
:class="{ hover }"
|
||||||
class="my-1 health-bar"
|
class="my-1 health-bar"
|
||||||
:data-id="_id"
|
:data-id="model._id"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="subheading text-truncate pa-2 name"
|
class="subheading text-truncate pa-2 name"
|
||||||
@@ -14,14 +14,10 @@
|
|||||||
@mouseleave="hover = false"
|
@mouseleave="hover = false"
|
||||||
@click="$emit('click')"
|
@click="$emit('click')"
|
||||||
>
|
>
|
||||||
{{ name }}
|
{{ model.name }}
|
||||||
</div>
|
</div>
|
||||||
<v-flex
|
<v-flex
|
||||||
style="
|
style="height: 24px; flex-basis: 300px; flex-grow: 100;"
|
||||||
height: 24px;
|
|
||||||
flex-basis: 300px;
|
|
||||||
flex-grow: 100;
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
column
|
column
|
||||||
@@ -50,8 +46,7 @@
|
|||||||
'white--text': isTextLight,
|
'white--text': isTextLight,
|
||||||
'black--text': !isTextLight,
|
'black--text': !isTextLight,
|
||||||
}"
|
}"
|
||||||
style="
|
style="font-size: 15px;
|
||||||
font-size: 15px;
|
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -59,30 +54,30 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
text-align: center;
|
text-align: center;"
|
||||||
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
{{ value }} / {{ maxValue }}
|
{{ model.value }} / {{ model.total }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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
|
<increment-menu
|
||||||
v-show="editing"
|
:value="model.value"
|
||||||
:value="value"
|
|
||||||
:open="editing"
|
:open="editing"
|
||||||
@change="changeIncrementMenu"
|
@change="changeIncrementMenu"
|
||||||
@close="cancelEdit"
|
@close="cancelEdit"
|
||||||
/>
|
/>
|
||||||
</transition>
|
</v-menu>
|
||||||
<transition name="background-transition">
|
|
||||||
<div
|
|
||||||
v-if="editing"
|
|
||||||
class="page-tint"
|
|
||||||
@click="cancelEdit"
|
|
||||||
/>
|
|
||||||
</transition>
|
|
||||||
</v-flex>
|
</v-flex>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
</template>
|
</template>
|
||||||
@@ -104,31 +99,9 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
model: {
|
||||||
type: Number,
|
type: Object,
|
||||||
default: undefined,
|
required: true,
|
||||||
},
|
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
_id: String,
|
_id: String,
|
||||||
},
|
},
|
||||||
@@ -136,24 +109,29 @@ export default {
|
|||||||
return {
|
return {
|
||||||
editing: false,
|
editing: false,
|
||||||
hover: false,
|
hover: false,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fillFraction() {
|
fillFraction() {
|
||||||
let fraction = this.value / this.maxValue;
|
let fraction = this.model.value / this.model.total;
|
||||||
if (fraction < 0) fraction = 0;
|
if (fraction < 0) fraction = 0;
|
||||||
if (fraction > 1) fraction = 1;
|
if (fraction > 1) fraction = 1;
|
||||||
return fraction;
|
return fraction;
|
||||||
},
|
},
|
||||||
|
color() {
|
||||||
|
return this.model.color || this.$vuetify.theme.currentTheme.primary
|
||||||
|
},
|
||||||
barColor() {
|
barColor() {
|
||||||
const fraction = this.value / this.maxValue;
|
const fraction = this.model.value / this.model.total;
|
||||||
if (!Number.isFinite(fraction)) return this.color;
|
if (!Number.isFinite(fraction)) return this.color;
|
||||||
if (fraction > 0.5) {
|
if (fraction > 0.5) {
|
||||||
return this.color;
|
return this.color;
|
||||||
} else if (this.midColor && this.lowColor) {
|
} else if (this.model.healthBarColorMid && this.model.healthBarColorLow) {
|
||||||
return chroma.mix(this.lowColor, this.midColor, fraction * 2).hex();
|
return chroma.mix(this.model.healthBarColorLow, this.model.healthBarColorMid, fraction * 2).hex();
|
||||||
} else if (this.midColor) {
|
} else if (this.model.healthBarColorMid) {
|
||||||
return this.midColor;
|
return this.model.healthBarColorMid;
|
||||||
}
|
}
|
||||||
return this.color;
|
return this.color;
|
||||||
},
|
},
|
||||||
@@ -166,7 +144,7 @@ export default {
|
|||||||
isTextLight() {
|
isTextLight() {
|
||||||
return isDarkColor(this.barBackgroundColor);
|
return isDarkColor(this.barBackgroundColor);
|
||||||
/* Change color at the halfway mark
|
/* Change color at the halfway mark
|
||||||
const fraction = this.value / this.maxValue;
|
const fraction = this.model.value / this.model.total;
|
||||||
if (fraction >= 0.5){
|
if (fraction >= 0.5){
|
||||||
return isDarkColor(this.barColor);
|
return isDarkColor(this.barColor);
|
||||||
} else {
|
} else {
|
||||||
@@ -176,8 +154,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
edit() {
|
edit(e) {
|
||||||
this.editing = true;
|
e.preventDefault()
|
||||||
|
this.editing = false;
|
||||||
|
this.x = e.clientX - 165;
|
||||||
|
this.y = e.clientY - 24;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.editing = true
|
||||||
|
});
|
||||||
},
|
},
|
||||||
cancelEdit() {
|
cancelEdit() {
|
||||||
this.editing = false;
|
this.editing = false;
|
||||||
@@ -199,6 +183,10 @@ export default {
|
|||||||
z-index: 7;
|
z-index: 7;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-menu-shadow {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -3,13 +3,7 @@
|
|||||||
<health-bar
|
<health-bar
|
||||||
v-for="attribute in attributes"
|
v-for="attribute in attributes"
|
||||||
:key="attribute._id"
|
:key="attribute._id"
|
||||||
:value="attribute.value"
|
:model="attribute"
|
||||||
:max-value="attribute.total"
|
|
||||||
:name="attribute.name"
|
|
||||||
:color="attribute.color"
|
|
||||||
:mid-color="attribute.healthBarColorMid"
|
|
||||||
:low-color="attribute.healthBarColorLow"
|
|
||||||
:_id="attribute._id"
|
|
||||||
@change="e => $emit('change', {_id: attribute._id, change: e})"
|
@change="e => $emit('change', {_id: attribute._id, change: e})"
|
||||||
@click="e => $emit('click', {_id: attribute._id})"
|
@click="e => $emit('click', {_id: attribute._id})"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ export default {
|
|||||||
removed: { $ne: true },
|
removed: { $ne: true },
|
||||||
inactive: { $ne: true },
|
inactive: { $ne: true },
|
||||||
overridden: { $ne: true },
|
overridden: { $ne: true },
|
||||||
|
$nor: [
|
||||||
|
{ hideWhenTotalZero: true, total: 0 },
|
||||||
|
{ hideWhenValueZero: true, value: 0 },
|
||||||
|
],
|
||||||
};
|
};
|
||||||
if (creature.settings.hideUnusedStats) {
|
if (creature.settings.hideUnusedStats) {
|
||||||
filter.hide = { $ne: true };
|
filter.hide = { $ne: true };
|
||||||
|
|||||||
@@ -3,57 +3,26 @@
|
|||||||
class="resource-card"
|
class="resource-card"
|
||||||
:class="hover ? 'elevation-8': ''"
|
:class="hover ? 'elevation-8': ''"
|
||||||
>
|
>
|
||||||
<v-layout>
|
<resource-card-content
|
||||||
<div class="buttons layout column justify-center pl-3">
|
:model="model"
|
||||||
<v-btn
|
:hover="hover"
|
||||||
icon
|
@mouseover="hover = true"
|
||||||
small
|
@mouseleave="hover = false"
|
||||||
:disabled="(model.value >= model.total && !model.ignoreUpperLimit) || context.editPermission === false"
|
@click="$emit('click')"
|
||||||
@click="increment(1)"
|
@change="e => $emit('change', e)"
|
||||||
>
|
/>
|
||||||
<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>
|
|
||||||
<card-highlight :active="hover" />
|
<card-highlight :active="hover" />
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
|
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
|
||||||
|
import ResourceCardContent from '/imports/ui/properties/components/attributes/ResourceCardContent.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
CardHighlight,
|
CardHighlight,
|
||||||
|
ResourceCardContent,
|
||||||
},
|
},
|
||||||
inject: {
|
inject: {
|
||||||
context: { default: {} }
|
context: { default: {} }
|
||||||
@@ -69,46 +38,16 @@ export default {
|
|||||||
hover: false,
|
hover: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
click(e) {
|
|
||||||
this.$emit('click', e);
|
|
||||||
},
|
|
||||||
increment(value) {
|
|
||||||
this.$emit('change', { type: 'increment', value })
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css">
|
||||||
.resource-card {
|
.resource-card {
|
||||||
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.resource-card>div {
|
.resource-card > div {
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
padding-bottom: 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>
|
</style>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -61,7 +61,6 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
dark: Boolean,
|
dark: Boolean,
|
||||||
hideCastButton: Boolean,
|
|
||||||
disabled: Boolean,
|
disabled: Boolean,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|||||||
30
app/imports/ui/properties/components/buffs/BuffListItem.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
@change="changeQuantity"
|
@change="changeQuantity"
|
||||||
/>
|
/>
|
||||||
</v-list-item-action>
|
</v-list-item-action>
|
||||||
<v-list-item-action>
|
<v-list-item-action class="drag-handle">
|
||||||
<v-icon
|
<v-icon
|
||||||
:disabled="context.editPermission === false"
|
:disabled="context.editPermission === false"
|
||||||
style="height: 100%; width: 40px; cursor: move;"
|
style="height: 100%; width: 40px; cursor: move;"
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<v-card
|
<v-card
|
||||||
:hover="hasClickListener"
|
:class="hover ? 'elevation-8': ''"
|
||||||
@click="click"
|
@click="click"
|
||||||
|
@mouseover="hover = true"
|
||||||
|
@mouseleave="hover = false"
|
||||||
>
|
>
|
||||||
<div class="layout align-center">
|
<div class="layout align-center">
|
||||||
<div
|
<div
|
||||||
@@ -18,19 +20,29 @@
|
|||||||
{{ model.name }}
|
{{ model.name }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
</div>
|
</div>
|
||||||
|
<card-highlight :active="hover" />
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import flipToggle from '/imports/api/creature/creatureProperties/methods/flipToggle.js';
|
import flipToggle from '/imports/api/creature/creatureProperties/methods/flipToggle.js';
|
||||||
|
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
CardHighlight,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
model: {
|
model: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hover: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasClickListener(){
|
hasClickListener(){
|
||||||
return this.$listeners && !!this.$listeners.click
|
return this.$listeners && !!this.$listeners.click
|
||||||
|
|||||||
@@ -41,6 +41,17 @@
|
|||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</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-slide-x-transition mode="out-in">
|
||||||
<v-switch
|
<v-switch
|
||||||
v-if="!isAttack"
|
v-if="!isAttack"
|
||||||
@@ -154,15 +165,10 @@
|
|||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
<smart-select
|
<reset-selector
|
||||||
label="Reset"
|
|
||||||
clearable
|
|
||||||
hint="When number of uses used should be reset to zero"
|
hint="When number of uses used should be reset to zero"
|
||||||
style="flex-basis: 300px;"
|
|
||||||
:items="resetOptions"
|
|
||||||
:value="model.reset"
|
:value="model.reset"
|
||||||
:error-messages="errors.reset"
|
:error-messages="errors.reset"
|
||||||
:menu-props="{auto: true, lazy: true}"
|
|
||||||
@change="change('reset', ...arguments)"
|
@change="change('reset', ...arguments)"
|
||||||
/>
|
/>
|
||||||
</form-section>
|
</form-section>
|
||||||
@@ -171,77 +177,74 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import ResourcesForm from '/imports/ui/properties/forms/ResourcesForm.vue';
|
import ResourcesForm from '/imports/ui/properties/forms/ResourcesForm.vue';
|
||||||
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
||||||
import IconColorMenu from '/imports/ui/properties/forms/shared/IconColorMenu.vue';
|
import IconColorMenu from '/imports/ui/properties/forms/shared/IconColorMenu.vue';
|
||||||
|
import ResetSelector from '/imports/ui/components/ResetSelector.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ResourcesForm,
|
ResourcesForm,
|
||||||
IconColorMenu,
|
IconColorMenu,
|
||||||
},
|
ResetSelector,
|
||||||
mixins: [propertyFormMixin],
|
},
|
||||||
data(){
|
mixins: [propertyFormMixin],
|
||||||
let data = {
|
data(){
|
||||||
actionTypes: [
|
let data = {
|
||||||
{
|
actionTypes: [
|
||||||
text: 'Action',
|
{
|
||||||
value: 'action',
|
text: 'Action',
|
||||||
}, {
|
value: 'action',
|
||||||
text: 'Bonus action',
|
}, {
|
||||||
value: 'bonus',
|
text: 'Bonus action',
|
||||||
}, {
|
value: 'bonus',
|
||||||
text: 'Attack action',
|
}, {
|
||||||
value: 'attack',
|
text: 'Attack action',
|
||||||
help: 'Attack actions replace a single attack when you choose to use your Action to attack',
|
value: 'attack',
|
||||||
}, {
|
help: 'Attack actions replace a single attack when you choose to use your Action to attack',
|
||||||
text: 'Reaction',
|
}, {
|
||||||
value: 'reaction',
|
text: 'Reaction',
|
||||||
}, {
|
value: 'reaction',
|
||||||
text: 'Free action',
|
}, {
|
||||||
value: 'free',
|
text: 'Free action',
|
||||||
help: 'You can take one free action on your turn without using an action or bonus 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',
|
text: 'Long action',
|
||||||
help: 'Long actions take longer than one turn to complete'
|
value: 'long',
|
||||||
},
|
help: 'Long actions take longer than one turn to complete'
|
||||||
],
|
}, {
|
||||||
targetOptions: [
|
text: 'Event',
|
||||||
{
|
value: 'event',
|
||||||
text: 'Self',
|
help: 'Events are actions that happen to the character like rests or dawn'
|
||||||
value: 'self',
|
},
|
||||||
}, {
|
],
|
||||||
text: 'Single target',
|
targetOptions: [
|
||||||
value: 'singleTarget',
|
{
|
||||||
}, {
|
text: 'Self',
|
||||||
text: 'Multiple targets',
|
value: 'self',
|
||||||
value: 'multipleTargets',
|
}, {
|
||||||
},
|
text: 'Single target',
|
||||||
],
|
value: 'singleTarget',
|
||||||
resetOptions: [
|
}, {
|
||||||
{
|
text: 'Multiple targets',
|
||||||
text: 'Short rest',
|
value: 'multipleTargets',
|
||||||
value: 'shortRest',
|
},
|
||||||
}, {
|
],
|
||||||
text: 'Long rest',
|
attackSwitch: false,
|
||||||
value: 'longRest',
|
};
|
||||||
}
|
data.actionTypeHints = {};
|
||||||
],
|
data.actionTypes.forEach(type => {
|
||||||
attackSwitch: false,
|
data.actionTypeHints[type.value] = type.help;
|
||||||
};
|
});
|
||||||
data.actionTypeHints = {};
|
return data;
|
||||||
data.actionTypes.forEach(type => {
|
},
|
||||||
data.actionTypeHints[type.value] = type.help;
|
computed: {
|
||||||
});
|
isAttack(){
|
||||||
return data;
|
return this.attackSwitch || !!this.model.attackRoll?.calculation
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
isAttack(){
|
|
||||||
return this.attackSwitch || !!this.model.attackRoll?.calculation
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
|
|||||||
@@ -106,10 +106,17 @@
|
|||||||
/>
|
/>
|
||||||
<smart-switch
|
<smart-switch
|
||||||
label="Ignore damage"
|
label="Ignore damage"
|
||||||
|
class="mr-4"
|
||||||
:value="model.healthBarNoDamage"
|
:value="model.healthBarNoDamage"
|
||||||
:error-messages="errors.healthBarNoDamage"
|
:error-messages="errors.healthBarNoDamage"
|
||||||
@change="change('healthBarNoDamage', ...arguments)"
|
@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>
|
||||||
<v-layout wrap>
|
<v-layout wrap>
|
||||||
<text-field
|
<text-field
|
||||||
@@ -125,14 +132,20 @@
|
|||||||
/>
|
/>
|
||||||
<smart-switch
|
<smart-switch
|
||||||
label="Ignore healing"
|
label="Ignore healing"
|
||||||
|
class="mr-4"
|
||||||
:value="model.healthBarNoHealing"
|
:value="model.healthBarNoHealing"
|
||||||
:error-messages="errors.healthBarNoHealing"
|
:error-messages="errors.healthBarNoHealing"
|
||||||
@change="change('healthBarNoHealing', ...arguments)"
|
@change="change('healthBarNoHealing', ...arguments)"
|
||||||
/>
|
/>
|
||||||
|
<smart-switch
|
||||||
|
label="Prevent healing overflow"
|
||||||
|
:value="model.healthBarNoHealingOverflow"
|
||||||
|
:error-messages="errors.healthBarNoHealingOverflow"
|
||||||
|
@change="change('healthBarNoHealingOverflow', ...arguments)"
|
||||||
|
/>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
</form-section>
|
</form-section>
|
||||||
</v-expand-transition>
|
</v-expand-transition>
|
||||||
|
|
||||||
<form-section
|
<form-section
|
||||||
v-if="$slots.children"
|
v-if="$slots.children"
|
||||||
name="Children"
|
name="Children"
|
||||||
@@ -151,26 +164,74 @@
|
|||||||
@change="change('tags', ...arguments)"
|
@change="change('tags', ...arguments)"
|
||||||
/>
|
/>
|
||||||
<div class="layout column align-center">
|
<div class="layout column align-center">
|
||||||
<smart-switch
|
<v-row dense>
|
||||||
v-if="model.attributeType !== 'hitDice'"
|
<v-col
|
||||||
label="Allow decimal values"
|
cols="12"
|
||||||
class="no-flex"
|
sm="6"
|
||||||
:value="model.decimal"
|
md="4"
|
||||||
:error-messages="errors.decimal"
|
>
|
||||||
@change="change('decimal', ...arguments)"
|
<smart-switch
|
||||||
/>
|
v-if="model.attributeType !== 'hitDice'"
|
||||||
<smart-switch
|
label="Allow decimal values"
|
||||||
label="Can be damaged into negative values"
|
class="mx-4"
|
||||||
:value="model.ignoreLowerLimit"
|
:value="model.decimal"
|
||||||
:error-messages="errors.ignoreLowerLimit"
|
:error-messages="errors.decimal"
|
||||||
@change="change('ignoreLowerLimit', ...arguments)"
|
@change="change('decimal', ...arguments)"
|
||||||
/>
|
/>
|
||||||
<smart-switch
|
</v-col>
|
||||||
label="Can be incremented above total"
|
<v-col
|
||||||
:value="model.ignoreUpperLimit"
|
cols="12"
|
||||||
:error-messages="errors.ignoreUpperLimit"
|
sm="6"
|
||||||
@change="change('ignoreUpperLimit', ...arguments)"
|
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
|
<div
|
||||||
class="layout justify-center"
|
class="layout justify-center"
|
||||||
style="align-self: stretch;"
|
style="align-self: stretch;"
|
||||||
@@ -189,16 +250,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layout wrap">
|
<div class="layout wrap">
|
||||||
<smart-select
|
<reset-selector
|
||||||
v-if="model.attributeType !== 'hitDice'"
|
v-if="model.attributeType !== 'hitDice'"
|
||||||
label="Reset"
|
|
||||||
clearable
|
|
||||||
style="flex-basis: 300px;"
|
|
||||||
hint="When damage should be reset to zero"
|
hint="When damage should be reset to zero"
|
||||||
:items="resetOptions"
|
|
||||||
:value="model.reset"
|
:value="model.reset"
|
||||||
:error-messages="errors.reset"
|
:error-messages="errors.reset"
|
||||||
:menu-props="{auto: true, lazy: true}"
|
|
||||||
@change="change('reset', ...arguments)"
|
@change="change('reset', ...arguments)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 FormSections from '/imports/ui/properties/forms/shared/FormSections.vue';
|
||||||
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
||||||
import ColorPicker from '/imports/ui/components/ColorPicker.vue';
|
import ColorPicker from '/imports/ui/components/ColorPicker.vue';
|
||||||
|
import ResetSelector from '/imports/ui/components/ResetSelector.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FormSection,
|
FormSection,
|
||||||
FormSections,
|
FormSections,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
|
ResetSelector,
|
||||||
},
|
},
|
||||||
mixins: [propertyFormMixin],
|
mixins: [propertyFormMixin],
|
||||||
inject: {
|
inject: {
|
||||||
|
|||||||
@@ -26,6 +26,12 @@
|
|||||||
:value="model.tags"
|
:value="model.tags"
|
||||||
@change="change('tags', ...arguments)"
|
@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-section>
|
||||||
</form-sections>
|
</form-sections>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -278,15 +278,10 @@
|
|||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
<smart-select
|
<reset-selector
|
||||||
label="Reset"
|
|
||||||
clearable
|
|
||||||
hint="When number of uses used should be reset to zero"
|
hint="When number of uses used should be reset to zero"
|
||||||
style="flex-basis: 300px;"
|
|
||||||
:items="resetOptions"
|
|
||||||
:value="model.reset"
|
:value="model.reset"
|
||||||
:error-messages="errors.reset"
|
:error-messages="errors.reset"
|
||||||
:menu-props="{auto: true, lazy: true}"
|
|
||||||
@change="change('reset', ...arguments)"
|
@change="change('reset', ...arguments)"
|
||||||
/>
|
/>
|
||||||
</form-section>
|
</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 propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
||||||
import IconColorMenu from '/imports/ui/properties/forms/shared/IconColorMenu.vue';
|
import IconColorMenu from '/imports/ui/properties/forms/shared/IconColorMenu.vue';
|
||||||
import ResourcesForm from '/imports/ui/properties/forms/ResourcesForm.vue';
|
import ResourcesForm from '/imports/ui/properties/forms/ResourcesForm.vue';
|
||||||
|
import ResetSelector from '/imports/ui/components/ResetSelector.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -325,6 +321,7 @@ export default {
|
|||||||
FormSection,
|
FormSection,
|
||||||
IconColorMenu,
|
IconColorMenu,
|
||||||
ResourcesForm,
|
ResourcesForm,
|
||||||
|
ResetSelector,
|
||||||
},
|
},
|
||||||
mixins: [propertyFormMixin],
|
mixins: [propertyFormMixin],
|
||||||
data() {
|
data() {
|
||||||
@@ -401,15 +398,6 @@ export default {
|
|||||||
value: 'multipleTargets',
|
value: 'multipleTargets',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
resetOptions: [
|
|
||||||
{
|
|
||||||
text: 'Short rest',
|
|
||||||
value: 'shortRest',
|
|
||||||
}, {
|
|
||||||
text: 'Long rest',
|
|
||||||
value: 'longRest',
|
|
||||||
}
|
|
||||||
],
|
|
||||||
attackSwitch: false,
|
attackSwitch: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -27,6 +27,15 @@
|
|||||||
$emit('change', {path: ['maxPrepared', ...path], value, ack})"
|
$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
|
<computed-field
|
||||||
label="Spell save DC"
|
label="Spell save DC"
|
||||||
hint="The spell save DC of spells in this list"
|
hint="The spell save DC of spells in this list"
|
||||||
@@ -67,9 +76,46 @@
|
|||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
||||||
|
import createListOfProperties from '/imports/ui/properties/forms/shared/lists/createListOfProperties.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [propertyFormMixin],
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||||
|
|
||||||
export default function createListOfProperties(filter = {}){
|
export default function createListOfProperties(filter = {}, getNamesWithValues) {
|
||||||
filter.removed = {$ne: true};
|
filter.removed = { $ne: true };
|
||||||
let propertyList = [];
|
let propertyList = [];
|
||||||
let variableNames = new Set();
|
let variableNames = new Set();
|
||||||
function addUniquePropertys(property){
|
function addUniquePropertys(property) {
|
||||||
if (property.variableName && !variableNames.has(property.variableName)){
|
if (property.variableName && !variableNames.has(property.variableName)) {
|
||||||
variableNames.add(property.variableName);
|
variableNames.add(property.variableName);
|
||||||
propertyList.push({
|
propertyList.push({
|
||||||
text: property.name || property.variableName,
|
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);
|
CreatureProperties.find(filter, options).forEach(addUniquePropertys);
|
||||||
LibraryNodes.find(filter, options).forEach(addUniquePropertys);
|
LibraryNodes.find(filter, options).forEach(addUniquePropertys);
|
||||||
|
if (getNamesWithValues) return propertyList;
|
||||||
return Array.from(variableNames);
|
return Array.from(variableNames);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
const ActionForm = () => import('/imports/ui/properties/forms/ActionForm.vue');
|
import ActionForm from '/imports/ui/properties/forms/ActionForm.vue';
|
||||||
const AdjustmentForm = () => import('/imports/ui/properties/forms/AdjustmentForm.vue');
|
import AdjustmentForm from '/imports/ui/properties/forms/AdjustmentForm.vue';
|
||||||
const AttributeForm = () => import('/imports/ui/properties/forms/AttributeForm.vue');
|
import AttributeForm from '/imports/ui/properties/forms/AttributeForm.vue';
|
||||||
const BuffForm = () => import('/imports/ui/properties/forms/BuffForm.vue');
|
import BuffForm from '/imports/ui/properties/forms/BuffForm.vue';
|
||||||
const BuffRemoverForm = () => import('/imports/ui/properties/forms/BuffRemoverForm.vue');
|
import BuffRemoverForm from '/imports/ui/properties/forms/BuffRemoverForm.vue';
|
||||||
const BranchForm = () => import('/imports/ui/properties/forms/BranchForm.vue');
|
import BranchForm from '/imports/ui/properties/forms/BranchForm.vue';
|
||||||
const ClassForm = () => import('/imports/ui/properties/forms/ClassForm.vue');
|
import ClassForm from '/imports/ui/properties/forms/ClassForm.vue';
|
||||||
const ClassLevelForm = () => import('/imports/ui/properties/forms/ClassLevelForm.vue');
|
import ClassLevelForm from '/imports/ui/properties/forms/ClassLevelForm.vue';
|
||||||
const ConstantForm = () => import('/imports/ui/properties/forms/ConstantForm.vue');
|
import ConstantForm from '/imports/ui/properties/forms/ConstantForm.vue';
|
||||||
const ContainerForm = () => import('/imports/ui/properties/forms/ContainerForm.vue');
|
import ContainerForm from '/imports/ui/properties/forms/ContainerForm.vue';
|
||||||
const DamageForm = () => import('/imports/ui/properties/forms/DamageForm.vue');
|
import DamageForm from '/imports/ui/properties/forms/DamageForm.vue';
|
||||||
const DamageMultiplierForm = () => import('/imports/ui/properties/forms/DamageMultiplierForm.vue');
|
import DamageMultiplierForm from '/imports/ui/properties/forms/DamageMultiplierForm.vue';
|
||||||
const EffectForm = () => import('/imports/ui/properties/forms/EffectForm.vue');
|
import EffectForm from '/imports/ui/properties/forms/EffectForm.vue';
|
||||||
const FeatureForm = () => import('/imports/ui/properties/forms/FeatureForm.vue');
|
import FeatureForm from '/imports/ui/properties/forms/FeatureForm.vue';
|
||||||
const FolderForm = () => import('/imports/ui/properties/forms/FolderForm.vue');
|
import FolderForm from '/imports/ui/properties/forms/FolderForm.vue';
|
||||||
const ItemForm = () => import('/imports/ui/properties/forms/ItemForm.vue');
|
import ItemForm from '/imports/ui/properties/forms/ItemForm.vue';
|
||||||
const NoteForm = () => import('/imports/ui/properties/forms/NoteForm.vue');
|
import NoteForm from '/imports/ui/properties/forms/NoteForm.vue';
|
||||||
const PointBuyForm = () => import('/imports/ui/properties/forms/PointBuyForm.vue');
|
import PointBuyForm from '/imports/ui/properties/forms/PointBuyForm.vue';
|
||||||
const ProficiencyForm = () => import('/imports/ui/properties/forms/ProficiencyForm.vue');
|
import ProficiencyForm from '/imports/ui/properties/forms/ProficiencyForm.vue';
|
||||||
const ReferenceForm = () => import('/imports/ui/properties/forms/ReferenceForm.vue');
|
import ReferenceForm from '/imports/ui/properties/forms/ReferenceForm.vue';
|
||||||
const RollForm = () => import('/imports/ui/properties/forms/RollForm.vue');
|
import RollForm from '/imports/ui/properties/forms/RollForm.vue';
|
||||||
const SavingThrowForm = () => import('/imports/ui/properties/forms/SavingThrowForm.vue');
|
import SavingThrowForm from '/imports/ui/properties/forms/SavingThrowForm.vue';
|
||||||
const SkillForm = () => import('/imports/ui/properties/forms/SkillForm.vue');
|
import SkillForm from '/imports/ui/properties/forms/SkillForm.vue';
|
||||||
const SlotForm = () => import('/imports/ui/properties/forms/SlotForm.vue');
|
import SlotForm from '/imports/ui/properties/forms/SlotForm.vue';
|
||||||
const SlotFillerForm = () => import('/imports/ui/properties/forms/SlotFillerForm.vue');
|
import SlotFillerForm from '/imports/ui/properties/forms/SlotFillerForm.vue';
|
||||||
const SpellListForm = () => import('/imports/ui/properties/forms/SpellListForm.vue');
|
import SpellListForm from '/imports/ui/properties/forms/SpellListForm.vue';
|
||||||
const SpellForm = () => import('/imports/ui/properties/forms/SpellForm.vue');
|
import SpellForm from '/imports/ui/properties/forms/SpellForm.vue';
|
||||||
const ToggleForm = () => import('/imports/ui/properties/forms/ToggleForm.vue');
|
import ToggleForm from '/imports/ui/properties/forms/ToggleForm.vue';
|
||||||
const TriggerForm = () => import('/imports/ui/properties/forms/TriggerForm.vue');
|
import TriggerForm from '/imports/ui/properties/forms/TriggerForm.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
action: ActionForm,
|
action: ActionForm,
|
||||||
|
|||||||
@@ -13,6 +13,18 @@
|
|||||||
center
|
center
|
||||||
:calculation="model.maxPrepared"
|
: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
|
<property-field
|
||||||
name="Spell Save DC"
|
name="Spell Save DC"
|
||||||
large
|
large
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
>
|
>
|
||||||
<slot>
|
<slot>
|
||||||
<template v-if="value !== undefined">
|
<template v-if="value !== undefined">
|
||||||
{{ value }}
|
{{ valueText }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="calculation !== undefined">
|
<template v-else-if="calculation !== undefined">
|
||||||
{{ calculationText }}
|
{{ calculationText }}
|
||||||
@@ -117,6 +117,13 @@ export default {
|
|||||||
if (!this.calculation) return;
|
if (!this.calculation) return;
|
||||||
return typeof this.calculation.value === 'string'
|
return typeof this.calculation.value === 'string'
|
||||||
},
|
},
|
||||||
|
valueText() {
|
||||||
|
if (this.signed) {
|
||||||
|
return numberToSignedString(this.value);
|
||||||
|
} else {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
},
|
||||||
calculationText(){
|
calculationText(){
|
||||||
const calculation = this.calculation;
|
const calculation = this.calculation;
|
||||||
if (!calculation) {
|
if (!calculation) {
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
const ActionViewer = () => import ('/imports/ui/properties/viewers/ActionViewer.vue');
|
import ActionViewer from '/imports/ui/properties/viewers/ActionViewer.vue';
|
||||||
const AdjustmentViewer = () => import ('/imports/ui/properties/viewers/AdjustmentViewer.vue');
|
import AdjustmentViewer from '/imports/ui/properties/viewers/AdjustmentViewer.vue';
|
||||||
const AttributeViewer = () => import ('/imports/ui/properties/viewers/AttributeViewer.vue');
|
import AttributeViewer from '/imports/ui/properties/viewers/AttributeViewer.vue';
|
||||||
const BuffViewer = () => import ('/imports/ui/properties/viewers/BuffViewer.vue');
|
import BuffViewer from '/imports/ui/properties/viewers/BuffViewer.vue';
|
||||||
const BuffRemoverViewer = () => import ('/imports/ui/properties/viewers/BuffRemoverViewer.vue');
|
import BuffRemoverViewer from '/imports/ui/properties/viewers/BuffRemoverViewer.vue';
|
||||||
const BranchViewer = () => import ('/imports/ui/properties/viewers/BranchViewer.vue');
|
import BranchViewer from '/imports/ui/properties/viewers/BranchViewer.vue';
|
||||||
const ContainerViewer = () => import ('/imports/ui/properties/viewers/ContainerViewer.vue');
|
import ContainerViewer from '/imports/ui/properties/viewers/ContainerViewer.vue';
|
||||||
const ClassViewer = () => import ('/imports/ui/properties/viewers/ClassViewer.vue');
|
import ClassViewer from '/imports/ui/properties/viewers/ClassViewer.vue';
|
||||||
const ClassLevelViewer = () => import ('/imports/ui/properties/viewers/ClassLevelViewer.vue');
|
import ClassLevelViewer from '/imports/ui/properties/viewers/ClassLevelViewer.vue';
|
||||||
const ConstantViewer = () => import ('/imports/ui/properties/viewers/ConstantViewer.vue');
|
import ConstantViewer from '/imports/ui/properties/viewers/ConstantViewer.vue';
|
||||||
const DamageViewer = () => import ('/imports/ui/properties/viewers/DamageViewer.vue');
|
import DamageViewer from '/imports/ui/properties/viewers/DamageViewer.vue';
|
||||||
const DamageMultiplierViewer = () => import ('/imports/ui/properties/viewers/DamageMultiplierViewer.vue');
|
import DamageMultiplierViewer from '/imports/ui/properties/viewers/DamageMultiplierViewer.vue';
|
||||||
const EffectViewer = () => import ('/imports/ui/properties/viewers/EffectViewer.vue');
|
import EffectViewer from '/imports/ui/properties/viewers/EffectViewer.vue';
|
||||||
const FeatureViewer = () => import ('/imports/ui/properties/viewers/FeatureViewer.vue');
|
import FeatureViewer from '/imports/ui/properties/viewers/FeatureViewer.vue';
|
||||||
const FolderViewer = () => import ('/imports/ui/properties/viewers/FolderViewer.vue');
|
import FolderViewer from '/imports/ui/properties/viewers/FolderViewer.vue';
|
||||||
const ItemViewer = () => import ('/imports/ui/properties/viewers/ItemViewer.vue');
|
import ItemViewer from '/imports/ui/properties/viewers/ItemViewer.vue';
|
||||||
const NoteViewer = () => import ('/imports/ui/properties/viewers/NoteViewer.vue');
|
import NoteViewer from '/imports/ui/properties/viewers/NoteViewer.vue';
|
||||||
const PointBuyViewer = () => import ('/imports/ui/properties/viewers/PointBuyViewer.vue');
|
import PointBuyViewer from '/imports/ui/properties/viewers/PointBuyViewer.vue';
|
||||||
const ProficiencyViewer = () => import ('/imports/ui/properties/viewers/ProficiencyViewer.vue');
|
import ProficiencyViewer from '/imports/ui/properties/viewers/ProficiencyViewer.vue';
|
||||||
const ReferenceViewer = () => import ('/imports/ui/properties/viewers/ReferenceViewer.vue');
|
import ReferenceViewer from '/imports/ui/properties/viewers/ReferenceViewer.vue';
|
||||||
const RollViewer = () => import ('/imports/ui/properties/viewers/RollViewer.vue');
|
import RollViewer from '/imports/ui/properties/viewers/RollViewer.vue';
|
||||||
const SkillViewer = () => import ('/imports/ui/properties/viewers/SkillViewer.vue');
|
import SkillViewer from '/imports/ui/properties/viewers/SkillViewer.vue';
|
||||||
const SavingThrowViewer = () => import ('/imports/ui/properties/viewers/SavingThrowViewer.vue');
|
import SavingThrowViewer from '/imports/ui/properties/viewers/SavingThrowViewer.vue';
|
||||||
const SlotViewer = () => import ('/imports/ui/properties/viewers/SlotViewer.vue');
|
import SlotViewer from '/imports/ui/properties/viewers/SlotViewer.vue';
|
||||||
const SlotFillerViewer = () => import ('/imports/ui/properties/viewers/SlotFillerViewer.vue');
|
import SlotFillerViewer from '/imports/ui/properties/viewers/SlotFillerViewer.vue';
|
||||||
const SpellListViewer = () => import ('/imports/ui/properties/viewers/SpellListViewer.vue');
|
import SpellListViewer from '/imports/ui/properties/viewers/SpellListViewer.vue';
|
||||||
const SpellViewer = () => import ('/imports/ui/properties/viewers/SpellViewer.vue');
|
import SpellViewer from '/imports/ui/properties/viewers/SpellViewer.vue';
|
||||||
const ToggleViewer = () => import ('/imports/ui/properties/viewers/ToggleViewer.vue');
|
import ToggleViewer from '/imports/ui/properties/viewers/ToggleViewer.vue';
|
||||||
const TriggerViewer = () => import ('/imports/ui/properties/viewers/TriggerViewer.vue');
|
import TriggerViewer from '/imports/ui/properties/viewers/TriggerViewer.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
action: ActionViewer,
|
action: ActionViewer,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const LibraryCollectionToolbar = () => import('/imports/ui/library/LibraryCollec
|
|||||||
const CharacterSheetPage = () => import('/imports/ui/pages/CharacterSheetPage.vue');
|
const CharacterSheetPage = () => import('/imports/ui/pages/CharacterSheetPage.vue');
|
||||||
const CharacterSheetToolbar = () => import('/imports/ui/creature/character/CharacterSheetToolbar.vue');
|
const CharacterSheetToolbar = () => import('/imports/ui/creature/character/CharacterSheetToolbar.vue');
|
||||||
const CharacterSheetRightDrawer = () => import('/imports/ui/creature/character/CharacterSheetRightDrawer.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 SignIn = () => import('/imports/ui/pages/SignIn.vue');
|
||||||
const Register = () => import('/imports/ui/pages/Register.vue');
|
const Register = () => import('/imports/ui/pages/Register.vue');
|
||||||
const IconAdmin = () => import('/imports/ui/icons/IconAdmin.vue');
|
const IconAdmin = () => import('/imports/ui/icons/IconAdmin.vue');
|
||||||
@@ -177,6 +178,16 @@ RouterFactory.configure(router => {
|
|||||||
meta: {
|
meta: {
|
||||||
title: 'Character Sheet',
|
title: 'Character Sheet',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
name: 'printCharacterSheet',
|
||||||
|
path: '/print-character/:id',
|
||||||
|
alias: '/print-character/:id/:urlName',
|
||||||
|
components: {
|
||||||
|
default: CharacterSheetPrinted,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
title: 'Print Character Sheet',
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
path: '/tabletops',
|
path: '/tabletops',
|
||||||
name: 'tabletops',
|
name: 'tabletops',
|
||||||
|
|||||||
@@ -13,6 +13,16 @@
|
|||||||
:value="!!model.public + ''"
|
:value="!!model.public + ''"
|
||||||
@change="(value, ack) => setSheetPublic({value, ack})"
|
@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
|
<text-field
|
||||||
v-if="model.public && docRef.collection === 'libraries'"
|
v-if="model.public && docRef.collection === 'libraries'"
|
||||||
readonly
|
readonly
|
||||||
@@ -30,6 +40,7 @@
|
|||||||
@change="(value, ack) => getUser({value, ack})"
|
@change="(value, ack) => getUser({value, ack})"
|
||||||
/>
|
/>
|
||||||
<v-btn
|
<v-btn
|
||||||
|
class="ml-2 mt-2"
|
||||||
:disabled="userFoundState !== 'found'"
|
:disabled="userFoundState !== 'found'"
|
||||||
@click="updateSharing(userId, 'reader')"
|
@click="updateSharing(userId, 'reader')"
|
||||||
>
|
>
|
||||||
@@ -126,6 +137,7 @@
|
|||||||
<script lang="js">
|
<script lang="js">
|
||||||
import {
|
import {
|
||||||
setPublic,
|
setPublic,
|
||||||
|
setReadersCanCopy,
|
||||||
updateUserSharePermissions
|
updateUserSharePermissions
|
||||||
} from '/imports/api/sharing/sharing.js';
|
} from '/imports/api/sharing/sharing.js';
|
||||||
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
import fetchDocByRef from '/imports/api/parenting/fetchDocByRef.js';
|
||||||
@@ -157,6 +169,14 @@ export default {
|
|||||||
ack(error && error.reason || error);
|
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 }) {
|
getUser({ value, ack }) {
|
||||||
this.userSearched = value;
|
this.userSearched = value;
|
||||||
if (!value) {
|
if (!value) {
|
||||||
|
|||||||
756
app/package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dicecloud",
|
"name": "dicecloud",
|
||||||
"version": "2.0.38",
|
"version": "2.0.44",
|
||||||
"description": "Unofficial Online Realtime D&D 5e App",
|
"description": "Unofficial Online Realtime D&D 5e App",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -19,57 +19,58 @@
|
|||||||
"npm": "6.13.x"
|
"npm": "6.13.x"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.3",
|
"@babel/runtime": "^7.20.1",
|
||||||
"@chenfengyuan/vue-countdown": "^1.1.5",
|
"@chenfengyuan/vue-countdown": "^1.1.5",
|
||||||
"@tozd/vue-observer-utils": "^0.5.0",
|
"@tozd/vue-observer-utils": "^0.5.0",
|
||||||
"animejs": "^2.2.0",
|
"animejs": "^2.2.0",
|
||||||
"aws-sdk": "^2.1148.0",
|
"aws-sdk": "^2.1247.0",
|
||||||
"bcrypt": "^5.0.0",
|
"bcrypt": "^5.1.0",
|
||||||
"chroma-js": "^2.4.2",
|
"chroma-js": "^2.4.2",
|
||||||
"core-js": "^2.6.11",
|
"core-js": "^2.6.11",
|
||||||
"css-box-shadow": "^1.0.0-3",
|
"css-box-shadow": "^1.0.0-3",
|
||||||
"date-fns": "^1.30.1",
|
"date-fns": "^1.30.1",
|
||||||
"ddp-rate-limiter-mixin": "^1.1.10",
|
"ddp-rate-limiter-mixin": "^1.1.10",
|
||||||
"discord.js": "^12.5.3",
|
"discord.js": "^12.5.3",
|
||||||
"dompurify": "^2.3.8",
|
"dompurify": "^2.4.0",
|
||||||
"ignore": "^5.2.0",
|
"ignore": "^5.2.0",
|
||||||
"ignore-styles": "^5.0.1",
|
"ignore-styles": "^5.0.1",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"marked": "^4.0.16",
|
"marked": "^4.2.1",
|
||||||
"meteor-node-stubs": "^1.2.3",
|
"meteor-node-stubs": "^1.2.5",
|
||||||
"minify-css-string": "^1.0.0",
|
"minify-css-string": "^1.0.0",
|
||||||
"moo": "^0.5.1",
|
"moo": "^0.5.2",
|
||||||
"nearley": "^2.19.1",
|
"nearley": "^2.19.1",
|
||||||
"ngraph.graph": "^19.1.0",
|
"ngraph.graph": "^19.1.0",
|
||||||
"ngraph.path": "^1.4.0",
|
"ngraph.path": "^1.4.0",
|
||||||
"pretty-bytes": "^6.0.0",
|
"pretty-bytes": "^6.0.0",
|
||||||
"qrcode": "^1.5.0",
|
"qrcode": "^1.5.1",
|
||||||
|
"qrcode.vue": "^1.7.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"sharp": "^0.30.4",
|
"sharp": "^0.30.7",
|
||||||
"simpl-schema": "^1.12.2",
|
"simpl-schema": "^1.13.1",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"speakingurl": "^14.0.1",
|
"speakingurl": "^14.0.1",
|
||||||
"styles": "^0.2.1",
|
"styles": "^0.2.1",
|
||||||
"underscore": "^1.13.4",
|
"underscore": "^1.13.6",
|
||||||
"vue": "2.6.10",
|
"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-reactive-provide": "^0.3.0",
|
||||||
"vue-router": "^3.5.4",
|
"vue-router": "^3.6.5",
|
||||||
"vuedraggable": "^2.23.2",
|
"vuedraggable": "^2.23.2",
|
||||||
"vuetify": "^2.6.6",
|
"vuetify": "^2.6.12",
|
||||||
"vuetify-upload-button": "^2.0.2",
|
"vuetify-upload-button": "^2.0.2",
|
||||||
"vuex": "^3.1.3"
|
"vuex": "^3.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^5.39.0",
|
"@typescript-eslint/eslint-plugin": "^5.42.0",
|
||||||
"@typescript-eslint/parser": "^5.39.0",
|
"@typescript-eslint/parser": "^5.42.0",
|
||||||
"@vue/compiler-dom": "^3.2.40",
|
"@vue/compiler-dom": "^3.2.41",
|
||||||
"chai": "^4.3.6",
|
"chai": "^4.3.6",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-plugin-vue": "^7.20.0",
|
"eslint-plugin-vue": "^7.20.0",
|
||||||
"eslint-plugin-vuetify": "^1.1.0",
|
"eslint-plugin-vuetify": "^1.1.0",
|
||||||
"mem": "^6.1.1",
|
"mem": "^6.1.1",
|
||||||
"sass": "^1.52.2",
|
"sass": "^1.56.0",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^4.8.4"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
@@ -120,8 +121,7 @@
|
|||||||
"quotes": [
|
"quotes": [
|
||||||
"error",
|
"error",
|
||||||
"single"
|
"single"
|
||||||
],
|
]
|
||||||
"vuetify/no-deprecated-classes": "error"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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`.
|
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
|
### Name
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Skills
|
# 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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
|
### 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`
|
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`
|
||||||
|
|||||||
47
app/private/docs/walkthroughs/create-a-class.md
Normal 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.
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
Expand the children of the class slot, and click the plus button to add a child property.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This brings up the create a property dialog, we are creating a class, so select the class property type.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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**.
|
||||||
|
|
||||||
|

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

|
||||||
BIN
app/public/images/docs/walkthroughs/create-a-class-1.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
app/public/images/docs/walkthroughs/create-a-class-2.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
app/public/images/docs/walkthroughs/create-a-class-3.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
app/public/images/docs/walkthroughs/create-a-class-4.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
app/public/images/docs/walkthroughs/create-a-class-5.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
app/public/images/docs/walkthroughs/create-a-class-6.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |