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