Compare commits
44 Commits
2.0-beta.3
...
2.0-beta.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b876c2801d | ||
|
|
698c9c7bbf | ||
|
|
7544243640 | ||
|
|
4b4e3a8928 | ||
|
|
92a588bfcc | ||
|
|
43e956eb6a | ||
|
|
c4429f5dd7 | ||
|
|
4edfe1bcb9 | ||
|
|
473a9f0253 | ||
|
|
94cdca4f31 | ||
|
|
10d0a3f763 | ||
|
|
afe6c044cd | ||
|
|
e6c7d79d7d | ||
|
|
49fa9cc470 | ||
|
|
3646c13355 | ||
|
|
27665e0bdc | ||
|
|
fea29e60b7 | ||
|
|
653f05012a | ||
|
|
7ee4a22d77 | ||
|
|
59c69a46a8 | ||
|
|
f79a6d98ec | ||
|
|
0ffa736143 | ||
|
|
f1b4071c46 | ||
|
|
249ece352c | ||
|
|
4fe3f30090 | ||
|
|
44d3fbc065 | ||
|
|
b1feb126df | ||
|
|
69f9636688 | ||
|
|
5383804af7 | ||
|
|
0b8c88daef | ||
|
|
5b6bff91a4 | ||
|
|
52453b46e9 | ||
|
|
78c67a4fd6 | ||
|
|
90b277e181 | ||
|
|
dc4d0416a2 | ||
|
|
12a0dff43f | ||
|
|
b9f79f1c51 | ||
|
|
92d32e7cf8 | ||
|
|
80460ceaed | ||
|
|
8f30c1419c | ||
|
|
4c6d70b084 | ||
|
|
ee2b400ee6 | ||
|
|
ef8aafc1a1 | ||
|
|
b68637e525 |
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
.meteor/meteorite
|
.meteor/meteorite
|
||||||
.demeteorized
|
.demeteorized
|
||||||
.cache
|
.cache
|
||||||
|
.vscode
|
||||||
settings.json
|
settings.json
|
||||||
public/components
|
public/components
|
||||||
public/_imports.html
|
public/_imports.html
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ accounts-google@1.4.0
|
|||||||
email@2.2.0
|
email@2.2.0
|
||||||
meteor-base@1.5.1
|
meteor-base@1.5.1
|
||||||
mobile-experience@1.1.0
|
mobile-experience@1.1.0
|
||||||
mongo@1.14.0
|
mongo@1.14.6
|
||||||
session@1.2.0
|
session@1.2.0
|
||||||
tracker@1.2.0
|
tracker@1.2.0
|
||||||
logging@1.3.1
|
logging@1.3.1
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
METEOR@2.6
|
METEOR@2.6.1
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ aldeed:collection2@3.5.0
|
|||||||
aldeed:schema-index@3.0.0
|
aldeed:schema-index@3.0.0
|
||||||
allow-deny@1.1.1
|
allow-deny@1.1.1
|
||||||
autoupdate@1.8.0
|
autoupdate@1.8.0
|
||||||
babel-compiler@7.8.0
|
babel-compiler@7.8.1
|
||||||
babel-runtime@1.5.0
|
babel-runtime@1.5.0
|
||||||
base64@1.0.12
|
base64@1.0.12
|
||||||
binary-heap@1.0.11
|
binary-heap@1.0.11
|
||||||
@@ -68,7 +68,7 @@ mobile-status-bar@1.1.0
|
|||||||
modern-browsers@0.1.7
|
modern-browsers@0.1.7
|
||||||
modules@0.18.0
|
modules@0.18.0
|
||||||
modules-runtime@0.12.0
|
modules-runtime@0.12.0
|
||||||
mongo@1.14.4
|
mongo@1.14.6
|
||||||
mongo-decimal@0.1.2
|
mongo-decimal@0.1.2
|
||||||
mongo-dev-server@1.1.0
|
mongo-dev-server@1.1.0
|
||||||
mongo-id@1.0.8
|
mongo-id@1.0.8
|
||||||
@@ -121,6 +121,6 @@ tracker@1.2.0
|
|||||||
typescript@4.4.1
|
typescript@4.4.1
|
||||||
underscore@1.0.10
|
underscore@1.0.10
|
||||||
url@1.3.2
|
url@1.3.2
|
||||||
webapp@1.13.0
|
webapp@1.13.1
|
||||||
webapp-hashing@1.1.0
|
webapp-hashing@1.1.0
|
||||||
zer0th:meteor-vuetify-loader@0.1.41
|
zer0th:meteor-vuetify-loader@0.1.41
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createS3FilesCollection } from '/imports/api/files/s3FileStorage.js';
|
|||||||
|
|
||||||
const ArchiveCreatureFiles = createS3FilesCollection({
|
const ArchiveCreatureFiles = createS3FilesCollection({
|
||||||
collectionName: 'archiveCreatureFiles',
|
collectionName: 'archiveCreatureFiles',
|
||||||
storagePath: '/DiceCloud/archiveCreatures/',
|
storagePath: Meteor.isDevelopment ? '/DiceCloud/archiveCreatures/' : 'assets/app/archiveCreatures',
|
||||||
onBeforeUpload(file) {
|
onBeforeUpload(file) {
|
||||||
// Allow upload files under 10MB, and only in json format
|
// Allow upload files under 10MB, and only in json format
|
||||||
if (file.size > 10485760) {
|
if (file.size > 10485760) {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const damageProperty = new ValidatedMethod({
|
|||||||
export function damagePropertyWork({property, operation, value}){
|
export function damagePropertyWork({property, operation, value}){
|
||||||
let damage, newValue;
|
let damage, newValue;
|
||||||
if (operation === 'set'){
|
if (operation === 'set'){
|
||||||
const total = property.total;
|
const total = property.total || 0;
|
||||||
// Set represents what we want the value to be after damage
|
// Set represents what we want the value to be after damage
|
||||||
// So we need the actual damage to get to that value
|
// So we need the actual damage to get to that value
|
||||||
damage = total - value;
|
damage = total - value;
|
||||||
@@ -57,8 +57,8 @@ export function damagePropertyWork({property, operation, value}){
|
|||||||
if (damage < 0) damage = 0;
|
if (damage < 0) damage = 0;
|
||||||
newValue = property.total - damage;
|
newValue = property.total - damage;
|
||||||
} else if (operation === 'increment'){
|
} else if (operation === 'increment'){
|
||||||
let currentValue = property.value;
|
let currentValue = property.value || 0;
|
||||||
let currentDamage = property.damage;
|
let currentDamage = property.damage || 0;
|
||||||
let increment = value;
|
let increment = value;
|
||||||
// Can't increase damage above the remaining value
|
// Can't increase damage above the remaining value
|
||||||
if (increment > currentValue) increment = currentValue;
|
if (increment > currentValue) increment = currentValue;
|
||||||
|
|||||||
@@ -120,6 +120,20 @@ let CreatureSchema = new SimpleSchema({
|
|||||||
blackbox: true,
|
blackbox: true,
|
||||||
defaultValue: {}
|
defaultValue: {}
|
||||||
},
|
},
|
||||||
|
computeErrors: {
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
'computeErrors.$': {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
'computeErrors.$.type': {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
'computeErrors.$.details' : {
|
||||||
|
type: Object,
|
||||||
|
blackbox: true,
|
||||||
|
},
|
||||||
|
|
||||||
// Tabletop
|
// Tabletop
|
||||||
tabletop: {
|
tabletop: {
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const insertExperienceForCreature = function({experience, creatureId, userId}){
|
|||||||
}
|
}
|
||||||
experience.creatureId = creatureId;
|
experience.creatureId = creatureId;
|
||||||
let id = Experiences.insert(experience);
|
let id = Experiences.insert(experience);
|
||||||
recomputeCreatureById(creatureId);
|
computeCreature(creatureId);
|
||||||
return id;
|
return id;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ const removeExperience = new ValidatedMethod({
|
|||||||
}
|
}
|
||||||
experience.creatureId = creatureId;
|
experience.creatureId = creatureId;
|
||||||
let numRemoved = Experiences.remove(experienceId);
|
let numRemoved = Experiences.remove(experienceId);
|
||||||
recomputeCreatureById(creatureId);
|
computeCreature(creatureId);
|
||||||
return numRemoved;
|
return numRemoved;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ const logRoll = new ValidatedMethod({
|
|||||||
let {result: rolled} = resolve('roll', compiled, creature.variables, context);
|
let {result: rolled} = resolve('roll', compiled, creature.variables, context);
|
||||||
let rolledString = toString(rolled);
|
let rolledString = toString(rolled);
|
||||||
if (rolledString !== compiledString) logContent.push({
|
if (rolledString !== compiledString) logContent.push({
|
||||||
value: rolled.toString()
|
value: rolledString
|
||||||
});
|
});
|
||||||
let {result} = resolve('reduce', rolled, creature.variables, context);
|
let {result} = resolve('reduce', rolled, creature.variables, context);
|
||||||
let resultString = toString(result);
|
let resultString = toString(result);
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ let LogContentSchema = new SimpleSchema({
|
|||||||
optional: true,
|
optional: true,
|
||||||
max: STORAGE_LIMITS.summary,
|
max: STORAGE_LIMITS.summary,
|
||||||
},
|
},
|
||||||
|
// Inline with other content fields
|
||||||
|
inline: {
|
||||||
|
type: Boolean,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
context: {
|
context: {
|
||||||
type: Object,
|
type: Object,
|
||||||
optional: true,
|
optional: true,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import applyProperty from '../applyProperty.js';
|
|||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
||||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||||
|
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||||
|
|
||||||
export default function applyAction(node, {creature, targets, scope, log}){
|
export default function applyAction(node, {creature, targets, scope, log}){
|
||||||
const prop = node.node;
|
const prop = node.node;
|
||||||
@@ -24,16 +25,18 @@ export default function applyAction(node, {creature, targets, scope, log}){
|
|||||||
const failed = spendResources({prop, log, scope});
|
const failed = spendResources({prop, log, scope});
|
||||||
if (failed) return;
|
if (failed) return;
|
||||||
|
|
||||||
|
const attack = prop.attackRoll || prop.attackRollBonus;
|
||||||
|
|
||||||
// Attack if there is an attack roll
|
// Attack if there is an attack roll
|
||||||
if (prop.attackRoll && prop.attackRoll.calculation){
|
if (attack && attack.calculation){
|
||||||
if (targets.length){
|
if (targets.length){
|
||||||
targets.forEach(target => {
|
targets.forEach(target => {
|
||||||
applyAttackToTarget({prop, target, scope, log});
|
applyAttackToTarget({attack, target, scope, log});
|
||||||
// Apply the children, but only to the current target
|
// Apply the children, but only to the current target
|
||||||
applyChildren(node, {targets: [target], scope, log});
|
applyChildren(node, {targets: [target], scope, log});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
applyAttackWithoutTarget({prop, scope, log});
|
applyAttackWithoutTarget({attack, scope, log});
|
||||||
applyChildren(node, {creature, targets, scope, log});
|
applyChildren(node, {creature, targets, scope, log});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -41,44 +44,35 @@ export default function applyAction(node, {creature, targets, scope, log}){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyAttackWithoutTarget({prop, scope, log}){
|
function applyAttackWithoutTarget({attack, scope, log}){
|
||||||
delete scope['$attackHit'];
|
delete scope['$attackHit'];
|
||||||
delete scope['$attackMiss'];
|
delete scope['$attackMiss'];
|
||||||
delete scope['$criticalHit'];
|
delete scope['$criticalHit'];
|
||||||
delete scope['$criticalMiss'];
|
delete scope['$criticalMiss'];
|
||||||
delete scope['$attackRoll'];
|
delete scope['$attackRoll'];
|
||||||
|
|
||||||
recalculateCalculation(prop.attackRoll, scope, log);
|
recalculateCalculation(attack, scope, log);
|
||||||
|
|
||||||
let value = rollDice(1, 20)[0];
|
let {
|
||||||
scope['$attackRoll'] = {value};
|
resultPrefix,
|
||||||
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
|
result,
|
||||||
let criticalHit = value >= criticalHitTarget;
|
criticalHit,
|
||||||
if (criticalHit){
|
criticalMiss,
|
||||||
scope['$criticalHit'] = {value: true};
|
} = rollAttack(attack, scope);
|
||||||
scope['$attackHit'] = {value: true};
|
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
|
||||||
} else {
|
if (scope['$attackAdvantage'] === 1){
|
||||||
let criticalMiss = value === 1;
|
name += ' (Advantage)';
|
||||||
if (criticalMiss){
|
} else if(scope['$attackAdvantage'] === -1){
|
||||||
scope['$criticalMiss'] = 1;
|
name += ' (Disadvantage)';
|
||||||
log.content.push({
|
|
||||||
name: 'Critical Miss!',
|
|
||||||
});
|
|
||||||
scope['$attackMiss'] = {value: true};
|
|
||||||
} else {
|
|
||||||
// Untargeted attacks hit by default
|
|
||||||
scope['$attackHit'] = {value: true}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let result = value + prop.attackRoll.value;
|
|
||||||
scope['$attackRoll'] = {value: result};
|
|
||||||
log.content.push({
|
log.content.push({
|
||||||
name: criticalHit ? 'Critical Hit!' : 'To Hit',
|
name,
|
||||||
value: `1d20 [${value}] + ${prop.attackRoll.value} = ` + result,
|
value: `${resultPrefix}\n**${result}**`,
|
||||||
|
inline: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyAttackToTarget({prop, target, scope, log}){
|
function applyAttackToTarget({attack, target, scope, log}){
|
||||||
delete scope['$attackHit'];
|
delete scope['$attackHit'];
|
||||||
delete scope['$attackMiss'];
|
delete scope['$attackMiss'];
|
||||||
delete scope['$criticalHit'];
|
delete scope['$criticalHit'];
|
||||||
@@ -86,26 +80,31 @@ function applyAttackToTarget({prop, target, scope, log}){
|
|||||||
delete scope['$attackDiceRoll'];
|
delete scope['$attackDiceRoll'];
|
||||||
delete scope['$attackRoll'];
|
delete scope['$attackRoll'];
|
||||||
|
|
||||||
recalculateCalculation(prop.attackRoll, scope, log);
|
recalculateCalculation(attack, scope, log);
|
||||||
|
|
||||||
|
let {
|
||||||
|
resultPrefix,
|
||||||
|
result,
|
||||||
|
criticalHit,
|
||||||
|
criticalMiss,
|
||||||
|
} = rollAttack(attack, scope);
|
||||||
|
|
||||||
const value = rollDice(1, 20)[0];
|
|
||||||
scope['$attackDiceRoll'] = {value};
|
|
||||||
const criticalHitTarget = scope.criticalHitTarget?.value || 20;
|
|
||||||
const criticalHit = value >= criticalHitTarget;
|
|
||||||
const criticalMiss = value === 1;
|
|
||||||
if (criticalHit) scope['$criticalHit'] = {value: true};
|
|
||||||
if (criticalMiss) scope['$criticalMiss'] = {value: true};
|
|
||||||
const result = value + prop.attackRoll.value;
|
|
||||||
scope['$attackRoll'] = {value: result};
|
|
||||||
if (target.variables.armor){
|
if (target.variables.armor){
|
||||||
const armor = target.variables.armor.value;
|
const armor = target.variables.armor.value;
|
||||||
const name = criticalHit ? 'Critical Hit!' :
|
|
||||||
criticalMiss ? 'Critical miss!' :
|
let name = criticalHit ? 'Critical Hit!' :
|
||||||
result > armor ? 'Hit!' :
|
criticalMiss ? 'Critical Miss!' :
|
||||||
'Miss!'
|
result > armor ? 'Hit!' : 'Miss!';
|
||||||
|
if (scope['$attackAdvantage'] === 1){
|
||||||
|
name += ' (Advantage)';
|
||||||
|
} else if(scope['$attackAdvantage'] === -1){
|
||||||
|
name += ' (Disadvantage)';
|
||||||
|
}
|
||||||
|
|
||||||
log.content.push({
|
log.content.push({
|
||||||
name,
|
name,
|
||||||
value: `1d20 {${value}} + ${prop.attackRoll.value} = ` + result,
|
value: `${resultPrefix}\n**${result}**`,
|
||||||
|
inline: true,
|
||||||
});
|
});
|
||||||
if ((result > armor) || (criticalHit)){
|
if ((result > armor) || (criticalHit)){
|
||||||
scope['$attackHit'] = true;
|
scope['$attackHit'] = true;
|
||||||
@@ -118,19 +117,71 @@ function applyAttackToTarget({prop, target, scope, log}){
|
|||||||
value:'Target has no `armor`',
|
value:'Target has no `armor`',
|
||||||
});
|
});
|
||||||
log.content.push({
|
log.content.push({
|
||||||
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical miss!' : 'To Hit',
|
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
|
||||||
value: `1d20 {${value}} + ${prop.attackRoll.value} = ` + result,
|
value: `${resultPrefix}\n**${result}**`,
|
||||||
|
inline: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rollAttack(attack, scope){
|
||||||
|
const rollModifierText = numberToSignedString(attack.value, true);
|
||||||
|
let value, resultPrefix;
|
||||||
|
if (attack.advantage === 1 || scope['$attackAdvantage']){
|
||||||
|
const [a, b] = rollDice(2, 20);
|
||||||
|
if (a >= b) {
|
||||||
|
value = a;
|
||||||
|
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
|
||||||
|
} else {
|
||||||
|
value = b;
|
||||||
|
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
||||||
|
}
|
||||||
|
} else if (attack.advantage === -1 || scope['$attackDisadvantage']){
|
||||||
|
const [a, b] = rollDice(2, 20);
|
||||||
|
if (a <= b) {
|
||||||
|
value = a;
|
||||||
|
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
|
||||||
|
} else {
|
||||||
|
value = b;
|
||||||
|
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = rollDice(1, 20)[0];
|
||||||
|
resultPrefix = `1d20 [${value}] ${rollModifierText}`
|
||||||
|
}
|
||||||
|
scope['$attackRoll'] = {value};
|
||||||
|
const result = value + attack.value;
|
||||||
|
const {criticalHit, criticalMiss} = applyCrits(value, scope);
|
||||||
|
return {resultPrefix, result, value, criticalHit, criticalMiss};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCrits(value, scope){
|
||||||
|
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
|
||||||
|
let criticalHit = value >= criticalHitTarget;
|
||||||
|
let criticalMiss;
|
||||||
|
if (criticalHit){
|
||||||
|
scope['$criticalHit'] = {value: true};
|
||||||
|
scope['$attackHit'] = {value: true};
|
||||||
|
} else {
|
||||||
|
criticalMiss = value === 1;
|
||||||
|
if (criticalMiss){
|
||||||
|
scope['$criticalMiss'] = 1;
|
||||||
|
scope['$attackMiss'] = {value: true};
|
||||||
|
} else {
|
||||||
|
// Untargeted attacks hit by default
|
||||||
|
scope['$attackHit'] = {value: true}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {criticalHit, criticalMiss};
|
||||||
|
}
|
||||||
|
|
||||||
function applyChildren(node, args){
|
function applyChildren(node, args){
|
||||||
node.children.forEach(child => applyProperty(child, args));
|
node.children.forEach(child => applyProperty(child, args));
|
||||||
}
|
}
|
||||||
|
|
||||||
function spendResources({prop, log, scope}){
|
function spendResources({prop, log, scope}){
|
||||||
// Check Uses
|
// Check Uses
|
||||||
if (prop.usesLeft < 0){
|
if (prop.usesLeft <= 0){
|
||||||
log.content.push({
|
log.content.push({
|
||||||
name: 'Error',
|
name: 'Error',
|
||||||
value: `${prop.name || 'action'} does not have enough uses left`,
|
value: `${prop.name || 'action'} does not have enough uses left`,
|
||||||
@@ -202,7 +253,8 @@ function spendResources({prop, log, scope}){
|
|||||||
});
|
});
|
||||||
log.content.push({
|
log.content.push({
|
||||||
name: 'Uses left',
|
name: 'Uses left',
|
||||||
value: prop.usesLeft - (prop.usesUsed || 0) - 1,
|
value: prop.usesLeft - 1,
|
||||||
|
inline: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,9 +284,11 @@ function spendResources({prop, log, scope}){
|
|||||||
if (gainLog.length) log.content.push({
|
if (gainLog.length) log.content.push({
|
||||||
name: 'Gained',
|
name: 'Gained',
|
||||||
value: gainLog.join('\n'),
|
value: gainLog.join('\n'),
|
||||||
|
inline: true,
|
||||||
});
|
});
|
||||||
if (spendLog.length) log.content.push({
|
if (spendLog.length) log.content.push({
|
||||||
name: 'Spent',
|
name: 'Spent',
|
||||||
value: spendLog.join('\n'),
|
value: spendLog.join('\n'),
|
||||||
|
inline: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default function applyAdjustment(node, {
|
|||||||
|
|
||||||
// Evaluate the amount
|
// Evaluate the amount
|
||||||
recalculateCalculation(prop.amount, scope, log);
|
recalculateCalculation(prop.amount, scope, log);
|
||||||
|
|
||||||
const value = +prop.amount.value;
|
const value = +prop.amount.value;
|
||||||
if (!isFinite(value)) {
|
if (!isFinite(value)) {
|
||||||
return applyChildren(node, {creature, targets, scope, log});
|
return applyChildren(node, {creature, targets, scope, log});
|
||||||
@@ -23,8 +23,8 @@ export default function applyAdjustment(node, {
|
|||||||
if (damageTargets?.length) {
|
if (damageTargets?.length) {
|
||||||
damageTargets.forEach(target => {
|
damageTargets.forEach(target => {
|
||||||
let stat = target.variables[prop.stat];
|
let stat = target.variables[prop.stat];
|
||||||
if (!stat) {
|
if (!stat?.type) {
|
||||||
log({
|
log.content.push({
|
||||||
name: 'Error',
|
name: 'Error',
|
||||||
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`
|
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`
|
||||||
});
|
});
|
||||||
@@ -39,6 +39,7 @@ export default function applyAdjustment(node, {
|
|||||||
name: 'Attribute damage',
|
name: 'Attribute damage',
|
||||||
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
|
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
|
||||||
` ${value}`,
|
` ${value}`,
|
||||||
|
inline: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -46,6 +47,7 @@ export default function applyAdjustment(node, {
|
|||||||
name: 'Attribute damage',
|
name: 'Attribute damage',
|
||||||
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
|
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
|
||||||
` ${value}`,
|
` ${value}`,
|
||||||
|
inline: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
|||||||
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
|
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
|
||||||
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
|
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
|
||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import resolve, { map } from '/imports/parser/resolve.js';
|
import resolve, { map, toString } from '/imports/parser/resolve.js';
|
||||||
|
import symbol from '/imports/parser/parseTree/symbol.js';
|
||||||
import logErrors from './shared/logErrors.js';
|
import logErrors from './shared/logErrors.js';
|
||||||
|
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
|
||||||
|
|
||||||
export default function applyBuff(node, {creature, targets, scope, log}){
|
export default function applyBuff(node, {creature, targets, scope, log}){
|
||||||
const prop = node.node;
|
const prop = node.node;
|
||||||
@@ -63,7 +65,7 @@ function crystalizeVariables({propList, scope, log}){
|
|||||||
applyFnToKey(prop, calcKey, (prop, key) => {
|
applyFnToKey(prop, calcKey, (prop, key) => {
|
||||||
const calcObj = get(prop, key);
|
const calcObj = get(prop, key);
|
||||||
if (!calcObj?.parseNode) return;
|
if (!calcObj?.parseNode) return;
|
||||||
map(calcObj.parseNode, node => {
|
calcObj.parseNode = map(calcObj.parseNode, node => {
|
||||||
// Skip nodes that aren't symbols or accessors
|
// Skip nodes that aren't symbols or accessors
|
||||||
if (
|
if (
|
||||||
node.parseType !== 'accessor' && node.parseType !== 'symbol'
|
node.parseType !== 'accessor' && node.parseType !== 'symbol'
|
||||||
@@ -73,11 +75,14 @@ function crystalizeVariables({propList, scope, log}){
|
|||||||
// strip $target
|
// strip $target
|
||||||
if (node.parseType === 'accessor'){
|
if (node.parseType === 'accessor'){
|
||||||
node.name = node.path.shift();
|
node.name = node.path.shift();
|
||||||
|
if (!node.path.length){
|
||||||
|
return symbol.create({name: node.name})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Can't strip symbols
|
// Can't strip symbols
|
||||||
log.content.push({
|
log.content.push({
|
||||||
name: 'Error',
|
name: 'Error',
|
||||||
value: 'Variable `$target` should not be used without a property: $target.property'
|
value: 'Variable `$target` should not be used without a property: $target.property',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return node;
|
return node;
|
||||||
@@ -88,6 +93,8 @@ function crystalizeVariables({propList, scope, log}){
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
calcObj.calculation = toString(calcObj.parseNode);
|
||||||
|
calcObj.hash = cyrb53(calcObj.calculation);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -88,16 +88,16 @@ export default function applyDamage(node, {
|
|||||||
// Log the damage done
|
// Log the damage done
|
||||||
if (target._id === creature._id){
|
if (target._id === creature._id){
|
||||||
// Target is same as self, log damage as such
|
// Target is same as self, log damage as such
|
||||||
logValue.push(damageDealt + suffix + ' to self');
|
logValue.push(`**${damageDealt}** ${suffix} to self`);
|
||||||
} else {
|
} else {
|
||||||
logValue.push('Dealt ' + damageDealt + suffix + ` ${target.name && ' to '}${target.name}`);
|
logValue.push(`Dealt **${damageDealt}** ${suffix} ${target.name && ' to '}${target.name}`);
|
||||||
// Log the damage received on that creature's log as well
|
// Log the damage received on that creature's log as well
|
||||||
insertCreatureLog.call({
|
insertCreatureLog.call({
|
||||||
log: {
|
log: {
|
||||||
creatureId: target._id,
|
creatureId: target._id,
|
||||||
content: [{
|
content: [{
|
||||||
name,
|
name,
|
||||||
value: 'Recieved ' + damageDealt + suffix,
|
value: `Recieved **${damageDealt}** ${suffix}`,
|
||||||
}],
|
}],
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -105,11 +105,12 @@ export default function applyDamage(node, {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// There are no targets, just log the result
|
// There are no targets, just log the result
|
||||||
logValue.push(damage + suffix);
|
logValue.push(`**${damage}** ${suffix}`);
|
||||||
}
|
}
|
||||||
log.content.push({
|
log.content.push({
|
||||||
name: logName,
|
name: logName,
|
||||||
value: logValue.join('\n'),
|
value: logValue.join('\n'),
|
||||||
|
inline: true,
|
||||||
});
|
});
|
||||||
return applyChildren();
|
return applyChildren();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default function applyRoll(node, {creature, targets, scope, log}){
|
|||||||
log.content.push({
|
log.content.push({
|
||||||
name: prop.name,
|
name: prop.name,
|
||||||
value: prop.variableName + ' = ' + prop.roll.calculation + ' = ' + prop.roll.value,
|
value: prop.variableName + ' = ' + prop.roll.calculation + ' = ' + prop.roll.value,
|
||||||
|
inline: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return node.children.forEach(child => applyProperty(child, {
|
return node.children.forEach(child => applyProperty(child, {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import rollDice from '/imports/parser/rollDice.js';
|
import rollDice from '/imports/parser/rollDice.js';
|
||||||
import recalculateCalculation from './shared/recalculateCalculation.js';
|
import recalculateCalculation from './shared/recalculateCalculation.js';
|
||||||
import applyProperty from '../applyProperty.js';
|
import applyProperty from '../applyProperty.js';
|
||||||
|
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||||
|
|
||||||
export default function applySavingThrow(node, {creature, targets, scope, log}){
|
export default function applySavingThrow(node, {creature, targets, scope, log}){
|
||||||
const prop = node.node;
|
const prop = node.node;
|
||||||
@@ -22,6 +23,7 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
|
|||||||
log.content.push({
|
log.content.push({
|
||||||
name: prop.name,
|
name: prop.name,
|
||||||
value: ' DC ' + dc,
|
value: ' DC ' + dc,
|
||||||
|
inline: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
saveTargets.forEach(target => {
|
saveTargets.forEach(target => {
|
||||||
@@ -46,19 +48,31 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
|
|||||||
return applyChildren();
|
return applyChildren();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rollModifierText = numberToSignedString(save.value, true);
|
||||||
|
|
||||||
let value, values, resultPrefix;
|
let value, values, resultPrefix;
|
||||||
if (save.advantage === 1){
|
if (save.advantage === 1){
|
||||||
values = rollDice(2, 20).sort().reverse();
|
const [a, b] = rollDice(2, 20);
|
||||||
value = values[0];
|
if (a >= b) {
|
||||||
resultPrefix = `Advantage: 1d20 [${values[0]},~~${values[1]}~~] + ${save.value} = `
|
value = a;
|
||||||
|
resultPrefix = `Advantage: 1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
|
||||||
|
} else {
|
||||||
|
value = b;
|
||||||
|
resultPrefix = `Advantage: 1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||||
|
}
|
||||||
} else if (save.advantage === -1){
|
} else if (save.advantage === -1){
|
||||||
values = rollDice(2, 20).sort();
|
const [a, b] = rollDice(2, 20);
|
||||||
value = values[0];
|
if (a <= b) {
|
||||||
resultPrefix = `Disadvantage: 1d20 [${values[0]},~~${values[1]}~~] + ${save.value} = `
|
value = a;
|
||||||
|
resultPrefix = `Disadvantage: 1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
|
||||||
|
} else {
|
||||||
|
value = b;
|
||||||
|
resultPrefix = `Disadvantage: 1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
values = rollDice(1, 20);
|
values = rollDice(1, 20);
|
||||||
value = values[0];
|
value = values[0];
|
||||||
resultPrefix = `1d20 [${value}] + ${save.value} = `
|
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
|
||||||
}
|
}
|
||||||
scope['$saveDiceRoll'] = {value};
|
scope['$saveDiceRoll'] = {value};
|
||||||
const result = value + save.value || 0;
|
const result = value + save.value || 0;
|
||||||
@@ -71,7 +85,8 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
|
|||||||
}
|
}
|
||||||
log.content.push({
|
log.content.push({
|
||||||
name: 'Save',
|
name: 'Save',
|
||||||
value: resultPrefix + result + (saveSuccess ? 'Passed' : 'Failed')
|
value: resultPrefix + result + (saveSuccess ? 'Passed' : 'Failed'),
|
||||||
|
inline: true,
|
||||||
});
|
});
|
||||||
return applyChildren();
|
return applyChildren();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import recalculateCalculation from './recalculateCalculation.js'
|
|||||||
|
|
||||||
export default function recalculateInlineCalculations(inlineCalcObj, scope, log){
|
export default function recalculateInlineCalculations(inlineCalcObj, scope, log){
|
||||||
// Skip if there are no calculations
|
// Skip if there are no calculations
|
||||||
if (!inlineCalcObj?.calculations?.length) return;
|
if (!inlineCalcObj?.inlineCalculations?.length) return;
|
||||||
// Recalculate each calculation with the current scope
|
// Recalculate each calculation with the current scope
|
||||||
inlineCalcObj.inlineCalculations.forEach(calc => {
|
inlineCalcObj.inlineCalculations.forEach(calc => {
|
||||||
recalculateCalculation(calc, scope, log);
|
recalculateCalculation(calc, scope, log);
|
||||||
|
|||||||
@@ -24,13 +24,18 @@ const doAction = new ValidatedMethod({
|
|||||||
type: String,
|
type: String,
|
||||||
regEx: SimpleSchema.RegEx.Id,
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
},
|
},
|
||||||
|
scope: {
|
||||||
|
type: Object,
|
||||||
|
blackbox: true,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
}).validator(),
|
}).validator(),
|
||||||
mixins: [RateLimiterMixin],
|
mixins: [RateLimiterMixin],
|
||||||
rateLimit: {
|
rateLimit: {
|
||||||
numRequests: 10,
|
numRequests: 10,
|
||||||
timeInterval: 5000,
|
timeInterval: 5000,
|
||||||
},
|
},
|
||||||
run({actionId, targetIds = []}) {
|
run({actionId, targetIds = [], scope}) {
|
||||||
let action = CreatureProperties.findOne(actionId);
|
let action = CreatureProperties.findOne(actionId);
|
||||||
// Check permissions
|
// Check permissions
|
||||||
let creature = getRootCreatureAncestor(action);
|
let creature = getRootCreatureAncestor(action);
|
||||||
@@ -69,7 +74,7 @@ const doAction = new ValidatedMethod({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Do the action
|
// Do the action
|
||||||
doActionWork({creature, targets, properties, ancestors, method: this});
|
doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope});
|
||||||
|
|
||||||
// Recompute all involved creatures
|
// Recompute all involved creatures
|
||||||
computeCreature(creature._id);
|
computeCreature(creature._id);
|
||||||
@@ -82,7 +87,7 @@ const doAction = new ValidatedMethod({
|
|||||||
export default doAction;
|
export default doAction;
|
||||||
|
|
||||||
export function doActionWork({
|
export function doActionWork({
|
||||||
creature, targets, properties, ancestors, method
|
creature, targets, properties, ancestors, method, methodScope = {}, log
|
||||||
}){
|
}){
|
||||||
// get the docs
|
// get the docs
|
||||||
const ancestorScope = getAncestorScope(ancestors);
|
const ancestorScope = getAncestorScope(ancestors);
|
||||||
@@ -92,7 +97,7 @@ export function doActionWork({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the log
|
// Create the log
|
||||||
let log = CreatureLogSchema.clean({
|
if (!log) log = CreatureLogSchema.clean({
|
||||||
creatureId: creature._id,
|
creatureId: creature._id,
|
||||||
creatureName: creature.name,
|
creatureName: creature.name,
|
||||||
});
|
});
|
||||||
@@ -102,6 +107,7 @@ export function doActionWork({
|
|||||||
const scope = {
|
const scope = {
|
||||||
...creature.variables,
|
...creature.variables,
|
||||||
...ancestorScope,
|
...ancestorScope,
|
||||||
|
...methodScope
|
||||||
}
|
}
|
||||||
applyProperty(propertyForest[0], {
|
applyProperty(propertyForest[0], {
|
||||||
creature,
|
creature,
|
||||||
|
|||||||
142
app/imports/api/engine/actions/doCastSpell.js
Normal file
142
app/imports/api/engine/actions/doCastSpell.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
|
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||||
|
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||||
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
|
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||||
|
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||||
|
import { doActionWork } from '/imports/api/engine/actions/doAction.js';
|
||||||
|
import computeCreature from '/imports/api/engine/computeCreature.js';
|
||||||
|
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
|
||||||
|
|
||||||
|
const doAction = new ValidatedMethod({
|
||||||
|
name: 'creatureProperties.doCastSpell',
|
||||||
|
validate: new SimpleSchema({
|
||||||
|
spellId: SimpleSchema.RegEx.Id,
|
||||||
|
slotId: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
targetIds: {
|
||||||
|
type: Array,
|
||||||
|
defaultValue: [],
|
||||||
|
maxCount: 20,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
'targetIds.$': {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
|
scope: {
|
||||||
|
type: Object,
|
||||||
|
blackbox: true,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
}).validator(),
|
||||||
|
mixins: [RateLimiterMixin],
|
||||||
|
rateLimit: {
|
||||||
|
numRequests: 10,
|
||||||
|
timeInterval: 5000,
|
||||||
|
},
|
||||||
|
run({spellId, slotId, targetIds = [], scope = {}}) {
|
||||||
|
let spell = CreatureProperties.findOne(spellId);
|
||||||
|
// Check permissions
|
||||||
|
let creature = getRootCreatureAncestor(spell);
|
||||||
|
|
||||||
|
assertEditPermission(creature, this.userId);
|
||||||
|
|
||||||
|
// Get all the targets and make sure we can edit them
|
||||||
|
let targets = [];
|
||||||
|
targetIds.forEach(targetId => {
|
||||||
|
let target = Creatures.findOne(targetId);
|
||||||
|
assertEditPermission(target, this.userId);
|
||||||
|
targets.push(target);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch all the action's ancestor creatureProperties
|
||||||
|
const ancestorIds = [];
|
||||||
|
spell.ancestors.forEach(ref => {
|
||||||
|
if (ref.collection === 'creatureProperties') {
|
||||||
|
ancestorIds.push(ref.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get cursor of ancestors
|
||||||
|
const ancestors = CreatureProperties.find({
|
||||||
|
_id: {$in: ancestorIds},
|
||||||
|
}, {
|
||||||
|
sort: {order: 1},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get cursor of the properties
|
||||||
|
const properties = CreatureProperties.find({
|
||||||
|
$or: [{_id: spell._id}, {'ancestors.id': spell._id}],
|
||||||
|
removed: {$ne: true},
|
||||||
|
}, {
|
||||||
|
sort: {order: 1},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Spend the appropriate slot
|
||||||
|
let slotLevel = spell.level || 0;
|
||||||
|
let slot;
|
||||||
|
if (slotId && !spell.castWithoutSpellSlots){
|
||||||
|
slot = CreatureProperties.findOne(slotId);
|
||||||
|
if (!slot){
|
||||||
|
throw new Meteor.Error('No slot',
|
||||||
|
'Slot not found to cast spell');
|
||||||
|
}
|
||||||
|
if (!slot.value){
|
||||||
|
throw new Meteor.Error('No slot',
|
||||||
|
'Slot depleted');
|
||||||
|
}
|
||||||
|
if (slot.attributeType !== 'spellSlot'){
|
||||||
|
throw new Meteor.Error('Not a slot',
|
||||||
|
'The given property is not a valid spell slot');
|
||||||
|
}
|
||||||
|
if (!slot.spellSlotLevel?.value){
|
||||||
|
throw new Meteor.Error('No slot level',
|
||||||
|
'Slot does not have a spell slot level');
|
||||||
|
}
|
||||||
|
if (slot.spellSlotLevel.value < spell.level){
|
||||||
|
throw new Meteor.Error('Slot too small',
|
||||||
|
'Slot is not large enough to cast spell');
|
||||||
|
}
|
||||||
|
slotLevel = slot.spellSlotLevel.value;
|
||||||
|
damagePropertyWork({
|
||||||
|
property: slot,
|
||||||
|
operation: 'increment',
|
||||||
|
value: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
scope['slotLevel'] = slotLevel;
|
||||||
|
|
||||||
|
// Post the slot level spent to the log
|
||||||
|
const log = CreatureLogSchema.clean({
|
||||||
|
creatureId: creature._id,
|
||||||
|
creatureName: creature.name,
|
||||||
|
});
|
||||||
|
if (slot?.spellSlotLevel?.value){
|
||||||
|
log.content.push({
|
||||||
|
name: `Casting using a level ${slotLevel} spell slot`
|
||||||
|
});
|
||||||
|
} else if (slotLevel) {
|
||||||
|
log.content.push({
|
||||||
|
name: `Casting at level ${slotLevel}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do the action
|
||||||
|
doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope, log});
|
||||||
|
|
||||||
|
// Recompute all involved creatures
|
||||||
|
computeCreature(creature._id);
|
||||||
|
targets.forEach(target => {
|
||||||
|
computeCreature(target._id);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default doAction;
|
||||||
114
app/imports/api/engine/actions/doCheck.js
Normal file
114
app/imports/api/engine/actions/doCheck.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import SimpleSchema from 'simpl-schema';
|
||||||
|
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||||
|
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||||
|
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
||||||
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
|
import { CreatureLogSchema, insertCreatureLogWork } from '/imports/api/creature/log/CreatureLogs.js';
|
||||||
|
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
||||||
|
import computeCreature from '/imports/api/engine/computeCreature.js';
|
||||||
|
import rollDice from '/imports/parser/rollDice.js';
|
||||||
|
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||||
|
|
||||||
|
const doCheck = new ValidatedMethod({
|
||||||
|
name: 'creatureProperties.doCheck',
|
||||||
|
validate: new SimpleSchema({
|
||||||
|
propId: SimpleSchema.RegEx.Id,
|
||||||
|
scope: {
|
||||||
|
type: Object,
|
||||||
|
blackbox: true,
|
||||||
|
},
|
||||||
|
}).validator(),
|
||||||
|
mixins: [RateLimiterMixin],
|
||||||
|
rateLimit: {
|
||||||
|
numRequests: 10,
|
||||||
|
timeInterval: 5000,
|
||||||
|
},
|
||||||
|
run({propId, scope}) {
|
||||||
|
const prop = CreatureProperties.findOne(propId);
|
||||||
|
const creature = getRootCreatureAncestor(prop);
|
||||||
|
|
||||||
|
// Check permissions
|
||||||
|
assertEditPermission(creature, this.userId);
|
||||||
|
|
||||||
|
// Do the check
|
||||||
|
doCheckWork({creature, prop, method: this, methodScope: scope});
|
||||||
|
|
||||||
|
// Recompute all involved creatures
|
||||||
|
computeCreature(creature._id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default doCheck;
|
||||||
|
|
||||||
|
export function doCheckWork({
|
||||||
|
creature, prop, method, methodScope = {}
|
||||||
|
}){
|
||||||
|
// Create the log
|
||||||
|
let log = CreatureLogSchema.clean({
|
||||||
|
creatureId: creature._id,
|
||||||
|
creatureName: creature.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
rollCheck({prop, log, methodScope});
|
||||||
|
|
||||||
|
// Insert the log
|
||||||
|
insertCreatureLogWork({log, creature, method});
|
||||||
|
}
|
||||||
|
|
||||||
|
function rollCheck({prop, log, methodScope}){
|
||||||
|
// get the modifier for the roll
|
||||||
|
let rollModifier;
|
||||||
|
let logName = `${prop.name} check`;
|
||||||
|
if (prop.type === 'skill'){
|
||||||
|
rollModifier = prop.value;
|
||||||
|
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'){
|
||||||
|
rollModifier = prop.modifier;
|
||||||
|
} else {
|
||||||
|
rollModifier = prop.value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw (`${prop.type} not supported for checks`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rollModifierText = numberToSignedString(rollModifier, true);
|
||||||
|
|
||||||
|
let value, values, resultPrefix;
|
||||||
|
if (methodScope['$checkAdvantage'] === 1){
|
||||||
|
logName += ' (Advantage)';
|
||||||
|
const [a, b] = rollDice(2, 20);
|
||||||
|
if (a >= b) {
|
||||||
|
value = a;
|
||||||
|
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
|
||||||
|
} else {
|
||||||
|
value = b;
|
||||||
|
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||||
|
}
|
||||||
|
} else if (methodScope['$checkAdvantage'] === -1){
|
||||||
|
logName += ' (Disadvantage)';
|
||||||
|
const [a, b] = rollDice(2, 20);
|
||||||
|
if (a <= b) {
|
||||||
|
value = a;
|
||||||
|
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
|
||||||
|
} else {
|
||||||
|
value = b;
|
||||||
|
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
values = rollDice(1, 20);
|
||||||
|
value = values[0];
|
||||||
|
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
|
||||||
|
}
|
||||||
|
const result = (value + rollModifier) || 0;
|
||||||
|
log.content.push({
|
||||||
|
name: logName,
|
||||||
|
value: `${resultPrefix} **${result}**`,
|
||||||
|
});
|
||||||
|
}
|
||||||
2
app/imports/api/engine/actions/index.js
Normal file
2
app/imports/api/engine/actions/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import './doCastSpell.js';
|
||||||
|
import './doCheck.js';
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import SimpleSchema from 'simpl-schema';
|
|
||||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
|
||||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
|
||||||
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
|
||||||
import getRootCreatureAncestor from '/imports/api/creature/creatureProperties/getRootCreatureAncestor.js';
|
|
||||||
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
|
|
||||||
import computeCreature from '/imports/api/engine/computeCreature.js';
|
|
||||||
import doAction from '../doAction.js';
|
|
||||||
|
|
||||||
const commitAction = new ValidatedMethod({
|
|
||||||
name: 'creatureProperties.doAction',
|
|
||||||
validate: new SimpleSchema({
|
|
||||||
actionId: SimpleSchema.RegEx.Id,
|
|
||||||
targetIds: {
|
|
||||||
type: Array,
|
|
||||||
defaultValue: [],
|
|
||||||
maxCount: 20,
|
|
||||||
optional: true,
|
|
||||||
},
|
|
||||||
'targetIds.$': {
|
|
||||||
type: String,
|
|
||||||
regEx: SimpleSchema.RegEx.Id,
|
|
||||||
},
|
|
||||||
}).validator(),
|
|
||||||
mixins: [RateLimiterMixin],
|
|
||||||
rateLimit: {
|
|
||||||
numRequests: 10,
|
|
||||||
timeInterval: 5000,
|
|
||||||
},
|
|
||||||
run({actionId, targetIds = []}) {
|
|
||||||
let action = CreatureProperties.findOne(actionId);
|
|
||||||
// Check permissions
|
|
||||||
let creature = getRootCreatureAncestor(action);
|
|
||||||
|
|
||||||
assertEditPermission(creature, this.userId);
|
|
||||||
let targets = [];
|
|
||||||
targetIds.forEach(targetId => {
|
|
||||||
let target = Creatures.findOne(targetId);
|
|
||||||
assertEditPermission(target, this.userId);
|
|
||||||
targets.push(target);
|
|
||||||
});
|
|
||||||
doAction({action, creature, targets, method: this});
|
|
||||||
|
|
||||||
// recompute creatures
|
|
||||||
computeCreature(creature._id);
|
|
||||||
|
|
||||||
targets.forEach(target => {
|
|
||||||
computeCreature(target._id);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default commitAction;
|
|
||||||
@@ -10,6 +10,7 @@ export default class CreatureComputation {
|
|||||||
this.scope = {};
|
this.scope = {};
|
||||||
this.props = properties;
|
this.props = properties;
|
||||||
this.dependencyGraph = createGraph();
|
this.dependencyGraph = createGraph();
|
||||||
|
this.errors = [];
|
||||||
|
|
||||||
// Store properties for easy access later
|
// Store properties for easy access later
|
||||||
properties.forEach(prop => {
|
properties.forEach(prop => {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const linkDependenciesByType = {
|
|||||||
effect: linkEffects,
|
effect: linkEffects,
|
||||||
proficiency: linkProficiencies,
|
proficiency: linkProficiencies,
|
||||||
roll: linkRoll,
|
roll: linkRoll,
|
||||||
slot: linkSlot,
|
propertySlot: linkSlot,
|
||||||
skill: linkSkill,
|
skill: linkSkill,
|
||||||
spell: linkAction,
|
spell: linkAction,
|
||||||
spellList: linkSpellList,
|
spellList: linkSpellList,
|
||||||
@@ -30,7 +30,6 @@ function dependOnCalc({dependencyGraph, prop, key}){
|
|||||||
let calc = get(prop, key);
|
let calc = get(prop, key);
|
||||||
if (!calc) return;
|
if (!calc) return;
|
||||||
if (calc.type !== '_calculation'){
|
if (calc.type !== '_calculation'){
|
||||||
console.log(calc);
|
|
||||||
throw `Expected calculation got ${calc.type}`
|
throw `Expected calculation got ${calc.type}`
|
||||||
}
|
}
|
||||||
dependencyGraph.addLink(prop._id, `${prop._id}.${key}`, 'calculation');
|
dependencyGraph.addLink(prop._id, `${prop._id}.${key}`, 'calculation');
|
||||||
@@ -63,7 +62,7 @@ function linkAction(dependencyGraph, prop, {propsById}){
|
|||||||
dependOnCalc({
|
dependOnCalc({
|
||||||
dependencyGraph,
|
dependencyGraph,
|
||||||
prop,
|
prop,
|
||||||
key: `${prop._id}.resources.itemsConsumed.${index}.quantity`,
|
key: `resources.itemsConsumed[${index}].quantity`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// Link attributes consumed
|
// Link attributes consumed
|
||||||
@@ -74,7 +73,7 @@ function linkAction(dependencyGraph, prop, {propsById}){
|
|||||||
dependOnCalc({
|
dependOnCalc({
|
||||||
dependencyGraph,
|
dependencyGraph,
|
||||||
prop,
|
prop,
|
||||||
key: `${prop._id}.resources.attributesConsumed.${index}.quantity`,
|
key: `resources.attributesConsumed[${index}].quantity`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -243,6 +242,9 @@ function linkSkill(dependencyGraph, prop){
|
|||||||
}
|
}
|
||||||
// Skills depend on the creature's proficiencyBonus
|
// Skills depend on the creature's proficiencyBonus
|
||||||
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
|
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
|
||||||
|
|
||||||
|
// Depends on base value
|
||||||
|
dependOnCalc({dependencyGraph, prop, key: 'baseValue'});
|
||||||
}
|
}
|
||||||
|
|
||||||
function linkSlot(dependencyGraph, prop){
|
function linkSlot(dependencyGraph, prop){
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ function parseCalculation(calcObj){
|
|||||||
calcObj.hash = calcHash;
|
calcObj.hash = calcHash;
|
||||||
try {
|
try {
|
||||||
calcObj.parseNode = parse(calcObj.calculation);
|
calcObj.parseNode = parse(calcObj.calculation);
|
||||||
delete calcObj.parseError;
|
calcObj.parseError = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
let error = {
|
let error = {
|
||||||
type: 'evaluation',
|
type: 'evaluation',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
|
|||||||
import CreatureProperties,
|
import CreatureProperties,
|
||||||
{ DenormalisedOnlyCreaturePropertySchema as denormSchema }
|
{ DenormalisedOnlyCreaturePropertySchema as denormSchema }
|
||||||
from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
|
import computedOnlySchemas from '/imports/api/properties/computedOnlyPropertySchemasIndex.js';
|
||||||
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
|
import computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js';
|
||||||
import linkInventory from './buildComputation/linkInventory.js';
|
import linkInventory from './buildComputation/linkInventory.js';
|
||||||
@@ -30,8 +31,9 @@ import removeSchemaFields from './buildComputation/removeSchemaFields.js';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export default function buildCreatureComputation(creatureId){
|
export default function buildCreatureComputation(creatureId){
|
||||||
|
const creature = getCreature(creatureId);
|
||||||
const properties = getProperties(creatureId);
|
const properties = getProperties(creatureId);
|
||||||
const computation = buildComputationFromProps(properties);
|
const computation = buildComputationFromProps(properties, creature);
|
||||||
return computation;
|
return computation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +46,13 @@ function getProperties(creatureId){
|
|||||||
}).fetch();
|
}).fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildComputationFromProps(properties){
|
function getCreature(creatureId){
|
||||||
|
return Creatures.findOne(creatureId, {
|
||||||
|
denormalizedStats: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildComputationFromProps(properties, creature){
|
||||||
|
|
||||||
const computation = new CreatureComputation(properties);
|
const computation = new CreatureComputation(properties);
|
||||||
// Dependency graph where edge(a, b) means a depends on b
|
// Dependency graph where edge(a, b) means a depends on b
|
||||||
@@ -55,6 +63,22 @@ export function buildComputationFromProps(properties){
|
|||||||
// Each link's data is a string representing the link type
|
// Each link's data is a string representing the link type
|
||||||
const dependencyGraph = computation.dependencyGraph;
|
const dependencyGraph = computation.dependencyGraph;
|
||||||
|
|
||||||
|
// Link the denormalizedStats from the creature
|
||||||
|
if (creature && creature.denormalizedStats){
|
||||||
|
if (creature.denormalizedStats.xp){
|
||||||
|
dependencyGraph.addNode('xp', {
|
||||||
|
baseValue: creature.denormalizedStats.xp,
|
||||||
|
type: '_variable'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (creature.denormalizedStats.milestoneLevels){
|
||||||
|
dependencyGraph.addNode('milestoneLevels', {
|
||||||
|
baseValue: creature.denormalizedStats.milestoneLevels,
|
||||||
|
type: '_variable'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Process the properties one by one
|
// Process the properties one by one
|
||||||
properties.forEach(prop => {
|
properties.forEach(prop => {
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import _variable from './computeByType/computeVariable.js';
|
|||||||
import action from './computeByType/computeAction.js';
|
import action from './computeByType/computeAction.js';
|
||||||
import attribute from './computeByType/computeAttribute.js';
|
import attribute from './computeByType/computeAttribute.js';
|
||||||
import skill from './computeByType/computeSkill.js';
|
import skill from './computeByType/computeSkill.js';
|
||||||
import slot from './computeByType/computeSlot.js';
|
import propertySlot from './computeByType/computeSlot.js';
|
||||||
import container from './computeByType/computeContainer.js';
|
import container from './computeByType/computeContainer.js';
|
||||||
import _calculation from './computeByType/computeCalculation.js';
|
import _calculation from './computeByType/computeCalculation.js';
|
||||||
|
|
||||||
@@ -13,6 +13,6 @@ export default Object.freeze({
|
|||||||
attribute,
|
attribute,
|
||||||
container,
|
container,
|
||||||
skill,
|
skill,
|
||||||
slot,
|
propertySlot,
|
||||||
spell: action,
|
spell: action,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default function computSlot(computation, node){
|
export default function computSlot(computation, node){
|
||||||
const prop = node.data;
|
const prop = node.data;
|
||||||
if (prop.quantityExpected){
|
if (prop.quantityExpected && prop.quantityExpected.value){
|
||||||
prop.spaceLeft = prop.quantityExpected - prop.totalFilled;
|
prop.spaceLeft = prop.quantityExpected.value - prop.totalFilled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,16 @@ import computeVariableAsConstant from './computeVariable/computeVariableAsConsta
|
|||||||
import computeVariableAsClass from './computeVariable/computeVariableAsClass.js';
|
import computeVariableAsClass from './computeVariable/computeVariableAsClass.js';
|
||||||
import computeVariableAsToggle from './computeVariable/computeVariableAsToggle.js';
|
import computeVariableAsToggle from './computeVariable/computeVariableAsToggle.js';
|
||||||
import computeImplicitVariable from './computeVariable/computeImplicitVariable.js';
|
import computeImplicitVariable from './computeVariable/computeImplicitVariable.js';
|
||||||
|
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
||||||
|
|
||||||
export default function computeVariable(computation, node){
|
export default function computeVariable(computation, node){
|
||||||
const scope = computation.scope;
|
const scope = computation.scope;
|
||||||
if (!node.data) node.data = {};
|
if (!node.data) node.data = {};
|
||||||
aggregateLinks(computation, node);
|
aggregateLinks(computation, node);
|
||||||
combineAggregations(computation, node);
|
combineAggregations(computation, node);
|
||||||
|
// Don't add to the scope if the node id is not a legitimate variable name
|
||||||
|
// Without this `some.thing` could break the entire sheet as a database key
|
||||||
|
if (!VARIABLE_NAME_REGEX.test(node.id)) return;
|
||||||
if (node.data.definingProp){
|
if (node.data.definingProp){
|
||||||
// Add the defining variable to the scope
|
// Add the defining variable to the scope
|
||||||
scope[node.id] = node.data.definingProp
|
scope[node.id] = node.data.definingProp
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export default function aggregateClassLevel({node, linkedNode, link}){
|
export default function aggregateClassLevel({node, linkedNode, link}){
|
||||||
|
if (node.data.inactive) return;
|
||||||
if (link.data === 'classLevel'){
|
if (link.data === 'classLevel'){
|
||||||
if (node.data.inactive) return;
|
|
||||||
if (!node.data.classLevelAggregator) node.data.classLevelAggregator = {
|
if (!node.data.classLevelAggregator) node.data.classLevelAggregator = {
|
||||||
levelsFilled: [true], // Level 0 is always filled
|
levelsFilled: [true], // Level 0 is always filled
|
||||||
level: 0,
|
level: 0,
|
||||||
@@ -11,6 +11,6 @@ export default function aggregateClassLevel({node, linkedNode, link}){
|
|||||||
aggregator.levelsFilled[linkedProp.level] = true;
|
aggregator.levelsFilled[linkedProp.level] = true;
|
||||||
} else if (link.data === 'level'){
|
} else if (link.data === 'level'){
|
||||||
node.data.baseValue = (node.data.baseValue || 0) +
|
node.data.baseValue = (node.data.baseValue || 0) +
|
||||||
linkedNode.data.classLevelAggregator.level;
|
(linkedNode.data.classLevelAggregator?.level || 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,18 +43,26 @@ export default function aggregateInventory({node, linkedNode, link}){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function quantity(prop){
|
||||||
|
if (typeof prop.quantity === 'number'){
|
||||||
|
return prop.quantity;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function weight(prop){
|
function weight(prop){
|
||||||
return (prop.weight || 0) + (prop.contentsWeight || 0);
|
return (prop.weight || 0) * quantity(prop) + (prop.contentsWeight || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function carriedWeight(prop){
|
function carriedWeight(prop){
|
||||||
return (prop.weight || 0) + (prop.carriedWeight || 0);
|
return (prop.weight || 0) * quantity(prop) + (prop.carriedWeight || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function value (prop){
|
function value (prop){
|
||||||
return (prop.value || 0) + (prop.contentsValue || 0);
|
return (prop.value || 0) * quantity(prop) + (prop.contentsValue || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function carriedValue (prop){
|
function carriedValue (prop){
|
||||||
return (prop.value || 0) + (prop.carriedValue || 0);
|
return (prop.value || 0) * quantity(prop) + (prop.carriedValue || 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default function(){
|
|||||||
assert.equal(prop.usesLeft, 2);
|
assert.equal(prop.usesLeft, 2);
|
||||||
|
|
||||||
const rolled = computation.propsById['rolledDescriptionId'];
|
const rolled = computation.propsById['rolledDescriptionId'];
|
||||||
assert.equal(rolled.summary.value, 'test roll gets compiled d4 + 4 properly');
|
assert.equal(rolled.summary.value, 'test roll gets compiled 8 properly');
|
||||||
|
|
||||||
const itemConsumed = prop.resources.itemsConsumed[0];
|
const itemConsumed = prop.resources.itemsConsumed[0];
|
||||||
assert.equal(itemConsumed.quantity.value, 3);
|
assert.equal(itemConsumed.quantity.value, 3);
|
||||||
@@ -67,7 +67,7 @@ var testProperties = [
|
|||||||
type: 'action',
|
type: 'action',
|
||||||
ancestors: [{id: 'charId'}],
|
ancestors: [{id: 'charId'}],
|
||||||
summary: {
|
summary: {
|
||||||
text: 'test roll gets compiled {1d4 + (2 + 2)} properly',
|
text: 'test roll gets compiled {4 + (2 + 2)} properly',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
clean({
|
clean({
|
||||||
|
|||||||
@@ -14,16 +14,14 @@ export default function(){
|
|||||||
|
|
||||||
assert.equal(scope('itemsAttuned'), 1);
|
assert.equal(scope('itemsAttuned'), 1);
|
||||||
|
|
||||||
assert.equal(prop('childContainerId').carriedWeight, 23);
|
assert.equal(prop('childContainerId').carriedWeight, 69);
|
||||||
assert.equal(prop('childContainerId').contentsWeight, 23);
|
assert.equal(prop('childContainerId').contentsWeight, 69);
|
||||||
|
|
||||||
assert.equal(scope('weightCarried'), 58);
|
assert.equal(scope('weightCarried'), 104);
|
||||||
|
assert.equal(scope('valueCarried'), 129);
|
||||||
|
|
||||||
assert.equal(scope('weightCarried'), 58);
|
assert.equal(scope('weightTotal'), 104);
|
||||||
assert.equal(scope('valueCarried'), 71);
|
assert.equal(scope('valueTotal'), 129);
|
||||||
|
|
||||||
assert.equal(scope('weightTotal'), 58);
|
|
||||||
assert.equal(scope('valueTotal'), 71);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var testProperties = [
|
var testProperties = [
|
||||||
@@ -62,8 +60,9 @@ var testProperties = [
|
|||||||
clean({
|
clean({
|
||||||
_id: 'grandchildItemId',
|
_id: 'grandchildItemId',
|
||||||
type: 'item',
|
type: 'item',
|
||||||
weight: 23,
|
weight: 23, // 69 total
|
||||||
value: 29,
|
value: 29, // 87 total
|
||||||
|
quantity: 3,
|
||||||
ancestors: [{id: 'charId'}, {id: 'containerId'}, {id: 'childContainerId'}],
|
ancestors: [{id: 'charId'}, {id: 'containerId'}, {id: 'childContainerId'}],
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import computeToggles from '/imports/api/engine/computation/computeComputation/computeToggles.js';
|
import computeToggles from '/imports/api/engine/computation/computeComputation/computeToggles.js';
|
||||||
import computeByType from '/imports/api/engine/computation/computeComputation/computeByType.js';
|
import computeByType from '/imports/api/engine/computation/computeComputation/computeByType.js';
|
||||||
import embedInlineCalculations from './utility/embedInlineCalculations.js';
|
import embedInlineCalculations from './utility/embedInlineCalculations.js';
|
||||||
|
import path from 'ngraph.path';
|
||||||
|
|
||||||
export default function computeCreatureComputation(computation){
|
export default function computeCreatureComputation(computation){
|
||||||
const stack = [];
|
const stack = [];
|
||||||
@@ -12,6 +13,12 @@ export default function computeCreatureComputation(computation){
|
|||||||
node._visitedChildren = false;
|
node._visitedChildren = false;
|
||||||
stack.push(node)
|
stack.push(node)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The graph nodes in the stack are ordered, by reversing the order we
|
||||||
|
// compute higher nodes in the tree first, which for dep loops is more likely
|
||||||
|
// to be a good guess of where to start thant the inverse
|
||||||
|
stack.reverse();
|
||||||
|
|
||||||
// Depth first traversal of nodes
|
// Depth first traversal of nodes
|
||||||
while (stack.length){
|
while (stack.length){
|
||||||
let top = stack[stack.length - 1];
|
let top = stack[stack.length - 1];
|
||||||
@@ -27,7 +34,7 @@ export default function computeCreatureComputation(computation){
|
|||||||
} else {
|
} else {
|
||||||
top._visitedChildren = true;
|
top._visitedChildren = true;
|
||||||
// Push dependencies to graph to be computed first
|
// Push dependencies to graph to be computed first
|
||||||
pushDependenciesToStack(top.id, graph, stack);
|
pushDependenciesToStack(top.id, graph, stack, computation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,8 +49,20 @@ function compute(computation, node){
|
|||||||
computeByType[node.data?.type || '_variable']?.(computation, node);
|
computeByType[node.data?.type || '_variable']?.(computation, node);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pushDependenciesToStack(nodeId, graph, stack){
|
function pushDependenciesToStack(nodeId, graph, stack, computation){
|
||||||
graph.forEachLinkedNode(nodeId, linkedNode => {
|
graph.forEachLinkedNode(nodeId, linkedNode => {
|
||||||
|
if (linkedNode._visitedChildren && !linkedNode._visited){
|
||||||
|
const pather = path.nba(graph, {
|
||||||
|
oriented: true
|
||||||
|
});
|
||||||
|
const loop = pather.find(nodeId, nodeId);
|
||||||
|
computation.errors.push({
|
||||||
|
type: 'dependencyLoop',
|
||||||
|
details: {
|
||||||
|
nodes: loop.map(node => node.id)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
stack.push(linkedNode);
|
stack.push(linkedNode);
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import Creatures from '/imports/api/creature/creatures/Creatures.js';
|
||||||
|
|
||||||
|
export default function(creatureId, errors = []){
|
||||||
|
if (errors.length){
|
||||||
|
Creatures.update(creatureId, {$set: {computeErrors: errors}});
|
||||||
|
} else {
|
||||||
|
Creatures.update(creatureId, {$unset: {computeErrors: 1}});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,23 @@ import buildCreatureComputation from './computation/buildCreatureComputation.js'
|
|||||||
import computeCreatureComputation from './computation/computeCreatureComputation.js';
|
import computeCreatureComputation from './computation/computeCreatureComputation.js';
|
||||||
import writeAlteredProperties from './computation/writeComputation/writeAlteredProperties.js';
|
import writeAlteredProperties from './computation/writeComputation/writeAlteredProperties.js';
|
||||||
import writeScope from './computation/writeComputation/writeScope.js';
|
import writeScope from './computation/writeComputation/writeScope.js';
|
||||||
|
import writeErrors from './computation/writeComputation/writeErrors.js';
|
||||||
|
|
||||||
export default function computeCreature(creatureId){
|
export default function computeCreature(creatureId){
|
||||||
|
if (Meteor.isClient) return;
|
||||||
const computation = buildCreatureComputation(creatureId);
|
const computation = buildCreatureComputation(creatureId);
|
||||||
computeCreatureComputation(computation);
|
try {
|
||||||
writeAlteredProperties(computation);
|
computeCreatureComputation(computation);
|
||||||
writeScope(creatureId, computation.scope);
|
writeAlteredProperties(computation);
|
||||||
|
writeScope(creatureId, computation.scope);
|
||||||
|
} catch (e){
|
||||||
|
computation.errors.push({
|
||||||
|
type: 'crash',
|
||||||
|
details: e.reason,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
writeErrors(creatureId, computation.errors);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now just recompute the whole creature, TODO only recompute a single
|
// For now just recompute the whole creature, TODO only recompute a single
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ import { restore } from '/imports/api/parenting/softRemove.js';
|
|||||||
let LibraryNodes = new Mongo.Collection('libraryNodes');
|
let LibraryNodes = new Mongo.Collection('libraryNodes');
|
||||||
|
|
||||||
let LibraryNodeSchema = new SimpleSchema({
|
let LibraryNodeSchema = new SimpleSchema({
|
||||||
|
_id: {
|
||||||
|
type: String,
|
||||||
|
regEx: SimpleSchema.RegEx.Id,
|
||||||
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
allowedValues: Object.keys(propertySchemasIndex),
|
allowedValues: Object.keys(propertySchemasIndex),
|
||||||
|
|||||||
@@ -26,9 +26,15 @@ const ClassLevelSchema = createPropertySchema({
|
|||||||
defaultValue: 1,
|
defaultValue: 1,
|
||||||
max: STORAGE_LIMITS.levelMax,
|
max: STORAGE_LIMITS.levelMax,
|
||||||
},
|
},
|
||||||
|
// Filters out of UI if condition isn't met, but isn't otherwise enforced
|
||||||
|
slotFillerCondition: {
|
||||||
|
type: String,
|
||||||
|
optional: true,
|
||||||
|
max: STORAGE_LIMITS.calculation,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const ComputedOnlyClassLevelSchema = new SimpleSchema({
|
const ComputedOnlyClassLevelSchema = createPropertySchema({
|
||||||
description: {
|
description: {
|
||||||
type: 'computedOnlyInlineCalculationField',
|
type: 'computedOnlyInlineCalculationField',
|
||||||
optional: true,
|
optional: true,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ let SpellListSchema = createPropertySchema({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const ComputedOnlySpellListSchema = new SimpleSchema({
|
const ComputedOnlySpellListSchema = createPropertySchema({
|
||||||
description: {
|
description: {
|
||||||
type: 'computedOnlyInlineCalculationField',
|
type: 'computedOnlyInlineCalculationField',
|
||||||
optional: true,
|
optional: true,
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ function computedOnlyInlineCalculationField(field){
|
|||||||
},
|
},
|
||||||
[`${field}.inlineCalculations.$`]: {
|
[`${field}.inlineCalculations.$`]: {
|
||||||
type: Object,
|
type: Object,
|
||||||
parseLevel: 'compile',
|
parseLevel: 'reduce',
|
||||||
computedField: true,
|
computedField: true,
|
||||||
},
|
},
|
||||||
// The part between bracers {}
|
// The part between bracers {}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Must contain a letter, and be made of word characters only
|
// Must contain a letter, and be made of word characters only
|
||||||
const VARIABLE_NAME_REGEX = /^\w*[a-z]\w*$/i;
|
const VARIABLE_NAME_REGEX = /^[a-z][\w-]*$/i;
|
||||||
|
|
||||||
export default VARIABLE_NAME_REGEX;
|
export default VARIABLE_NAME_REGEX;
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ const transformsByPropType = {
|
|||||||
function getComputedPropertyTransforms(key, toKey){
|
function getComputedPropertyTransforms(key, toKey){
|
||||||
if (!toKey) toKey = key;
|
if (!toKey) toKey = key;
|
||||||
return [
|
return [
|
||||||
{from: key, to: `${key}.calculation`, up: calculationUp, down: calculationDown},
|
{from: key, to: `${toKey}.calculation`, up: calculationUp, down: calculationDown},
|
||||||
{from: `${key}Result`, to: `${toKey}.value`, up: nanToNull},
|
{from: `${key}Result`, to: `${toKey}.value`, up: nanToNull},
|
||||||
{from: `${key}Errors`, to: `${toKey}.errors`, up: trimErrors},
|
{from: `${key}Errors`, to: `${toKey}.errors`, up: trimErrors},
|
||||||
];
|
];
|
||||||
@@ -209,7 +209,7 @@ function getComputedPropertyTransforms(key, toKey){
|
|||||||
|
|
||||||
function getInlineComputationTransforms(key){
|
function getInlineComputationTransforms(key){
|
||||||
return [
|
return [
|
||||||
{from: key, to: `${key}.text`},
|
{from: key, to: `${key}.text`, up: calculationUp, down: calculationDown},
|
||||||
{from: `${key}Calculations`, to: `${key}.inlineCalculations`, up: calculationUp, down: calculationDown},
|
{from: `${key}Calculations`, to: `${key}.inlineCalculations`, up: calculationUp, down: calculationDown},
|
||||||
{from: `${key}Calculations.$.result`, to: `${key}.inlineCalculations.$.value`},
|
{from: `${key}Calculations.$.result`, to: `${key}.inlineCalculations.$.value`},
|
||||||
];
|
];
|
||||||
@@ -217,7 +217,9 @@ function getInlineComputationTransforms(key){
|
|||||||
|
|
||||||
function calculationUp(val){
|
function calculationUp(val){
|
||||||
if (typeof val !== 'string') return val;
|
if (typeof val !== 'string') return val;
|
||||||
return val.replace('.value', '.total').replace('.currentValue', '.value');
|
return val.replace(/#(\w+).(\w+)Result/g, '#$1.$2')
|
||||||
|
.replace('.value', '.total')
|
||||||
|
.replace('.currentValue', '.value');
|
||||||
}
|
}
|
||||||
|
|
||||||
function calculationDown(val){
|
function calculationDown(val){
|
||||||
|
|||||||
@@ -1,118 +1,217 @@
|
|||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import { migrateProperty } from './dbv1.js';
|
import {
|
||||||
import { assert } from 'chai';
|
migrateProperty
|
||||||
|
} from './dbv1.js';
|
||||||
|
import {
|
||||||
|
assert
|
||||||
|
} from 'chai';
|
||||||
|
import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
||||||
|
|
||||||
const exampleAction = {
|
const exampleAction = {
|
||||||
'_id':'hY5MKZ4ivaoTRpNWy',
|
'_id': 'hY5MKZ4ivaoTRpNWy',
|
||||||
'actionType':'bonus',
|
'actionType': 'bonus',
|
||||||
'target':'singleTarget',
|
'target': 'singleTarget',
|
||||||
'tags':[],
|
'tags': [],
|
||||||
'resources':{
|
'resources': {
|
||||||
'itemsConsumed':[],
|
'itemsConsumed': [],
|
||||||
'attributesConsumed':[{
|
'attributesConsumed': [{
|
||||||
'_id':'FaK6jXEj3pSe7mNuu',
|
'_id': 'FaK6jXEj3pSe7mNuu',
|
||||||
'quantity': '1',
|
'quantity': '1',
|
||||||
'variableName':'HunterTech',
|
'variableName': 'HunterTech',
|
||||||
'statName':'Hunter\'s Technique',
|
'statName': 'Hunter\'s Technique',
|
||||||
'available':5
|
'available': 5
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
'type':'action',
|
'type': 'action',
|
||||||
'name':'Hexblade\\\'s Curse',
|
'name': 'Hexblade\\\'s Curse',
|
||||||
'parent':{
|
'parent': {
|
||||||
'id':'JqtDmqa5Zd3xpts5G',
|
'id': 'JqtDmqa5Zd3xpts5G',
|
||||||
'collection':'creatureProperties'
|
'collection': 'creatureProperties'
|
||||||
},
|
},
|
||||||
'ancestors':[
|
'ancestors': [{
|
||||||
{
|
'collection': 'creatures',
|
||||||
'collection':'creatures',
|
'id': 'X9rzFhsgFhodYfHmG'
|
||||||
'id':'X9rzFhsgFhodYfHmG'
|
}, ],
|
||||||
},
|
'order': 315,
|
||||||
],
|
'summary': 'Curse a creature for 1 minute. The curse ends early if {warlock.level >14 ? "" : "the target dies, or"} you are incapacitated. \nGain the following benefits: \n- *Bonus to damage rolls against the cursed target of* **+{proficiencyBonus}**. \n- Any attack roll you make against the cursed target is a **critical hit on a roll of 19 or 20**. \n- If the cursed target dies, you **regain {warlock.level+charisma.modifier} hit points**. \n{warlock.level <9 ? "" : "- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses."}',
|
||||||
'order':315,
|
'uses': '1',
|
||||||
'summary':'Curse a creature for 1 minute. The curse ends early if {warlock.level >14 ? "" : "the target dies, or"} you are incapacitated. \nGain the following benefits: \n- *Bonus to damage rolls against the cursed target of* **+{proficiencyBonus}**. \n- Any attack roll you make against the cursed target is a **critical hit on a roll of 19 or 20**. \n- If the cursed target dies, you **regain {warlock.level+charisma.modifier} hit points**. \n{warlock.level <9 ? "" : "- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses."}',
|
'usesResult': 1,
|
||||||
'uses':'1',
|
'reset': 'shortRest',
|
||||||
'usesResult':1,
|
'usesUsed': 0,
|
||||||
'reset':'shortRest',
|
'description': 'Starting at 1st level, you gain the ability to place a baleful curse on someone. As a bonus action, choose one creature you can see within 30 feet of you. The target is cursed for 1 minute. The curse ends early if the target dies, you die, or you are incapacitated. Until the curse ends, you gain the following benefits:\n\n- You gain a bonus to damage rolls against the cursed target. The bonus equals your proficiency bonus.\n- Any attack roll you make against the cursed target is a critical hit on a roll of 19 or 20 on the d20.\n- If the cursed target dies, you regain hit points equal to your warlock level + your Charisma modifier (minimum of 1 hit point). \n{warlock.level <10 ? "" :"- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses."} \nYou can\\\'t use this feature again until you finish a short or long rest.',
|
||||||
'usesUsed':0,
|
'color': '#8e24aa',
|
||||||
'description':'Starting at 1st level, you gain the ability to place a baleful curse on someone. As a bonus action, choose one creature you can see within 30 feet of you. The target is cursed for 1 minute. The curse ends early if the target dies, you die, or you are incapacitated. Until the curse ends, you gain the following benefits:\n\n- You gain a bonus to damage rolls against the cursed target. The bonus equals your proficiency bonus.\n- Any attack roll you make against the cursed target is a critical hit on a roll of 19 or 20 on the d20.\n- If the cursed target dies, you regain hit points equal to your warlock level + your Charisma modifier (minimum of 1 hit point). \n{warlock.level <10 ? "" :"- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses."} \nYou can\\\'t use this feature again until you finish a short or long rest.',
|
'descriptionCalculations': [{
|
||||||
'color':'#8e24aa',
|
'calculation': 'warlock.level <10 ? "" :"- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses."',
|
||||||
'descriptionCalculations':[
|
'result': '- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses.'
|
||||||
{
|
}],
|
||||||
'calculation':'warlock.level <10 ? "" :"- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses."',
|
'summaryCalculations': [{
|
||||||
'result':'- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses.'
|
'calculation': 'warlock.level >14 ? "" : "the target dies, or"',
|
||||||
}
|
'result': 'the target dies, or'
|
||||||
],
|
},
|
||||||
'summaryCalculations':[
|
{
|
||||||
{
|
'calculation': 'proficiencyBonus',
|
||||||
'calculation':'warlock.level >14 ? "" : "the target dies, or"',
|
'result': '4'
|
||||||
'result':'the target dies, or'
|
},
|
||||||
},
|
{
|
||||||
{
|
'calculation': 'warlock.level+charisma.modifier',
|
||||||
'calculation':'proficiencyBonus',
|
'result': '15'
|
||||||
'result':'4'
|
},
|
||||||
},
|
{
|
||||||
{
|
'calculation': 'warlock.level <9 ? "" : "- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses."',
|
||||||
'calculation':'warlock.level+charisma.modifier',
|
'result': '- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses.'
|
||||||
'result':'15'
|
}
|
||||||
},
|
]
|
||||||
{
|
|
||||||
'calculation':'warlock.level <9 ? "" : "- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses."',
|
|
||||||
'result':'- If you are hit with an attack by your cursed target, use your reaction to roll a d6. On a 4 or higher, the attack instead misses.'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const exampleAttribute = {
|
const exampleAttribute = {
|
||||||
_id:'idRWyoj5oxCv73feM',
|
_id: 'idRWyoj5oxCv73feM',
|
||||||
name:'Hit Dice',
|
name: 'Hit Dice',
|
||||||
variableName:'clericHitDice',
|
variableName: 'clericHitDice',
|
||||||
attributeType:'hitDice',
|
attributeType: 'hitDice',
|
||||||
type:'attribute',
|
type: 'attribute',
|
||||||
hitDiceSize:'d8',
|
hitDiceSize: 'd8',
|
||||||
baseValueCalculation:'cleric.level',
|
baseValueCalculation: 'cleric.level',
|
||||||
parent:{'id':'8jSWKxvgQyKbunFtD','collection':'creatureProperties'},
|
parent: {
|
||||||
ancestors:[
|
'id': '8jSWKxvgQyKbunFtD',
|
||||||
{'collection':'creatures','id':'m9sdCvs6iDf7qRaGv'},
|
'collection': 'creatureProperties'
|
||||||
{'id':'8jSWKxvgQyKbunFtD','collection':'creatureProperties'}
|
},
|
||||||
|
ancestors: [{
|
||||||
|
'collection': 'creatures',
|
||||||
|
'id': 'm9sdCvs6iDf7qRaGv'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': '8jSWKxvgQyKbunFtD',
|
||||||
|
'collection': 'creatureProperties'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
order: 84,
|
order: 84,
|
||||||
value: 20,
|
value: 20,
|
||||||
tags:[],
|
tags: [],
|
||||||
baseValue: 20,
|
baseValue: 20,
|
||||||
damage: 3,
|
damage: 3,
|
||||||
currentValue: 17,
|
currentValue: 17,
|
||||||
constitutionMod: 2,
|
constitutionMod: 2,
|
||||||
dependencies: ['8jSWKxvgQyKbunFtD','qPP5yQXPxS7uhuXo3']
|
dependencies: ['8jSWKxvgQyKbunFtD', 'qPP5yQXPxS7uhuXo3']
|
||||||
};
|
};
|
||||||
|
|
||||||
const expectedMigratedAttribute = {
|
const expectedMigratedAttribute = {
|
||||||
_id:'idRWyoj5oxCv73feM',
|
_id: 'idRWyoj5oxCv73feM',
|
||||||
name:'Hit Dice',
|
name: 'Hit Dice',
|
||||||
variableName:'clericHitDice',
|
variableName: 'clericHitDice',
|
||||||
attributeType:'hitDice',
|
attributeType: 'hitDice',
|
||||||
type:'attribute',
|
type: 'attribute',
|
||||||
hitDiceSize:'d8',
|
hitDiceSize: 'd8',
|
||||||
baseValue: {
|
baseValue: {
|
||||||
calculation: 'cleric.level',
|
calculation: 'cleric.level',
|
||||||
value: 20
|
value: 20
|
||||||
},
|
},
|
||||||
parent:{'id':'8jSWKxvgQyKbunFtD','collection':'creatureProperties'},
|
parent: {
|
||||||
ancestors:[
|
'id': '8jSWKxvgQyKbunFtD',
|
||||||
{'collection':'creatures','id':'m9sdCvs6iDf7qRaGv'},
|
'collection': 'creatureProperties'
|
||||||
{'id':'8jSWKxvgQyKbunFtD','collection':'creatureProperties'}
|
},
|
||||||
|
ancestors: [{
|
||||||
|
'collection': 'creatures',
|
||||||
|
'id': 'm9sdCvs6iDf7qRaGv'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': '8jSWKxvgQyKbunFtD',
|
||||||
|
'collection': 'creatureProperties'
|
||||||
|
}
|
||||||
],
|
],
|
||||||
order: 84,
|
order: 84,
|
||||||
total: 20,
|
total: 20,
|
||||||
tags:[],
|
tags: [],
|
||||||
damage: 3,
|
damage: 3,
|
||||||
value: 17,
|
value: 17,
|
||||||
constitutionMod: 2,
|
constitutionMod: 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('migrateProperty', function () {
|
const exampleAttack = {
|
||||||
it('Migrates actions reversibly', function () {
|
'_id': 'vw23EnJwBRcXEJg7i',
|
||||||
const action = {...exampleAction};
|
'actionType': 'attack',
|
||||||
|
'target': 'singleTarget',
|
||||||
|
'tags': ['attack'],
|
||||||
|
'results': {
|
||||||
|
'adjustments': [],
|
||||||
|
'damages': [{
|
||||||
|
'_id': 'RGJMeNJXBeqZsGmAw',
|
||||||
|
'damage': '1d4 + strength.modifier',
|
||||||
|
'target': 'every',
|
||||||
|
'damageType': 'slashing'
|
||||||
|
}],
|
||||||
|
'buffs': []
|
||||||
|
},
|
||||||
|
'resources': {
|
||||||
|
'itemsConsumed': [],
|
||||||
|
'attributesConsumed': []
|
||||||
|
},
|
||||||
|
'rollBonus': 'dexterity.modifier + proficiencyBonus + 2 - hp.value + hp.currentValue',
|
||||||
|
'type': 'attack',
|
||||||
|
'name': 'Claws',
|
||||||
|
'parent': {
|
||||||
|
'id': 'Jpx8q3WjM5SCoGBm8',
|
||||||
|
'collection': 'creatureProperties'
|
||||||
|
},
|
||||||
|
'ancestors': [{
|
||||||
|
'collection': 'creatures',
|
||||||
|
'id': 'm9sdCvs6iDf7qRaGv'
|
||||||
|
}, {
|
||||||
|
'id': '3WS2xsSPAqB4eF9YH',
|
||||||
|
'collection': 'creatureProperties'
|
||||||
|
}, {
|
||||||
|
'id': 'rhYLEycvtHjcioaQL',
|
||||||
|
'collection': 'creatureProperties'
|
||||||
|
}, {
|
||||||
|
'id': 'Jpx8q3WjM5SCoGBm8',
|
||||||
|
'collection': 'creatureProperties'
|
||||||
|
}],
|
||||||
|
'order': 56,
|
||||||
|
'rollBonusResult': 6,
|
||||||
|
'usesUsed': 2,
|
||||||
|
'dependencies': ['pg6cK5ghHTFvo8uyK', 'gAJBKYqXz2BPc9Aqf']
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectedMigratedAttack = {
|
||||||
|
'_id': 'vw23EnJwBRcXEJg7i',
|
||||||
|
'actionType': 'attack',
|
||||||
|
'target': 'singleTarget',
|
||||||
|
'tags': ['attack'],
|
||||||
|
'resources': {
|
||||||
|
'itemsConsumed': [],
|
||||||
|
'attributesConsumed': []
|
||||||
|
},
|
||||||
|
'attackRoll': {
|
||||||
|
calculation: 'dexterity.modifier + proficiencyBonus + 2 - hp.total + hp.value',
|
||||||
|
},
|
||||||
|
'type': 'action',
|
||||||
|
'name': 'Claws',
|
||||||
|
'parent': {
|
||||||
|
'id': 'Jpx8q3WjM5SCoGBm8',
|
||||||
|
'collection': 'creatureProperties'
|
||||||
|
},
|
||||||
|
'ancestors': [{
|
||||||
|
'collection': 'creatures',
|
||||||
|
'id': 'm9sdCvs6iDf7qRaGv'
|
||||||
|
}, {
|
||||||
|
'id': '3WS2xsSPAqB4eF9YH',
|
||||||
|
'collection': 'creatureProperties'
|
||||||
|
}, {
|
||||||
|
'id': 'rhYLEycvtHjcioaQL',
|
||||||
|
'collection': 'creatureProperties'
|
||||||
|
}, {
|
||||||
|
'id': 'Jpx8q3WjM5SCoGBm8',
|
||||||
|
'collection': 'creatureProperties'
|
||||||
|
}],
|
||||||
|
'order': 56,
|
||||||
|
'usesUsed': 2,
|
||||||
|
libraryTags: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('migrateProperty', function() {
|
||||||
|
it('Migrates actions reversibly', function() {
|
||||||
|
const action = {
|
||||||
|
...exampleAction
|
||||||
|
};
|
||||||
const newAction = migrateProperty({
|
const newAction = migrateProperty({
|
||||||
collection: CreatureProperties,
|
collection: CreatureProperties,
|
||||||
prop: action
|
prop: action
|
||||||
@@ -125,8 +224,10 @@ describe('migrateProperty', function () {
|
|||||||
assert.deepEqual(action, exampleAction, 'action should not be bashed');
|
assert.deepEqual(action, exampleAction, 'action should not be bashed');
|
||||||
assert.deepEqual(exampleAction, reversedAction, 'operation should be reversible');
|
assert.deepEqual(exampleAction, reversedAction, 'operation should be reversible');
|
||||||
});
|
});
|
||||||
it ('Migrates attributes as expected', function(){
|
it('Migrates attributes as expected', function() {
|
||||||
const attribute = {...exampleAttribute};
|
const attribute = {
|
||||||
|
...exampleAttribute
|
||||||
|
};
|
||||||
const newAttribute = migrateProperty({
|
const newAttribute = migrateProperty({
|
||||||
collection: CreatureProperties,
|
collection: CreatureProperties,
|
||||||
prop: attribute
|
prop: attribute
|
||||||
@@ -134,4 +235,15 @@ describe('migrateProperty', function () {
|
|||||||
assert.deepEqual(newAttribute, expectedMigratedAttribute,
|
assert.deepEqual(newAttribute, expectedMigratedAttribute,
|
||||||
'Attribute should match the expected result');
|
'Attribute should match the expected result');
|
||||||
});
|
});
|
||||||
|
it('Migrates attacks as expected', function() {
|
||||||
|
const attribute = {
|
||||||
|
...exampleAttack
|
||||||
|
};
|
||||||
|
const newAttribute = migrateProperty({
|
||||||
|
collection: LibraryNodes,
|
||||||
|
prop: attribute
|
||||||
|
});
|
||||||
|
assert.deepEqual(newAttribute, expectedMigratedAttack,
|
||||||
|
'Attribute should match the expected result');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import resolve from '/imports/parser/resolve.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
'abs': {
|
'abs': {
|
||||||
comment: 'Returns the absolute value of a number',
|
comment: 'Returns the absolute value of a number',
|
||||||
@@ -108,6 +110,18 @@ export default {
|
|||||||
}
|
}
|
||||||
return arrayNode.values.length;
|
return arrayNode.values.length;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'resolve': {
|
||||||
|
comment: 'Forces the given calcultion to resolve into a number',
|
||||||
|
examples: [
|
||||||
|
{input: 'resolve(someUndefinedVariable + 3 + 4)', result: '7'},
|
||||||
|
{input: 'resolve(3d6)', result: '2'},
|
||||||
|
],
|
||||||
|
arguments: ['parseNode'],
|
||||||
|
fn: function resolveFn(node){
|
||||||
|
let {result} = resolve('reduce', node, this.scope, this.context);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function id(x) { return x[0]; }
|
|||||||
value: s => s.slice(1, -1),
|
value: s => s.slice(1, -1),
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
match: /[a-zA-Z_#]*[a-ce-zA-Z_#][a-zA-Z0-9_#]*/,
|
match: /[a-zA-Z_#$]*[a-ce-zA-Z_#$][a-zA-Z0-9_#$]*/,
|
||||||
type: moo.keywords({
|
type: moo.keywords({
|
||||||
'keywords': ['true', 'false'],
|
'keywords': ['true', 'false'],
|
||||||
}),
|
}),
|
||||||
@@ -79,7 +79,7 @@ let ParserRules = [
|
|||||||
{"name": "exponentExpression", "symbols": ["unaryExpression"], "postprocess": id},
|
{"name": "exponentExpression", "symbols": ["unaryExpression"], "postprocess": id},
|
||||||
{"name": "unaryExpression", "symbols": [(lexer.has("additiveOperator") ? {type: "additiveOperator"} : additiveOperator), "_", "unaryExpression"], "postprocess": d => node.unaryOperator.create({operator: d[0].value, right: d[2]})},
|
{"name": "unaryExpression", "symbols": [(lexer.has("additiveOperator") ? {type: "additiveOperator"} : additiveOperator), "_", "unaryExpression"], "postprocess": d => node.unaryOperator.create({operator: d[0].value, right: d[2]})},
|
||||||
{"name": "unaryExpression", "symbols": ["notExpression"], "postprocess": id},
|
{"name": "unaryExpression", "symbols": ["notExpression"], "postprocess": id},
|
||||||
{"name": "notExpression", "symbols": [(lexer.has("notOperator") ? {type: "notOperator"} : notOperator), "_", "notExpression"], "postprocess": d => node.notOperator.create({right: d[2]})},
|
{"name": "notExpression", "symbols": [(lexer.has("notOperator") ? {type: "notOperator"} : notOperator), "_", "notExpression"], "postprocess": d => node.not.create({right: d[2]})},
|
||||||
{"name": "notExpression", "symbols": ["callExpression"], "postprocess": id},
|
{"name": "notExpression", "symbols": ["callExpression"], "postprocess": id},
|
||||||
{"name": "callExpression", "symbols": ["name", "_", "arguments"], "postprocess":
|
{"name": "callExpression", "symbols": ["name", "_", "arguments"], "postprocess":
|
||||||
d => node.call.create({functionName: d[0].name, args: d[2]})
|
d => node.call.create({functionName: d[0].name, args: d[2]})
|
||||||
@@ -109,14 +109,16 @@ let ParserRules = [
|
|||||||
{"name": "parenthesizedExpression", "symbols": [{"literal":"("}, "_", "expression", "_", {"literal":")"}], "postprocess": d => node.parenthesis.create({content: d[2]})},
|
{"name": "parenthesizedExpression", "symbols": [{"literal":"("}, "_", "expression", "_", {"literal":")"}], "postprocess": d => node.parenthesis.create({content: d[2]})},
|
||||||
{"name": "parenthesizedExpression", "symbols": ["accessorExpression"], "postprocess": id},
|
{"name": "parenthesizedExpression", "symbols": ["accessorExpression"], "postprocess": id},
|
||||||
{"name": "accessorExpression$subexpression$1", "symbols": [(lexer.has("name") ? {type: "name"} : name)], "postprocess": d => d[0].value},
|
{"name": "accessorExpression$subexpression$1", "symbols": [(lexer.has("name") ? {type: "name"} : name)], "postprocess": d => d[0].value},
|
||||||
{"name": "accessorExpression$ebnf$1$subexpression$1", "symbols": [{"literal":"."}, (lexer.has("name") ? {type: "name"} : name)], "postprocess": d => d[1].value},
|
{"name": "accessorExpression$ebnf$1$subexpression$1", "symbols": [{"literal":"."}, "keyExpression"], "postprocess": d => d[1]},
|
||||||
{"name": "accessorExpression$ebnf$1", "symbols": ["accessorExpression$ebnf$1$subexpression$1"]},
|
{"name": "accessorExpression$ebnf$1", "symbols": ["accessorExpression$ebnf$1$subexpression$1"]},
|
||||||
{"name": "accessorExpression$ebnf$1$subexpression$2", "symbols": [{"literal":"."}, (lexer.has("name") ? {type: "name"} : name)], "postprocess": d => d[1].value},
|
{"name": "accessorExpression$ebnf$1$subexpression$2", "symbols": [{"literal":"."}, "keyExpression"], "postprocess": d => d[1]},
|
||||||
{"name": "accessorExpression$ebnf$1", "symbols": ["accessorExpression$ebnf$1", "accessorExpression$ebnf$1$subexpression$2"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
|
{"name": "accessorExpression$ebnf$1", "symbols": ["accessorExpression$ebnf$1", "accessorExpression$ebnf$1$subexpression$2"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
|
||||||
{"name": "accessorExpression", "symbols": ["accessorExpression$subexpression$1", "accessorExpression$ebnf$1"], "postprocess":
|
{"name": "accessorExpression", "symbols": ["accessorExpression$subexpression$1", "accessorExpression$ebnf$1"], "postprocess":
|
||||||
d=> node.accessor.create({name: d[0], path: d[1]})
|
d=> node.accessor.create({name: d[0], path: d[1]})
|
||||||
},
|
},
|
||||||
{"name": "accessorExpression", "symbols": ["valueExpression"], "postprocess": id},
|
{"name": "accessorExpression", "symbols": ["valueExpression"], "postprocess": id},
|
||||||
|
{"name": "keyExpression", "symbols": ["name"], "postprocess": d => d[0].name},
|
||||||
|
{"name": "keyExpression", "symbols": ["number"], "postprocess": d => d[0].value},
|
||||||
{"name": "valueExpression", "symbols": ["name"], "postprocess": id},
|
{"name": "valueExpression", "symbols": ["name"], "postprocess": id},
|
||||||
{"name": "valueExpression", "symbols": ["number"], "postprocess": id},
|
{"name": "valueExpression", "symbols": ["number"], "postprocess": id},
|
||||||
{"name": "valueExpression", "symbols": ["string"], "postprocess": id},
|
{"name": "valueExpression", "symbols": ["string"], "postprocess": id},
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
value: s => s.slice(1, -1),
|
value: s => s.slice(1, -1),
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
match: /[a-zA-Z_#]*[a-ce-zA-Z_#][a-zA-Z0-9_#]*/,
|
match: /[a-zA-Z_#$]*[a-ce-zA-Z_#$][a-zA-Z0-9_#$]*/,
|
||||||
type: moo.keywords({
|
type: moo.keywords({
|
||||||
'keywords': ['true', 'false'],
|
'keywords': ['true', 'false'],
|
||||||
}),
|
}),
|
||||||
@@ -109,7 +109,7 @@ unaryExpression ->
|
|||||||
| notExpression {% id %}
|
| notExpression {% id %}
|
||||||
|
|
||||||
notExpression ->
|
notExpression ->
|
||||||
%notOperator _ notExpression {% d => node.notOperator.create({right: d[2]})%}
|
%notOperator _ notExpression {% d => node.not.create({right: d[2]})%}
|
||||||
| callExpression {% id %}
|
| callExpression {% id %}
|
||||||
|
|
||||||
callExpression ->
|
callExpression ->
|
||||||
@@ -138,11 +138,14 @@ parenthesizedExpression ->
|
|||||||
| accessorExpression {% id %}
|
| accessorExpression {% id %}
|
||||||
|
|
||||||
accessorExpression ->
|
accessorExpression ->
|
||||||
(%name {% d => d[0].value %}) ( "." %name {% d => d[1].value %} ):+ {%
|
(%name {% d => d[0].value %}) ( "." keyExpression {% d => d[1] %} ):+ {%
|
||||||
d=> node.accessor.create({name: d[0], path: d[1]})
|
d=> node.accessor.create({name: d[0], path: d[1]})
|
||||||
%}
|
%}
|
||||||
| valueExpression {% id %}
|
| valueExpression {% id %}
|
||||||
|
|
||||||
|
keyExpression -> name {% d => d[0].name %}
|
||||||
|
| number {% d => d[0].value %}
|
||||||
|
|
||||||
valueExpression ->
|
valueExpression ->
|
||||||
name {% id %}
|
name {% id %}
|
||||||
| number {% id %}
|
| number {% id %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import constant from './constant.js';
|
import constant from './constant.js';
|
||||||
|
import { toString } from '../resolve.js';
|
||||||
|
|
||||||
const accessor = {
|
const accessor = {
|
||||||
create({name, path}) {
|
create({name, path}) {
|
||||||
@@ -53,7 +54,7 @@ const accessor = {
|
|||||||
reduce(node, scope, context){
|
reduce(node, scope, context){
|
||||||
let { result } = accessor.compile(node, scope, context);
|
let { result } = accessor.compile(node, scope, context);
|
||||||
if (result.parseType === 'accessor'){
|
if (result.parseType === 'accessor'){
|
||||||
context.error(`${accessor.toString(result)} not found, set to 0`);
|
context.error(`${toString(result)} not found, set to 0`);
|
||||||
return {
|
return {
|
||||||
result: constant.create({
|
result: constant.create({
|
||||||
value: 0,
|
value: 0,
|
||||||
|
|||||||
@@ -61,8 +61,11 @@ const call = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Map contant nodes to constants before attempting to run the function
|
// Map contant nodes to constants before attempting to run the function
|
||||||
let mappedArgs = resolvedArgs.map(arg => {
|
let mappedArgs = resolvedArgs.map((arg, index) => {
|
||||||
if (arg.parseType === 'constant'){
|
if (
|
||||||
|
arg.parseType === 'constant' &&
|
||||||
|
func.arguments[index] !== 'parseNode'
|
||||||
|
){
|
||||||
return arg.value;
|
return arg.value;
|
||||||
} else {
|
} else {
|
||||||
return arg;
|
return arg;
|
||||||
@@ -71,7 +74,7 @@ const call = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Run the function
|
// Run the function
|
||||||
let value = func.fn.apply(null, mappedArgs);
|
let value = func.fn.apply({scope, context}, mappedArgs);
|
||||||
|
|
||||||
let valueType = typeof value;
|
let valueType = typeof value;
|
||||||
if (valueType === 'number' || valueType === 'string' || valueType === 'boolean'){
|
if (valueType === 'number' || valueType === 'string' || valueType === 'boolean'){
|
||||||
@@ -132,6 +135,7 @@ const call = {
|
|||||||
} else {
|
} else {
|
||||||
type = argumentsExpected[index];
|
type = argumentsExpected[index];
|
||||||
}
|
}
|
||||||
|
if (type === 'parseNode') return;
|
||||||
if (node.parseType !== type && node.valueType !== type) failed = true;
|
if (node.parseType !== type && node.valueType !== type) failed = true;
|
||||||
if (failed && fn === 'reduce'){
|
if (failed && fn === 'reduce'){
|
||||||
let typeName = typeof type === 'string' ? type : type.constructor.name;
|
let typeName = typeof type === 'string' ? type : type.constructor.name;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const indexNode = {
|
|||||||
}
|
}
|
||||||
} else if (fn === 'reduce'){
|
} else if (fn === 'reduce'){
|
||||||
if (array.parseType !== 'array'){
|
if (array.parseType !== 'array'){
|
||||||
const message = `Can not get the index of a non-array node: ${node.array.toString()} = ${array.toString()}`
|
const message = `Can not get the index of a non-array node: ${toString(node.array)} = ${toString(array)}`
|
||||||
context.error(message);
|
context.error(message);
|
||||||
return {
|
return {
|
||||||
result: error.create({
|
result: error.create({
|
||||||
@@ -41,7 +41,7 @@ const indexNode = {
|
|||||||
context,
|
context,
|
||||||
};
|
};
|
||||||
} else if (!index.isInteger){
|
} else if (!index.isInteger){
|
||||||
const message = `${array.toString()} is not an integer index of the array`
|
const message = `${toString(array)} is not an integer index of the array`
|
||||||
context.error(message);
|
context.error(message);
|
||||||
return {
|
return {
|
||||||
result: error.create({
|
result: error.create({
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const rollArray = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
toString(node){
|
toString(node){
|
||||||
return `${node.diceNum || ''}d${node.diceSize} [${node.values.join(', ')}]`;
|
return `${node.diceNum || ''}d${node.diceSize} [ ${node.values.join(', ')} ]`;
|
||||||
},
|
},
|
||||||
reduce(node, scope, context){
|
reduce(node, scope, context){
|
||||||
const total = node.values.reduce((a, b) => a + b, 0);
|
const total = node.values.reduce((a, b) => a + b, 0);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import resolve from '../resolve.js';
|
import resolve, { toString } from '../resolve.js';
|
||||||
import constant from './constant.js';
|
import constant from './constant.js';
|
||||||
|
|
||||||
const symbol = {
|
const symbol = {
|
||||||
@@ -46,7 +46,7 @@ const symbol = {
|
|||||||
if (result.parseType === 'symbol'){
|
if (result.parseType === 'symbol'){
|
||||||
context.error({
|
context.error({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: `${result.toString()} not found, set to 0`
|
message: `${toString(result)} not found, set to 0`
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
result: constant.create({value: 0}),
|
result: constant.create({value: 0}),
|
||||||
|
|||||||
43
app/imports/ui/components/CardHighlight.vue
Normal file
43
app/imports/ui/components/CardHighlight.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<template lang="html">
|
||||||
|
<div
|
||||||
|
v-if="dark || theme.isDark"
|
||||||
|
class="overlay"
|
||||||
|
:class="{active, 'extra-bright': dark && !theme.isDark}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="js">
|
||||||
|
export default {
|
||||||
|
inject: {
|
||||||
|
theme: {
|
||||||
|
default: {
|
||||||
|
isDark: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
active: Boolean,
|
||||||
|
dark: Boolean,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.overlay {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background-color: #fff;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.1s linear;
|
||||||
|
}
|
||||||
|
.overlay.active {
|
||||||
|
opacity: 0.08;
|
||||||
|
}
|
||||||
|
.overlay.active.extra-bright {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,10 +7,15 @@
|
|||||||
>
|
>
|
||||||
<template #activator="{ on }">
|
<template #activator="{ on }">
|
||||||
<v-btn
|
<v-btn
|
||||||
icon
|
:outlined="!!label"
|
||||||
|
:icon="!label"
|
||||||
|
:min-width="label && 108"
|
||||||
v-on="on"
|
v-on="on"
|
||||||
>
|
>
|
||||||
<v-icon>mdi-format-paint</v-icon>
|
{{ label }}
|
||||||
|
<v-icon :right="!!label">
|
||||||
|
mdi-format-paint
|
||||||
|
</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-card class="overflow-hidden">
|
<v-card class="overflow-hidden">
|
||||||
@@ -122,6 +127,10 @@
|
|||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data(){ return {
|
data(){ return {
|
||||||
colors: [
|
colors: [
|
||||||
|
|||||||
46
app/imports/ui/components/HorizontalHex.vue
Normal file
46
app/imports/ui/components/HorizontalHex.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<template lang="html">
|
||||||
|
<div
|
||||||
|
class="d-flex justify-center align-center"
|
||||||
|
style="height: 120px; width: 120px;"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
style="height: 120px; width: 120px; position: absolute;"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M 251.313 23.844 L 49.438 140.25 L 49.062 373.75 L 251.687 490.156 L 453.563 373.75 L 453.938 140.25 L 251.313 23.844 Z"
|
||||||
|
fill-opacity="1"
|
||||||
|
:fill="hover ? 'rgb(255, 255, 255)' : 'rgb(255, 255, 255)'"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M 249.801 51.001 L 71.808 153.637 L 71.477 359.513 L 250.131 462.148 L 428.125 359.513 L 428.455 153.637 L 249.801 51.001 Z"
|
||||||
|
fill-opacity="1"
|
||||||
|
:fill="hover ? 'rgb(40, 40, 40)' : 'rgb(80, 80, 80)'"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
v-ripple
|
||||||
|
style="height: 100px; width: 100px; border-radius: 50%; z-index: 1; cursor: pointer;"
|
||||||
|
class="d-flex justify-center align-center"
|
||||||
|
@mouseover="hover = true"
|
||||||
|
@mouseleave="hover = false"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="js">
|
||||||
|
export default {
|
||||||
|
data(){return {
|
||||||
|
hover: false,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
path {
|
||||||
|
transition: fill 0.3s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -10,11 +10,12 @@
|
|||||||
<template #activator="{ on }">
|
<template #activator="{ on }">
|
||||||
<v-btn
|
<v-btn
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
|
:loading="loading"
|
||||||
v-on="on"
|
v-on="on"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<slot>
|
<slot>
|
||||||
<v-icon>mdi-plus</v-icon>
|
<v-icon>$vuetify.icons.abacus</v-icon>
|
||||||
</slot>
|
</slot>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
@@ -42,6 +43,7 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
loading: Boolean,
|
||||||
},
|
},
|
||||||
data(){return {
|
data(){return {
|
||||||
open: false
|
open: false
|
||||||
|
|||||||
118
app/imports/ui/components/RollPopup.vue
Normal file
118
app/imports/ui/components/RollPopup.vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<template lang="html">
|
||||||
|
<div>
|
||||||
|
<v-menu
|
||||||
|
v-model="open"
|
||||||
|
origin="center center"
|
||||||
|
transition="scale-transition"
|
||||||
|
nudge-left="100px"
|
||||||
|
nudge-top="100px"
|
||||||
|
:close-on-content-click="false"
|
||||||
|
>
|
||||||
|
<template #activator="{ on }">
|
||||||
|
<v-btn
|
||||||
|
v-bind="$attrs"
|
||||||
|
:class="buttonClass"
|
||||||
|
v-on="on"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-sheet class="d-flex flex-column align-center justify-center">
|
||||||
|
<v-btn-toggle v-model="dataAdvantage">
|
||||||
|
<v-btn :value="-1">
|
||||||
|
Disadvantage
|
||||||
|
</v-btn>
|
||||||
|
<v-btn :value="1">
|
||||||
|
Advantage
|
||||||
|
</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
<div class="ma-1 text-subtitle-2">
|
||||||
|
{{ name }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<v-scale-transition
|
||||||
|
origin="center center"
|
||||||
|
>
|
||||||
|
<vertical-hex
|
||||||
|
v-if="dataAdvantage"
|
||||||
|
style="position:absolute; transition: margin-left 0.3s ease;"
|
||||||
|
:style="{marginLeft: dataAdvantage == 1 ? '24px' : '-24px'}"
|
||||||
|
disable-hover
|
||||||
|
/>
|
||||||
|
</v-scale-transition>
|
||||||
|
<vertical-hex @click="roll">
|
||||||
|
<div>
|
||||||
|
Roll
|
||||||
|
</div>
|
||||||
|
<div v-if="rollText">
|
||||||
|
{{ rollText }}
|
||||||
|
</div>
|
||||||
|
</vertical-hex>
|
||||||
|
</div>
|
||||||
|
<v-btn
|
||||||
|
text
|
||||||
|
color="primary"
|
||||||
|
style="align-self: end"
|
||||||
|
@click="close"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
</v-sheet>
|
||||||
|
</v-menu>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="js">
|
||||||
|
import VerticalHex from '/imports/ui/components/VerticalHex.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
VerticalHex
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
rollText: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
buttonClass: {
|
||||||
|
type: String,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
advantage: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data(){return {
|
||||||
|
open: false,
|
||||||
|
dataAdvantage: this.advantage,
|
||||||
|
}},
|
||||||
|
watch: {
|
||||||
|
advantage(val){
|
||||||
|
this.dataAdvantage = val;
|
||||||
|
},
|
||||||
|
open(val){
|
||||||
|
if(!val){
|
||||||
|
this.dataAdvantage = this.advantage;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
roll(){
|
||||||
|
this.$emit('roll', {advantage: this.dataAdvantage});
|
||||||
|
this.open = false;
|
||||||
|
},
|
||||||
|
close(){
|
||||||
|
this.open = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
</style>
|
||||||
@@ -2,15 +2,17 @@
|
|||||||
<v-card
|
<v-card
|
||||||
:hover="hasClickListener"
|
:hover="hasClickListener"
|
||||||
class="toolbar-card"
|
class="toolbar-card"
|
||||||
:class="hovering ? 'elevation-8': ''"
|
:class="{'transparent-toolbar': transparentToolbar, hovering}"
|
||||||
|
:elevation="hovering ? 8 : undefined"
|
||||||
@click.native="$emit('click')"
|
@click.native="$emit('click')"
|
||||||
>
|
>
|
||||||
<v-toolbar
|
<v-toolbar
|
||||||
flat
|
flat
|
||||||
:style="`transform: none; ${hasToolbarClickListener ? 'cursor: pointer;' : ''}`"
|
:style="`transform: none; ${hasToolbarClickListener ? 'cursor: pointer;' : ''}`"
|
||||||
:color="color"
|
:class="{}"
|
||||||
:dark="isDark"
|
:color="transparentToolbar ? undefined : color"
|
||||||
:light="!isDark"
|
:dark="transparentToolbar ? undefined : isDark"
|
||||||
|
:light="transparentToolbar ? undefined : !isDark"
|
||||||
@click="$emit('toolbarclick')"
|
@click="$emit('toolbarclick')"
|
||||||
@mouseover="hoverToolbar(true)"
|
@mouseover="hoverToolbar(true)"
|
||||||
@mouseleave="hoverToolbar(false)"
|
@mouseleave="hoverToolbar(false)"
|
||||||
@@ -20,14 +22,19 @@
|
|||||||
<div>
|
<div>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
<card-highlight :active="hovering" />
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
|
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
|
||||||
import getThemeColor from '/imports/ui/utility/getThemeColor.js';
|
import getThemeColor from '/imports/ui/utility/getThemeColor.js';
|
||||||
|
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
CardHighlight,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
color: {
|
color: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -35,6 +42,7 @@
|
|||||||
return getThemeColor('secondary');
|
return getThemeColor('secondary');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
transparentToolbar: Boolean,
|
||||||
},
|
},
|
||||||
data(){ return {
|
data(){ return {
|
||||||
hovering: false,
|
hovering: false,
|
||||||
@@ -62,9 +70,12 @@
|
|||||||
|
|
||||||
<style lang="css">
|
<style lang="css">
|
||||||
.toolbar-card .v-toolbar__title {
|
.toolbar-card .v-toolbar__title {
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
.toolbar-card {
|
.toolbar-card {
|
||||||
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
}
|
}
|
||||||
|
.toolbar-card.transparent-toolbar .theme--dark.v-toolbar.v-sheet {
|
||||||
|
background-color: #303030;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
68
app/imports/ui/components/VerticalHex.vue
Normal file
68
app/imports/ui/components/VerticalHex.vue
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<template lang="html">
|
||||||
|
<div
|
||||||
|
class="d-flex justify-center align-center"
|
||||||
|
:style="`height: ${height}px; width: ${width}px;`"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
style="height: 120px; width: 120px; position: absolute;"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M 251.313 23.844 L 49.438 140.25 L 49.062 373.75 L 251.687 490.156 L 453.563 373.75 L 453.938 140.25 L 251.313 23.844 Z"
|
||||||
|
fill-opacity="1"
|
||||||
|
:fill="hover ? '#f44336 ' : '#f44336 '"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M 249.801 51.001 L 71.808 153.637 L 71.477 359.513 L 250.131 462.148 L 428.125 359.513 L 428.455 153.637 L 249.801 51.001 Z"
|
||||||
|
fill-opacity="1"
|
||||||
|
:fill="theme.isDark ?
|
||||||
|
hover ? 'rgb(80, 80, 80)' : 'rgb(40, 40, 40)':
|
||||||
|
hover ? 'rgb(180, 180, 180)' : 'rgb(220, 220, 220)'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
v-ripple
|
||||||
|
style="height: 100px; width: 100px; border-radius: 50%; z-index: 1; cursor: pointer;"
|
||||||
|
class="d-flex flex-column justify-center align-center"
|
||||||
|
@mouseover="hover = !disableHover"
|
||||||
|
@mouseleave="hover = false"
|
||||||
|
@click="e => $emit('click', e)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="js">
|
||||||
|
export default {
|
||||||
|
inject: {
|
||||||
|
theme: {
|
||||||
|
default: {
|
||||||
|
isDark: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 120,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 120,
|
||||||
|
},
|
||||||
|
disableHover: Boolean,
|
||||||
|
},
|
||||||
|
data(){return {
|
||||||
|
hover: false,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
path {
|
||||||
|
transition: fill 0.3s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,21 +8,22 @@
|
|||||||
>
|
>
|
||||||
<template #activator="{ on }">
|
<template #activator="{ on }">
|
||||||
<div class="layout align-center">
|
<div class="layout align-center">
|
||||||
<v-label>{{ label }}</v-label>
|
|
||||||
<v-btn
|
<v-btn
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
large
|
outlined
|
||||||
icon
|
:min-width="108"
|
||||||
v-on="on"
|
v-on="on"
|
||||||
>
|
>
|
||||||
|
{{ label }}
|
||||||
<svg-icon
|
<svg-icon
|
||||||
v-if="safeValue && safeValue.shape"
|
v-if="safeValue && safeValue.shape"
|
||||||
large
|
right
|
||||||
|
class="ml-2"
|
||||||
:shape="safeValue.shape"
|
:shape="safeValue.shape"
|
||||||
/>
|
/>
|
||||||
<v-icon
|
<v-icon
|
||||||
v-else
|
v-else
|
||||||
large
|
right
|
||||||
>
|
>
|
||||||
mdi-select-search
|
mdi-select-search
|
||||||
</v-icon>
|
</v-icon>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
<v-card
|
<v-card
|
||||||
hover
|
hover
|
||||||
data-id="creature-summary"
|
data-id="creature-summary"
|
||||||
|
@mouseover="summaryHover = true"
|
||||||
|
@mouseleave="summaryHover = false"
|
||||||
@click="showCharacterForm"
|
@click="showCharacterForm"
|
||||||
>
|
>
|
||||||
<v-img
|
<v-img
|
||||||
@@ -18,6 +20,7 @@
|
|||||||
{{ creature.alignment }}<br>
|
{{ creature.alignment }}<br>
|
||||||
{{ creature.gender }}
|
{{ creature.gender }}
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
<card-highlight :active="summaryHover" />
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -25,9 +28,21 @@
|
|||||||
data-id="slot-card"
|
data-id="slot-card"
|
||||||
@toolbarclick="showSlotDialog"
|
@toolbarclick="showSlotDialog"
|
||||||
>
|
>
|
||||||
<v-toolbar-title slot="toolbar">
|
<template slot="toolbar">
|
||||||
Build
|
<v-toolbar-title>
|
||||||
</v-toolbar-title>
|
Build
|
||||||
|
</v-toolbar-title>
|
||||||
|
<v-spacer />
|
||||||
|
<v-toolbar-title>
|
||||||
|
<v-icon
|
||||||
|
small
|
||||||
|
style="width: 16px;"
|
||||||
|
class="mr-1"
|
||||||
|
>
|
||||||
|
mdi-pencil
|
||||||
|
</v-icon>
|
||||||
|
</v-toolbar-title>
|
||||||
|
</template>
|
||||||
<v-card-text style="background-color: inherit;">
|
<v-card-text style="background-color: inherit;">
|
||||||
<slots :creature-id="creatureId" />
|
<slots :creature-id="creatureId" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
@@ -121,6 +136,7 @@ import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
|
|||||||
import NoteCard from '/imports/ui/properties/components/persona/NoteCard.vue';
|
import NoteCard from '/imports/ui/properties/components/persona/NoteCard.vue';
|
||||||
import Slots from '/imports/ui/creature/slots/Slots.vue';
|
import Slots from '/imports/ui/creature/slots/Slots.vue';
|
||||||
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
|
import ToolbarCard from '/imports/ui/components/ToolbarCard.vue';
|
||||||
|
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -128,6 +144,7 @@ export default {
|
|||||||
NoteCard,
|
NoteCard,
|
||||||
Slots,
|
Slots,
|
||||||
ToolbarCard,
|
ToolbarCard,
|
||||||
|
CardHighlight,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
creatureId: {
|
creatureId: {
|
||||||
@@ -135,6 +152,9 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data(){return {
|
||||||
|
summaryHover: false,
|
||||||
|
}},
|
||||||
computed: {
|
computed: {
|
||||||
highestClassLevels(){
|
highestClassLevels(){
|
||||||
let highestLevels = {};
|
let highestLevels = {};
|
||||||
|
|||||||
@@ -31,12 +31,12 @@
|
|||||||
<v-list-item-action>
|
<v-list-item-action>
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
<coin-value
|
<coin-value
|
||||||
:value="creature.denormalizedStats.valueTotal || 0"
|
:value="creature.variables && creature.variables.valueTotal && creature.variables.valueTotal.value|| 0"
|
||||||
/>
|
/>
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
</v-list-item-action>
|
</v-list-item-action>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-list-item v-if="creature.denormalizedStats.itemsAttuned">
|
<v-list-item v-if="creature.variables && creature.variables.itemsAttuned && creature.variables.itemsAttuned.value">
|
||||||
<v-list-item-avatar>
|
<v-list-item-avatar>
|
||||||
<v-icon>$vuetify.icons.spell</v-icon>
|
<v-icon>$vuetify.icons.spell</v-icon>
|
||||||
</v-list-item-avatar>
|
</v-list-item-avatar>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
<v-list-item-action>
|
<v-list-item-action>
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
{{ creature.denormalizedStats.itemsAttuned }}
|
{{ creature.variables.itemsAttuned.value }}
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
</v-list-item-action>
|
</v-list-item-action>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
@@ -55,9 +55,7 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<toolbar-card
|
<toolbar-card transparent-toolbar>
|
||||||
:color="creature.color"
|
|
||||||
>
|
|
||||||
<v-toolbar-title slot="toolbar">
|
<v-toolbar-title slot="toolbar">
|
||||||
Equipped
|
Equipped
|
||||||
</v-toolbar-title>
|
</v-toolbar-title>
|
||||||
@@ -71,9 +69,7 @@
|
|||||||
</toolbar-card>
|
</toolbar-card>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<toolbar-card
|
<toolbar-card transparent-toolbar>
|
||||||
:color="creature.color"
|
|
||||||
>
|
|
||||||
<v-toolbar-title slot="toolbar">
|
<v-toolbar-title slot="toolbar">
|
||||||
Carried
|
Carried
|
||||||
</v-toolbar-title>
|
</v-toolbar-title>
|
||||||
@@ -140,7 +136,7 @@ export default {
|
|||||||
creature(){
|
creature(){
|
||||||
return Creatures.findOne(this.creatureId, {fields: {
|
return Creatures.findOne(this.creatureId, {fields: {
|
||||||
color: 1,
|
color: 1,
|
||||||
denormalizedStats: 1,
|
variables: 1,
|
||||||
}});
|
}});
|
||||||
},
|
},
|
||||||
containersWithoutAncestorContainers(){
|
containersWithoutAncestorContainers(){
|
||||||
@@ -210,7 +206,9 @@ export default {
|
|||||||
},
|
},
|
||||||
weightCarried(){
|
weightCarried(){
|
||||||
return stripFloatingPointOddities(
|
return stripFloatingPointOddities(
|
||||||
this.creature.denormalizedStats.weightCarried || 0
|
this.creature.variables &&
|
||||||
|
this.creature.variables.weightCarried &&
|
||||||
|
this.creature.variables.weightCarried.value || 0
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export default {
|
|||||||
type: 'spell',
|
type: 'spell',
|
||||||
removed: {$ne: true},
|
removed: {$ne: true},
|
||||||
deactivatedByAncestor: {$ne: true},
|
deactivatedByAncestor: {$ne: true},
|
||||||
|
deactivatedByToggle: {$ne: true},
|
||||||
}, {
|
}, {
|
||||||
sort: {
|
sort: {
|
||||||
level: 1,
|
level: 1,
|
||||||
|
|||||||
@@ -162,11 +162,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="spellSlots && spellSlots.length"
|
v-if="spellSlots && spellSlots.length || hasSpells"
|
||||||
class="spell-slots"
|
class="spell-slots"
|
||||||
>
|
>
|
||||||
<v-card>
|
<v-card
|
||||||
|
data-id="spell-slot-card"
|
||||||
|
>
|
||||||
<v-list
|
<v-list
|
||||||
|
v-if="spellSlots && spellSlots.length"
|
||||||
two-line
|
two-line
|
||||||
subheader
|
subheader
|
||||||
>
|
>
|
||||||
@@ -180,6 +183,19 @@
|
|||||||
@cast="castSpellWithSlot(spellSlot._id)"
|
@cast="castSpellWithSlot(spellSlot._id)"
|
||||||
/>
|
/>
|
||||||
</v-list>
|
</v-list>
|
||||||
|
<div
|
||||||
|
v-if="hasSpells"
|
||||||
|
class="d-flex justify-end"
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
color="accent"
|
||||||
|
style="width: 100%;"
|
||||||
|
outlined
|
||||||
|
@click="castSpell"
|
||||||
|
>
|
||||||
|
Cast a spell
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
</v-card>
|
</v-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -226,7 +242,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="action in actions"
|
v-for="action in actions"
|
||||||
:key="action._id"
|
:key="action._id"
|
||||||
class="actions"
|
class="action"
|
||||||
>
|
>
|
||||||
<action-card
|
<action-card
|
||||||
:model="action"
|
:model="action"
|
||||||
@@ -237,7 +253,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="attack in attacks"
|
v-for="attack in attacks"
|
||||||
:key="attack._id"
|
:key="attack._id"
|
||||||
class="attacks"
|
class="attack"
|
||||||
>
|
>
|
||||||
<action-card
|
<action-card
|
||||||
attack
|
attack
|
||||||
@@ -348,7 +364,8 @@
|
|||||||
import RestButton from '/imports/ui/creature/RestButton.vue';
|
import RestButton from '/imports/ui/creature/RestButton.vue';
|
||||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||||
import ToggleCard from '/imports/ui/properties/components/toggles/ToggleCard.vue';
|
import ToggleCard from '/imports/ui/properties/components/toggles/ToggleCard.vue';
|
||||||
//import castSpellWithSlot from '/imports/api/creature/actions/castSpellWithSlot.js';
|
import doCastSpell from '/imports/api/engine/actions/doCastSpell.js';
|
||||||
|
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||||
|
|
||||||
const getProperties = function(creature, filter){
|
const getProperties = function(creature, filter){
|
||||||
if (!creature) return;
|
if (!creature) return;
|
||||||
@@ -399,6 +416,9 @@
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data(){return {
|
||||||
|
doCheckLoading: false,
|
||||||
|
}},
|
||||||
meteor: {
|
meteor: {
|
||||||
creature(){
|
creature(){
|
||||||
return Creatures.findOne(this.creatureId);
|
return Creatures.findOne(this.creatureId);
|
||||||
@@ -429,6 +449,11 @@
|
|||||||
spellSlots(){
|
spellSlots(){
|
||||||
return getAttributeOfType(this.creature, 'spellSlot');
|
return getAttributeOfType(this.creature, 'spellSlot');
|
||||||
},
|
},
|
||||||
|
hasSpells(){
|
||||||
|
return getProperties(this.creature, {
|
||||||
|
type: 'spell',
|
||||||
|
}).count();
|
||||||
|
},
|
||||||
hitDice(){
|
hitDice(){
|
||||||
return getAttributeOfType(this.creature, 'hitDice');
|
return getAttributeOfType(this.creature, 'hitDice');
|
||||||
},
|
},
|
||||||
@@ -457,7 +482,7 @@
|
|||||||
return getProperties(this.creature, {type: 'action'});
|
return getProperties(this.creature, {type: 'action'});
|
||||||
},
|
},
|
||||||
appliedBuffs(){
|
appliedBuffs(){
|
||||||
return getProperties(this.creature, {type: 'buff', applied: true});
|
return getProperties(this.creature, {type: 'buff'});
|
||||||
},
|
},
|
||||||
attacks(){
|
attacks(){
|
||||||
let props = getProperties(this.creature, {type: 'attack'})
|
let props = getProperties(this.creature, {type: 'attack'})
|
||||||
@@ -495,18 +520,19 @@
|
|||||||
if (error) console.error(error);
|
if (error) console.error(error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
castSpellWithSlot(slotId){
|
castSpell(){
|
||||||
this.$store.commit('pushDialogStack', {
|
this.$store.commit('pushDialogStack', {
|
||||||
component: 'cast-spell-with-slot-dialog',
|
component: 'cast-spell-with-slot-dialog',
|
||||||
elementId: `spell-slot-cast-btn-${slotId}`,
|
elementId: 'spell-slot-card',
|
||||||
data: {
|
data: {
|
||||||
creatureId: this.creatureId,
|
creatureId: this.creatureId,
|
||||||
slotId,
|
|
||||||
},
|
},
|
||||||
callback({spellId, slotId} = {}){
|
callback({spellId, slotId} = {}){
|
||||||
if (!spellId) return;
|
if (!spellId) return;
|
||||||
castSpellWithSlot.call({spellId, slotId}, error => {
|
doCastSpell.call({spellId, slotId}, error => {
|
||||||
if (error) console.error(error);
|
if (!error) return;
|
||||||
|
snackbar({text: error.reason});
|
||||||
|
console.error(error);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -188,6 +188,8 @@ import LibraryNodes from '/imports/api/library/LibraryNodes.js';
|
|||||||
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
import DialogBase from '/imports/ui/dialogStack/DialogBase.vue';
|
||||||
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue';
|
||||||
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue'
|
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue'
|
||||||
|
import resolve, { toString } from '/imports/parser/resolve.js';
|
||||||
|
import { prettifyParseError, parse } from '/imports/parser/parser.js';
|
||||||
// import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
|
// import evaluateString from '/imports/api/creature/computation/afterComputation/evaluateString.js';
|
||||||
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'
|
import getSlotFillFilter from '/imports/api/creature/creatureProperties/methods/getSlotFillFilter.js'
|
||||||
import Libraries from '/imports/api/library/Libraries.js';
|
import Libraries from '/imports/api/library/Libraries.js';
|
||||||
@@ -291,8 +293,9 @@ export default {
|
|||||||
return CreatureProperties.findOne(this.slotId);
|
return CreatureProperties.findOne(this.slotId);
|
||||||
} else if (this.dummySlot) {
|
} else if (this.dummySlot) {
|
||||||
let model = clone(this.dummySlot)
|
let model = clone(this.dummySlot)
|
||||||
model.quantityExpectedResult = +model.quantityExpected;
|
if (!model.quantityExpected) model.quantityExpected = {};
|
||||||
model.spaceLeft = model.quantityExpectedResult;
|
model.quantityExpected.value = +model.quantityExpected.calculation;
|
||||||
|
model.spaceLeft = model.quantityExpected.value;
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -342,7 +345,7 @@ export default {
|
|||||||
return quantitySelected;
|
return quantitySelected;
|
||||||
},
|
},
|
||||||
spaceLeft(){
|
spaceLeft(){
|
||||||
if (this.model.quantityExpectedResult === 0) return undefined;
|
if (!this.model.quantityExpected || this.model.quantityExpected.value === 0) return undefined;
|
||||||
return this.model.spaceLeft - this.totalQuantitySelected;
|
return this.model.spaceLeft - this.totalQuantitySelected;
|
||||||
},
|
},
|
||||||
libraryNames(){
|
libraryNames(){
|
||||||
@@ -360,13 +363,24 @@ export default {
|
|||||||
// the quantity to fill
|
// the quantity to fill
|
||||||
nodes.forEach(node => {
|
nodes.forEach(node => {
|
||||||
if (node.slotFillerCondition){
|
if (node.slotFillerCondition){
|
||||||
let {result} = evaluateString({
|
try {
|
||||||
string: node.slotFillerCondition,
|
let parseNode = parse(node.slotFillerCondition);
|
||||||
scope: this.creature.variables,
|
const {result: resultNode} = resolve('reduce', parseNode, this.creature.variables);
|
||||||
fn: 'reduce',
|
if (resultNode?.parseType === 'constant'){
|
||||||
});
|
if (!resultNode.value){
|
||||||
if (!result.value){
|
node._disabledBySlotFillerCondition = true;
|
||||||
|
disabledNodeCount += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
node._disabledBySlotFillerCondition = true;
|
||||||
|
node._conditionError = toString(resultNode);
|
||||||
|
disabledNodeCount += 1;
|
||||||
|
}
|
||||||
|
} catch (e){
|
||||||
|
console.warn(e);
|
||||||
|
let error = prettifyParseError(e);
|
||||||
node._disabledBySlotFillerCondition = true;
|
node._disabledBySlotFillerCondition = true;
|
||||||
|
node._conditionError = error;
|
||||||
disabledNodeCount += 1;
|
disabledNodeCount += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
<h3 class="layout align-center">
|
<h3 class="layout align-center">
|
||||||
{{ slot.name }}
|
{{ slot.name }}
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
<span v-if="slot.quantityExpectedResult > 1">
|
<span v-if="slot.quantityExpected && slot.quantityExpected.value > 1">
|
||||||
{{ slot.totalFilled }} / {{ slot.quantityExpectedResult }}
|
{{ slot.totalFilled }} / {{ slot.quantityExpected.value }}
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
<v-list v-if="slot.children.length">
|
<v-list v-if="slot.children.length">
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="!slot.quantityExpectedResult || slot.spaceLeft"
|
v-if="!slot.quantityExpected || !slot.quantityExpected.value || slot.spaceLeft"
|
||||||
icon
|
icon
|
||||||
:data-id="`slot-add-button-${slot._id}`"
|
:data-id="`slot-add-button-${slot._id}`"
|
||||||
class="slot-add-button"
|
class="slot-add-button"
|
||||||
@@ -120,8 +120,8 @@ export default {
|
|||||||
'ancestors.id': this.creatureId,
|
'ancestors.id': this.creatureId,
|
||||||
type: 'propertySlot',
|
type: 'propertySlot',
|
||||||
$or: [
|
$or: [
|
||||||
{slotConditionResult: {$nin: [false, 0, '']}},
|
{'slotCondition.value': {$nin: [false, 0, '']}},
|
||||||
{slotConditionResult: {$exists: false}},
|
{'slotCondition.value': {$exists: false}},
|
||||||
],
|
],
|
||||||
removed: {$ne: true},
|
removed: {$ne: true},
|
||||||
inactive: {$ne: true},
|
inactive: {$ne: true},
|
||||||
@@ -130,7 +130,7 @@ export default {
|
|||||||
}).map(slot => {
|
}).map(slot => {
|
||||||
if (
|
if (
|
||||||
!this.showHiddenSlots &&
|
!this.showHiddenSlots &&
|
||||||
slot.quantityExpectedResult === 0 &&
|
(slot.quantityExpected && slot.quantityExpected.value) === 0 &&
|
||||||
slot.hideWhenFull
|
slot.hideWhenFull
|
||||||
){
|
){
|
||||||
slot.children = []
|
slot.children = []
|
||||||
@@ -146,7 +146,7 @@ export default {
|
|||||||
}).filter(slot => !( // Hide full and ignored slots
|
}).filter(slot => !( // Hide full and ignored slots
|
||||||
!this.showHiddenSlots && (
|
!this.showHiddenSlots && (
|
||||||
slot.hideWhenFull &&
|
slot.hideWhenFull &&
|
||||||
slot.quantityExpectedResult > 0 &&
|
(slot.quantityExpected && slot.quantityExpected.value) > 0 &&
|
||||||
slot.spaceLeft <= 0 ||
|
slot.spaceLeft <= 0 ||
|
||||||
slot.ignored
|
slot.ignored
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,30 @@
|
|||||||
>
|
>
|
||||||
<div class="layout align-center px-3">
|
<div class="layout align-center px-3">
|
||||||
<div class="avatar">
|
<div class="avatar">
|
||||||
|
<roll-popup
|
||||||
|
v-if="rollBonus"
|
||||||
|
icon
|
||||||
|
outlined
|
||||||
|
style="font-size: 16px; letter-spacing: normal;"
|
||||||
|
class="mr-2"
|
||||||
|
:color="model.color || 'primary'"
|
||||||
|
:loading="doActionLoading"
|
||||||
|
:disabled="model.insufficientResources || !context.editPermission"
|
||||||
|
:roll-text="rollBonus"
|
||||||
|
:name="model.name"
|
||||||
|
:advantage="model.attackRoll && model.attackRoll.advantage"
|
||||||
|
@roll="doAction"
|
||||||
|
>
|
||||||
|
<template v-if="rollBonus && !rollBonusTooLong">
|
||||||
|
{{ rollBonus }}
|
||||||
|
</template>
|
||||||
|
<property-icon
|
||||||
|
v-else
|
||||||
|
:model="model"
|
||||||
|
/>
|
||||||
|
</roll-popup>
|
||||||
<v-btn
|
<v-btn
|
||||||
|
v-else
|
||||||
icon
|
icon
|
||||||
outlined
|
outlined
|
||||||
style="font-size: 16px; letter-spacing: normal;"
|
style="font-size: 16px; letter-spacing: normal;"
|
||||||
@@ -15,11 +38,7 @@
|
|||||||
:disabled="model.insufficientResources || !context.editPermission"
|
:disabled="model.insufficientResources || !context.editPermission"
|
||||||
@click.stop="doAction"
|
@click.stop="doAction"
|
||||||
>
|
>
|
||||||
<template v-if="rollBonus && !rollBonusTooLong">
|
|
||||||
{{ rollBonus }}
|
|
||||||
</template>
|
|
||||||
<property-icon
|
<property-icon
|
||||||
v-else
|
|
||||||
:model="model"
|
:model="model"
|
||||||
/>
|
/>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@@ -75,6 +94,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<card-highlight :active="hovering" />
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -85,7 +105,10 @@ import doAction from '/imports/api/engine/actions/doAction.js';
|
|||||||
import AttributeConsumedView from '/imports/ui/properties/components/actions/AttributeConsumedView.vue';
|
import AttributeConsumedView from '/imports/ui/properties/components/actions/AttributeConsumedView.vue';
|
||||||
import ItemConsumedView from '/imports/ui/properties/components/actions/ItemConsumedView.vue';
|
import ItemConsumedView from '/imports/ui/properties/components/actions/ItemConsumedView.vue';
|
||||||
import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
|
import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
|
||||||
|
import RollPopup from '/imports/ui/components/RollPopup.vue';
|
||||||
import MarkdownText from '/imports/ui/components/MarkdownText.vue';
|
import MarkdownText from '/imports/ui/components/MarkdownText.vue';
|
||||||
|
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||||
|
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -93,6 +116,8 @@ export default {
|
|||||||
ItemConsumedView,
|
ItemConsumedView,
|
||||||
MarkdownText,
|
MarkdownText,
|
||||||
PropertyIcon,
|
PropertyIcon,
|
||||||
|
RollPopup,
|
||||||
|
CardHighlight
|
||||||
},
|
},
|
||||||
inject: {
|
inject: {
|
||||||
context: {
|
context: {
|
||||||
@@ -131,7 +156,7 @@ export default {
|
|||||||
'theme--dark': this.theme.isDark,
|
'theme--dark': this.theme.isDark,
|
||||||
'theme--light': !this.theme.isDark,
|
'theme--light': !this.theme.isDark,
|
||||||
'muted-text': this.model.insufficientResources,
|
'muted-text': this.model.insufficientResources,
|
||||||
'shrink': this.activated,
|
'active': this.activated,
|
||||||
'elevation-8': this.hovering,
|
'elevation-8': this.hovering,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -143,13 +168,19 @@ export default {
|
|||||||
click(e){
|
click(e){
|
||||||
this.$emit('click', e);
|
this.$emit('click', e);
|
||||||
},
|
},
|
||||||
doAction(){
|
doAction({advantage}){
|
||||||
this.doActionLoading = true;
|
this.doActionLoading = true;
|
||||||
this.shwing();
|
this.shwing();
|
||||||
doAction.call({actionId: this.model._id}, error => {
|
doAction.call({
|
||||||
|
actionId: this.model._id,
|
||||||
|
scope: {
|
||||||
|
$attackAdvantage: advantage,
|
||||||
|
}
|
||||||
|
}, error => {
|
||||||
this.doActionLoading = false;
|
this.doActionLoading = false;
|
||||||
if (error){
|
if (error){
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
snackbar({text: error.reason});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -157,7 +188,7 @@ export default {
|
|||||||
this.activated = true;
|
this.activated = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.activated = undefined;
|
this.activated = undefined;
|
||||||
}, 300);
|
}, 150);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,7 +196,11 @@ export default {
|
|||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
.action-card {
|
.action-card {
|
||||||
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1),
|
||||||
|
transform 0.075s ease;
|
||||||
|
}
|
||||||
|
.action-card.active {
|
||||||
|
transform: scale(0.92);
|
||||||
}
|
}
|
||||||
.action-title {
|
.action-title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
@@ -1,32 +1,46 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
class="ability-list-tile"
|
class="ability-list-tile pl-0"
|
||||||
v-on="hasClickListener ? {click} : {}"
|
v-on="hasClickListener ? {click} : {}"
|
||||||
>
|
>
|
||||||
<v-list-item-action
|
<v-list-item-action
|
||||||
class="mr-4"
|
class="ma-0"
|
||||||
style="min-width: 40px;"
|
style="min-width: 40px;"
|
||||||
>
|
>
|
||||||
<div class="text-h4 mod">
|
<roll-popup
|
||||||
<template v-if="swapScoresAndMods">
|
button-class="mr-4 py-2"
|
||||||
<span :class="{'primary--text': model.total !== model.value}">
|
text
|
||||||
{{ model.value }}
|
height="82"
|
||||||
</span>
|
:roll-text="numberToSignedString(model.modifier)"
|
||||||
</template>
|
:name="model.name"
|
||||||
<template v-else>
|
:advantage="model.advantage"
|
||||||
{{ numberToSignedString(model.modifier) }}
|
:loading="checkLoading"
|
||||||
</template>
|
:disabled="!context.editPermission"
|
||||||
</div>
|
@roll="check"
|
||||||
<div class="text-h6 value">
|
>
|
||||||
<template v-if="swapScoresAndMods">
|
<div>
|
||||||
{{ numberToSignedString(model.modifier) }}
|
<div class="text-h4 mod">
|
||||||
</template>
|
<template v-if="swapScoresAndMods">
|
||||||
<template v-else>
|
<span :class="{'primary--text': model.total !== model.value}">
|
||||||
<span :class="{'primary--text': model.total !== model.value}">
|
{{ model.value }}
|
||||||
{{ model.value }}
|
</span>
|
||||||
</span>
|
</template>
|
||||||
</template>
|
<template v-else>
|
||||||
</div>
|
{{ numberToSignedString(model.modifier) }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="text-h6 value">
|
||||||
|
<template v-if="swapScoresAndMods">
|
||||||
|
{{ numberToSignedString(model.modifier) }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span :class="{'primary--text': model.total !== model.value}">
|
||||||
|
{{ model.value }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</roll-popup>
|
||||||
</v-list-item-action>
|
</v-list-item-action>
|
||||||
|
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
@@ -39,10 +53,25 @@
|
|||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.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 {
|
export default {
|
||||||
|
components: {
|
||||||
|
RollPopup,
|
||||||
|
},
|
||||||
|
inject: {
|
||||||
|
context: {
|
||||||
|
default: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
model: {type: Object, required: true},
|
model: {type: Object, required: true},
|
||||||
},
|
},
|
||||||
|
data(){return {
|
||||||
|
checkLoading: false,
|
||||||
|
}},
|
||||||
computed: {
|
computed: {
|
||||||
hasClickListener(){
|
hasClickListener(){
|
||||||
return this.$listeners && this.$listeners.click
|
return this.$listeners && this.$listeners.click
|
||||||
@@ -53,6 +82,21 @@ export default {
|
|||||||
click(e){
|
click(e){
|
||||||
this.$emit('click', e);
|
this.$emit('click', e);
|
||||||
},
|
},
|
||||||
|
check({advantage}){
|
||||||
|
this.checkLoading = true;
|
||||||
|
doCheck.call({
|
||||||
|
propId: this.model._id,
|
||||||
|
scope: {
|
||||||
|
$checkAdvantage: advantage,
|
||||||
|
},
|
||||||
|
}, error => {
|
||||||
|
this.checkLoading = false;
|
||||||
|
if (error){
|
||||||
|
console.error(error);
|
||||||
|
snackbar({text: error.reason});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
meteor: {
|
meteor: {
|
||||||
swapScoresAndMods(){
|
swapScoresAndMods(){
|
||||||
@@ -86,5 +130,6 @@ export default {
|
|||||||
.mod, .value {
|
.mod, .value {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-width: 42px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,27 +2,68 @@
|
|||||||
<v-card
|
<v-card
|
||||||
:hover="hasClickListener"
|
:hover="hasClickListener"
|
||||||
@click="click"
|
@click="click"
|
||||||
|
@mouseover="hasClickListener ? hovering = true : undefined"
|
||||||
|
@mouseleave="hasClickListener ? hovering = false : undefined"
|
||||||
>
|
>
|
||||||
<div class="layout align-center">
|
<div class="layout align-center">
|
||||||
<v-card-title class="value text-h4 flex-shrink-0">
|
<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 }}
|
{{ computedValue }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-card-title class="name text-subtitle-1 text-truncate d-block pl-0">
|
<v-card-title class="name text-subtitle-1 text-truncate d-block pl-0">
|
||||||
{{ model.name }}
|
{{ model.name }}
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
</div>
|
</div>
|
||||||
|
<card-highlight :active="hasClickListener && hovering" />
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
import 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';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
RollPopup,
|
||||||
|
CardHighlight,
|
||||||
|
},
|
||||||
|
inject: {
|
||||||
|
context: {
|
||||||
|
default: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
model: {
|
model: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data(){return {
|
||||||
|
checkLoading: false,
|
||||||
|
hovering: false,
|
||||||
|
}},
|
||||||
computed: {
|
computed: {
|
||||||
hasClickListener(){
|
hasClickListener(){
|
||||||
return this.$listeners && !!this.$listeners.click
|
return this.$listeners && !!this.$listeners.click
|
||||||
@@ -40,13 +81,28 @@
|
|||||||
click(e){
|
click(e){
|
||||||
this.$emit('click', e);
|
this.$emit('click', e);
|
||||||
},
|
},
|
||||||
|
check({advantage}){
|
||||||
|
this.checkLoading = true;
|
||||||
|
doCheck.call({
|
||||||
|
propId: this.model._id,
|
||||||
|
scope: {
|
||||||
|
$checkAdvantage: advantage,
|
||||||
|
},
|
||||||
|
}, error => {
|
||||||
|
this.checkLoading = false;
|
||||||
|
if (error){
|
||||||
|
console.error(error);
|
||||||
|
snackbar({text: error.reason});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
.value {
|
.value {
|
||||||
min-width: 64px;
|
min-width: 72px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -43,11 +43,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
|
<card-highlight :active="hover" />
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
|
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
CardHighlight,
|
||||||
|
},
|
||||||
inject: {
|
inject: {
|
||||||
context: { default: {} }
|
context: { default: {} }
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
|
:key="model._id"
|
||||||
class="spell-slot-list-tile"
|
class="spell-slot-list-tile"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:disabled="disabled"
|
||||||
v-on="hasClickListener ? {click} : {}"
|
v-on="hasClickListener ? {click} : {}"
|
||||||
>
|
>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
<v-list-item-title>
|
<v-list-item-title
|
||||||
|
v-if="Number.isFinite(model.total)"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="model.total > 4"
|
v-if="model.total > 4"
|
||||||
class="layout value"
|
class="layout value"
|
||||||
@@ -27,6 +32,7 @@
|
|||||||
<v-icon
|
<v-icon
|
||||||
v-for="i in model.total"
|
v-for="i in model.total"
|
||||||
:key="i"
|
:key="i"
|
||||||
|
:disabled="disabled"
|
||||||
>
|
>
|
||||||
{{
|
{{
|
||||||
i > model.value ?
|
i > model.value ?
|
||||||
@@ -36,21 +42,15 @@
|
|||||||
</v-icon>
|
</v-icon>
|
||||||
</div>
|
</div>
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
|
<v-list-item-title v-else>
|
||||||
|
<code>
|
||||||
|
{{ model.total }}
|
||||||
|
</code>
|
||||||
|
</v-list-item-title>
|
||||||
<v-list-item-subtitle>
|
<v-list-item-subtitle>
|
||||||
{{ model.name }}
|
{{ model.name }}
|
||||||
</v-list-item-subtitle>
|
</v-list-item-subtitle>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
<v-list-item-avatar v-if="!hideCastButton">
|
|
||||||
<v-btn
|
|
||||||
icon
|
|
||||||
text
|
|
||||||
class="primary--text"
|
|
||||||
:data-id="`spell-slot-cast-btn-${model._id}`"
|
|
||||||
@click.stop="$emit('cast')"
|
|
||||||
>
|
|
||||||
<v-icon>$vuetify.icons.spell</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</v-list-item-avatar>
|
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -64,6 +64,7 @@ export default {
|
|||||||
},
|
},
|
||||||
dark: Boolean,
|
dark: Boolean,
|
||||||
hideCastButton: Boolean,
|
hideCastButton: Boolean,
|
||||||
|
disabled: Boolean,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasClickListener(){
|
hasClickListener(){
|
||||||
|
|||||||
@@ -28,12 +28,9 @@
|
|||||||
color="primary"
|
color="primary"
|
||||||
:disabled="context.editPermission === false"
|
:disabled="context.editPermission === false"
|
||||||
:value="model.quantity"
|
:value="model.quantity"
|
||||||
|
:loading="incrementLoading"
|
||||||
@change="changeQuantity"
|
@change="changeQuantity"
|
||||||
>
|
/>
|
||||||
<v-icon>
|
|
||||||
$vuetify.icons.abacus
|
|
||||||
</v-icon>
|
|
||||||
</increment-button>
|
|
||||||
</v-list-item-action>
|
</v-list-item-action>
|
||||||
<v-list-item-action>
|
<v-list-item-action>
|
||||||
<v-icon
|
<v-icon
|
||||||
@@ -52,6 +49,7 @@ import treeNodeViewMixin from '/imports/ui/properties/treeNodeViews/treeNodeView
|
|||||||
import PROPERTIES from '/imports/constants/PROPERTIES.js';
|
import PROPERTIES from '/imports/constants/PROPERTIES.js';
|
||||||
import adjustQuantity from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
import adjustQuantity from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
||||||
import IncrementButton from '/imports/ui/components/IncrementButton.vue';
|
import IncrementButton from '/imports/ui/components/IncrementButton.vue';
|
||||||
|
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components:{
|
components:{
|
||||||
@@ -64,6 +62,9 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
preparingSpells: Boolean,
|
preparingSpells: Boolean,
|
||||||
},
|
},
|
||||||
|
data(){return {
|
||||||
|
incrementLoading: false,
|
||||||
|
}},
|
||||||
computed: {
|
computed: {
|
||||||
hasClickListener(){
|
hasClickListener(){
|
||||||
return this.$listeners && !!this.$listeners.click;
|
return this.$listeners && !!this.$listeners.click;
|
||||||
@@ -89,10 +90,17 @@ export default {
|
|||||||
this.$emit('click', e);
|
this.$emit('click', e);
|
||||||
},
|
},
|
||||||
changeQuantity({type, value}) {
|
changeQuantity({type, value}) {
|
||||||
|
this.incrementLoading = true;
|
||||||
adjustQuantity.call({
|
adjustQuantity.call({
|
||||||
_id: this.model._id,
|
_id: this.model._id,
|
||||||
operation: type,
|
operation: type,
|
||||||
value: value
|
value: value
|
||||||
|
}, error => {
|
||||||
|
this.incrementLoading = false;
|
||||||
|
if (error){
|
||||||
|
snackbar({text: error.reason});
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
:dark="model.color && isDark"
|
:dark="model.color && isDark"
|
||||||
:light="model.color && !isDark"
|
:light="model.color && !isDark"
|
||||||
@click="clickProperty(model._id)"
|
@click="clickProperty(model._id)"
|
||||||
|
@mouseover="hover = true"
|
||||||
|
@mouseleave="hover = false"
|
||||||
>
|
>
|
||||||
<v-card-title class="text-h6">
|
<v-card-title class="text-h6">
|
||||||
{{ model.name }}
|
{{ model.name }}
|
||||||
@@ -16,23 +18,39 @@
|
|||||||
:model="model.summary"
|
:model="model.summary"
|
||||||
/>
|
/>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
<card-highlight
|
||||||
|
:active="hover"
|
||||||
|
:dark="theme.isDark"
|
||||||
|
/>
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue';
|
import PropertyDescription from '/imports/ui/properties/viewers/shared/PropertyDescription.vue';
|
||||||
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
|
import isDarkColor from '/imports/ui/utility/isDarkColor.js';
|
||||||
|
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
PropertyDescription,
|
PropertyDescription,
|
||||||
|
CardHighlight,
|
||||||
},
|
},
|
||||||
|
inject: {
|
||||||
|
theme: {
|
||||||
|
default: {
|
||||||
|
isDark: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
model: {
|
model: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data(){ return{
|
||||||
|
hover: false,
|
||||||
|
}},
|
||||||
computed: {
|
computed: {
|
||||||
isDark(){
|
isDark(){
|
||||||
return isDarkColor(this.model.color);
|
return isDarkColor(this.model.color);
|
||||||
|
|||||||
@@ -1,40 +1,57 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
class="skill-list-tile"
|
class="skill-list-tile pl-0"
|
||||||
style="min-height: 36px;"
|
style="min-height: 36px;"
|
||||||
v-on="hasClickListener ? {click} : {}"
|
v-on="hasClickListener ? {click} : {}"
|
||||||
>
|
>
|
||||||
<proficiency-icon
|
<v-list-item-content class="py-0">
|
||||||
:value="model.proficiency"
|
<v-list-item-title class="d-flex align-center">
|
||||||
class="prof-icon"
|
<roll-popup
|
||||||
/>
|
|
||||||
<v-list-item-content class="py-1">
|
|
||||||
<v-list-item-title>
|
|
||||||
<span
|
|
||||||
v-if="!hideModifier"
|
v-if="!hideModifier"
|
||||||
class="prof-mod"
|
class="prof-mod mr-1"
|
||||||
|
button-class="pl-3 pr-2"
|
||||||
|
text
|
||||||
|
:roll-text="displayedModifier"
|
||||||
|
:name="model.name"
|
||||||
|
:advantage="model.advantage"
|
||||||
|
:loading="checkLoading"
|
||||||
|
:disabled="!context.editPermission"
|
||||||
|
@roll="check"
|
||||||
>
|
>
|
||||||
{{ displayedModifier }}
|
<proficiency-icon
|
||||||
</span>
|
:value="model.proficiency"
|
||||||
<v-icon
|
class="prof-icon"
|
||||||
v-if="model.advantage > 0"
|
/>
|
||||||
size="20px"
|
<div class="prof-mod">
|
||||||
>
|
{{ displayedModifier }}
|
||||||
mdi-chevron-double-up
|
</div>
|
||||||
</v-icon>
|
<v-icon
|
||||||
<v-icon
|
v-if="model.advantage > 0"
|
||||||
v-if="model.advantage < 0"
|
size="20px"
|
||||||
size="20px"
|
>
|
||||||
>
|
mdi-chevron-double-up
|
||||||
mdi-chevron-double-down
|
</v-icon>
|
||||||
</v-icon>
|
<v-icon
|
||||||
{{ model.name }}
|
v-if="model.advantage < 0"
|
||||||
<template v-if="model.conditionalBenefits && model.conditionalBenefits.length">
|
size="20px"
|
||||||
*
|
>
|
||||||
</template>
|
mdi-chevron-double-down
|
||||||
<template v-if="'passiveBonus' in model">
|
</v-icon>
|
||||||
({{ passiveScore }})
|
</roll-popup>
|
||||||
</template>
|
<proficiency-icon
|
||||||
|
v-else
|
||||||
|
:value="model.proficiency"
|
||||||
|
class="prof-icon ml-3 mr-2"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
{{ model.name }}
|
||||||
|
<template v-if="model.conditionalBenefits && model.conditionalBenefits.length">
|
||||||
|
*
|
||||||
|
</template>
|
||||||
|
<template v-if="'passiveBonus' in model">
|
||||||
|
({{ passiveScore }})
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
@@ -43,10 +60,19 @@
|
|||||||
<script lang="js">
|
<script lang="js">
|
||||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
||||||
import ProficiencyIcon from '/imports/ui/properties/shared/ProficiencyIcon.vue';
|
import ProficiencyIcon from '/imports/ui/properties/shared/ProficiencyIcon.vue';
|
||||||
|
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 {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ProficiencyIcon,
|
ProficiencyIcon,
|
||||||
|
RollPopup,
|
||||||
|
},
|
||||||
|
inject: {
|
||||||
|
context: {
|
||||||
|
default: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
model: {
|
model: {
|
||||||
@@ -55,6 +81,9 @@ export default {
|
|||||||
},
|
},
|
||||||
hideModifier: Boolean,
|
hideModifier: Boolean,
|
||||||
},
|
},
|
||||||
|
data(){return {
|
||||||
|
checkLoading: false,
|
||||||
|
}},
|
||||||
computed: {
|
computed: {
|
||||||
displayedModifier(){
|
displayedModifier(){
|
||||||
let mod = this.model.value;
|
let mod = this.model.value;
|
||||||
@@ -75,6 +104,21 @@ export default {
|
|||||||
click(e){
|
click(e){
|
||||||
this.$emit('click', e);
|
this.$emit('click', e);
|
||||||
},
|
},
|
||||||
|
check({advantage}){
|
||||||
|
this.checkLoading = true;
|
||||||
|
doCheck.call({
|
||||||
|
propId: this.model._id,
|
||||||
|
scope: {
|
||||||
|
$checkAdvantage: advantage,
|
||||||
|
},
|
||||||
|
}, error => {
|
||||||
|
this.checkLoading = false;
|
||||||
|
if (error){
|
||||||
|
console.error(error);
|
||||||
|
snackbar({text: error.reason});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -84,8 +128,9 @@ export default {
|
|||||||
min-width: 30px;
|
min-width: 30px;
|
||||||
}
|
}
|
||||||
.prof-mod {
|
.prof-mod {
|
||||||
display: inline-block;
|
min-width: 32px;
|
||||||
width: 45px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
.v-icon.theme--light {
|
||||||
|
color: rgba(0, 0, 0, 0.54) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -73,27 +73,35 @@
|
|||||||
>
|
>
|
||||||
Slot
|
Slot
|
||||||
</div>
|
</div>
|
||||||
<v-list-item
|
<v-list-item-group
|
||||||
v-if="!(selectedSpell && selectedSpell.level > 0)"
|
key="slot-list"
|
||||||
key="cantrip-dummy-slot"
|
v-model="selectedSlotId"
|
||||||
class="spell-slot-list-tile"
|
|
||||||
:class="{ 'primary--text': selectedSlotId === undefined}"
|
|
||||||
@click="selectedSlotId = undefined"
|
|
||||||
>
|
>
|
||||||
<v-list-item-content>
|
<v-list-item
|
||||||
<v-list-item-title class="text-h6">
|
key="cantrip-dummy-slot"
|
||||||
Cantrip
|
class="spell-slot-list-tile"
|
||||||
</v-list-item-title>
|
:class="{ 'primary--text': selectedSlotId === 'no-slot' }"
|
||||||
</v-list-item-content>
|
value="no-slot"
|
||||||
</v-list-item>
|
:disabled="!canCastSpellWithSlot(selectedSpell, 'no-slot')"
|
||||||
<spell-slot-list-tile
|
@click="selectedSlotId = 'no-slot'"
|
||||||
v-for="spellSlot in spellSlots"
|
>
|
||||||
:key="spellSlot._id"
|
<v-list-item-content>
|
||||||
:model="spellSlot"
|
<v-list-item-title>
|
||||||
:class="{ 'primary--text': selectedSlotId === spellSlot._id }"
|
Cast without spell slot
|
||||||
hide-cast-button
|
</v-list-item-title>
|
||||||
@click="selectedSlotId = spellSlot._id"
|
</v-list-item-content>
|
||||||
/>
|
</v-list-item>
|
||||||
|
<spell-slot-list-tile
|
||||||
|
v-for="spellSlot in spellSlots"
|
||||||
|
:key="spellSlot._id"
|
||||||
|
:model="spellSlot"
|
||||||
|
:class="{ 'primary--text': selectedSlotId === spellSlot._id }"
|
||||||
|
:value="spellSlot._id"
|
||||||
|
:disabled="!canCastSpellWithSlot(selectedSpell, spellSlot._id, spellSlot)"
|
||||||
|
hide-cast-button
|
||||||
|
@click="selectedSlotId = spellSlot._id"
|
||||||
|
/>
|
||||||
|
</v-list-item-group>
|
||||||
</template>
|
</template>
|
||||||
<template slot="right">
|
<template slot="right">
|
||||||
<div
|
<div
|
||||||
@@ -102,25 +110,31 @@
|
|||||||
>
|
>
|
||||||
Spell
|
Spell
|
||||||
</div>
|
</div>
|
||||||
<template v-for="spell in computedSpells">
|
<v-list-item-group
|
||||||
<v-subheader
|
key="slot-list"
|
||||||
v-if="spell.isSubheader"
|
v-model="selectedSpellId"
|
||||||
:key="`${spell.level}-header`"
|
>
|
||||||
class="item"
|
<template v-for="spell in computedSpells">
|
||||||
>
|
<v-subheader
|
||||||
{{ spell.level === 0 ? 'Cantrips' : `Level ${spell.level}` }}
|
v-if="spell.isSubheader"
|
||||||
</v-subheader>
|
:key="`${spell.level}-header`"
|
||||||
<spell-list-tile
|
class="item"
|
||||||
v-else
|
>
|
||||||
:key="spell._id"
|
{{ spell.level === 0 ? 'Cantrips' : `Level ${spell.level}` }}
|
||||||
hide-handle
|
</v-subheader>
|
||||||
show-info-button
|
<spell-list-tile
|
||||||
:class="{ 'primary--text': selectedSpellId === spell._id}"
|
v-else
|
||||||
:model="spell"
|
:key="spell._id"
|
||||||
@click="selectedSpellId = spell._id"
|
hide-handle
|
||||||
@show-info="spellDialog(spell._id)"
|
show-info-button
|
||||||
/>
|
:model="spell"
|
||||||
</template>
|
:value="spell._id"
|
||||||
|
:class="{ 'primary--text': selectedSpellId === spell._id }"
|
||||||
|
:disabled="!canCastSpellWithSlot(spell, selectedSlotId, selectedSlot)"
|
||||||
|
@show-info="spellDialog(spell._id)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-list-item-group>
|
||||||
</template>
|
</template>
|
||||||
</split-list-layout>
|
</split-list-layout>
|
||||||
<template slot="actions">
|
<template slot="actions">
|
||||||
@@ -135,10 +149,7 @@
|
|||||||
text
|
text
|
||||||
:disabled="!canCast"
|
:disabled="!canCast"
|
||||||
class="primary--text"
|
class="primary--text"
|
||||||
@click="$store.dispatch('popDialogStack', {
|
@click="cast"
|
||||||
spellId: selectedSpellId,
|
|
||||||
slotId: selectedSlotId,
|
|
||||||
})"
|
|
||||||
>
|
>
|
||||||
Cast
|
Cast
|
||||||
</v-btn>
|
</v-btn>
|
||||||
@@ -153,6 +164,16 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
|
|||||||
import spellsWithSubheaders from '/imports/ui/properties/components/spells/spellsWithSubheaders.js';
|
import spellsWithSubheaders from '/imports/ui/properties/components/spells/spellsWithSubheaders.js';
|
||||||
import SpellSlotListTile from '/imports/ui/properties/components/attributes/SpellSlotListTile.vue';
|
import SpellSlotListTile from '/imports/ui/properties/components/attributes/SpellSlotListTile.vue';
|
||||||
import SpellListTile from '/imports/ui/properties/components/spells/SpellListTile.vue';
|
import SpellListTile from '/imports/ui/properties/components/spells/SpellListTile.vue';
|
||||||
|
import { find } from 'lodash';
|
||||||
|
|
||||||
|
const slotFilter = {
|
||||||
|
type: 'attribute',
|
||||||
|
attributeType: 'spellSlot',
|
||||||
|
removed: {$ne: true},
|
||||||
|
inactive: {$ne: true},
|
||||||
|
overridden: {$ne: true},
|
||||||
|
'spellSlotLevel.value': {$gte: 1},
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -179,6 +200,8 @@ export default {
|
|||||||
searchString: undefined,
|
searchString: undefined,
|
||||||
selectedSlotId: this.slotId,
|
selectedSlotId: this.slotId,
|
||||||
selectedSpellId: this.spellId,
|
selectedSpellId: this.spellId,
|
||||||
|
selectedSlot: undefined,
|
||||||
|
selectedSpell: undefined,
|
||||||
searchValue: undefined,
|
searchValue: undefined,
|
||||||
searchError: undefined,
|
searchError: undefined,
|
||||||
filterMenuOpen: false,
|
filterMenuOpen: false,
|
||||||
@@ -195,16 +218,10 @@ export default {
|
|||||||
return spellsWithSubheaders(this.spells);
|
return spellsWithSubheaders(this.spells);
|
||||||
},
|
},
|
||||||
canCast(){
|
canCast(){
|
||||||
let spell = this.selectedSpell;
|
if (!this.selectedSpell || !this.selectedSlotId) return false;
|
||||||
let slot = this.selectedSlot;
|
return this.canCastSpellWithSlot(
|
||||||
if (!spell) return false;
|
this.selectedSpell, this.selectedSlotId, this.selectedSlot
|
||||||
if (spell.level === 0){
|
);
|
||||||
return this.selectedSlotId === undefined;
|
|
||||||
} else if (!slot) {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return slot.spellSlotLevelValue >= spell.level;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
filtersApplied(){
|
filtersApplied(){
|
||||||
for (let key in this.booleanFilters){
|
for (let key in this.booleanFilters){
|
||||||
@@ -216,19 +233,61 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
selectedSpell(spell){
|
selectedSpellId: {
|
||||||
if (!spell) return;
|
handler(spellId){
|
||||||
if(spell.level === 0){
|
this.selectedSpell = CreatureProperties.findOne(spellId)
|
||||||
this.selectedSlotId = undefined;
|
},
|
||||||
}
|
immediate: true
|
||||||
},
|
},
|
||||||
selectedSlot(slot){
|
selectedSpell: {
|
||||||
if (!slot) return;
|
handler(spell){
|
||||||
if (!this.selectedSpell) return;
|
if (!spell) return;
|
||||||
if(slot.spellSlotLevelValue > 0 && this.selectedSpell.level === 0){
|
if(spell.level === 0 || spell.castWithoutSpellSlots){
|
||||||
this.selectedSpellId = undefined;
|
this.selectedSlotId = 'no-slot';
|
||||||
}
|
} else if (
|
||||||
|
!this.selectedSlotId ||
|
||||||
|
this.selectedSlotId == 'no-slot' ||
|
||||||
|
this.selectedSlot.spellSlotLevel.value < spell.level
|
||||||
|
) {
|
||||||
|
const newSlot = find(
|
||||||
|
CreatureProperties.find({
|
||||||
|
'ancestors.id': this.creatureId,
|
||||||
|
...slotFilter
|
||||||
|
}, {
|
||||||
|
sort: {'spellSlotLevel.value': 1, order: 1},
|
||||||
|
}).fetch(),
|
||||||
|
slot => {
|
||||||
|
return this.canCastSpellWithSlot(spell, slot._id, slot)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (newSlot){
|
||||||
|
this.selectedSlotId = newSlot._id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
},
|
},
|
||||||
|
selectedSlotId: {
|
||||||
|
handler(slotId){
|
||||||
|
this.selectedSlot = CreatureProperties.findOne(slotId);
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
},
|
||||||
|
selectedSlot:{
|
||||||
|
handler(slot){
|
||||||
|
if (!slot) return;
|
||||||
|
if (!this.selectedSpell) return;
|
||||||
|
if(this.selectedSpell.level > slot.spellSlotLevel.value){
|
||||||
|
this.selectedSpellId = undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted(){
|
||||||
|
if (this.selectedSpellId){
|
||||||
|
this.$vuetify.goTo('.spell.v-list-item--active', {container: '.right'});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clearBooleanFilters(){
|
clearBooleanFilters(){
|
||||||
@@ -247,10 +306,36 @@ export default {
|
|||||||
this.searchValue = val;
|
this.searchValue = val;
|
||||||
setTimeout(ack, 200);
|
setTimeout(ack, 200);
|
||||||
},
|
},
|
||||||
|
canCastSpellWithSlot(spell, slotId, slot){
|
||||||
|
if (slot && !slot.value) return false;
|
||||||
|
if (!spell) return true;
|
||||||
|
if (!slotId) return true;
|
||||||
|
if (
|
||||||
|
spell.castWithoutSpellSlots &&
|
||||||
|
spell.insufficientResources
|
||||||
|
) return false;
|
||||||
|
return (!spell.level || spell.castWithoutSpellSlots) ? (
|
||||||
|
// Cantrips and no-slot spells
|
||||||
|
slotId && slotId === 'no-slot'
|
||||||
|
) : (
|
||||||
|
// Leveled spells
|
||||||
|
slotId !== 'no-slot' &&
|
||||||
|
slot && spell && (
|
||||||
|
spell.level <= slot.spellSlotLevel.value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cast(){
|
||||||
|
let selectedSlotId = this.selectedSlotId;
|
||||||
|
if (selectedSlotId === 'no-slot') selectedSlotId = undefined;
|
||||||
|
this.$store.dispatch('popDialogStack', {
|
||||||
|
spellId: this.selectedSpellId,
|
||||||
|
slotId: selectedSlotId,
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
meteor: {
|
meteor: {
|
||||||
spells(){
|
spells(){
|
||||||
let slotLevel = this.selectedSlot && this.selectedSlot.spellSlotLevelValue || 0;
|
|
||||||
let filter = {
|
let filter = {
|
||||||
'ancestors.id': this.creatureId,
|
'ancestors.id': this.creatureId,
|
||||||
removed: {$ne: true},
|
removed: {$ne: true},
|
||||||
@@ -259,8 +344,8 @@ export default {
|
|||||||
{prepared: true},
|
{prepared: true},
|
||||||
{alwaysPrepared: true},
|
{alwaysPrepared: true},
|
||||||
],
|
],
|
||||||
level: {$lte: slotLevel},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply the filters from the filter menu
|
// Apply the filters from the filter menu
|
||||||
for (let key in this.booleanFilters){
|
for (let key in this.booleanFilters){
|
||||||
if (this.booleanFilters[key].enabled){
|
if (this.booleanFilters[key].enabled){
|
||||||
@@ -284,27 +369,13 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
spellSlots(){
|
spellSlots(){
|
||||||
let filter = {
|
return CreatureProperties.find({
|
||||||
'ancestors.id': this.creatureId,
|
'ancestors.id': this.creatureId,
|
||||||
type: 'attribute',
|
...slotFilter
|
||||||
attributeType: 'spellSlot',
|
}, {
|
||||||
removed: {$ne: true},
|
sort: {'spellSlotLevel.value': 1, order: 1},
|
||||||
inactive: {$ne: true},
|
|
||||||
currentValue: {$gte: 1},
|
|
||||||
};
|
|
||||||
if (this.selectedSpell){
|
|
||||||
filter.spellSlotLevelValue = {$gte: this.selectedSpell.level};
|
|
||||||
}
|
|
||||||
return CreatureProperties.find(filter, {
|
|
||||||
sort: {order: 1},
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
selectedSlot(){
|
|
||||||
return CreatureProperties.findOne(this.selectedSlotId);
|
|
||||||
},
|
|
||||||
selectedSpell(){
|
|
||||||
return CreatureProperties.findOne(this.selectedSpellId);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -40,8 +40,8 @@
|
|||||||
:class="{'error--text' : preparedError}"
|
:class="{'error--text' : preparedError}"
|
||||||
class="pb-0"
|
class="pb-0"
|
||||||
>
|
>
|
||||||
<div v-if="model.maxPrepared">
|
<div v-if="model.maxPrepared && model.maxPrepared.value">
|
||||||
{{ numPrepared }}/{{ model.maxPreparedResult }} spells prepared
|
{{ numPrepared }}/{{ model.maxPrepared.value }} spells prepared
|
||||||
</div>
|
</div>
|
||||||
<v-switch
|
<v-switch
|
||||||
v-model="preparingSpells"
|
v-model="preparingSpells"
|
||||||
@@ -86,6 +86,7 @@ export default {
|
|||||||
};
|
};
|
||||||
if (this.preparingSpells){
|
if (this.preparingSpells){
|
||||||
filter.deactivatedByAncestor = {$ne: true};
|
filter.deactivatedByAncestor = {$ne: true};
|
||||||
|
filter.deactivatedByToggle = {$ne: true};
|
||||||
} else {
|
} else {
|
||||||
filter.inactive = {$ne: true};
|
filter.inactive = {$ne: true};
|
||||||
}
|
}
|
||||||
@@ -104,12 +105,13 @@ export default {
|
|||||||
prepared: true,
|
prepared: true,
|
||||||
alwaysPrepared: {$ne: true},
|
alwaysPrepared: {$ne: true},
|
||||||
deactivatedByAncestor: {$ne: true},
|
deactivatedByAncestor: {$ne: true},
|
||||||
|
deactivatedByToggle: {$ne: true},
|
||||||
}).count();
|
}).count();
|
||||||
},
|
},
|
||||||
preparedError(){
|
preparedError(){
|
||||||
if (!this.model.maxPrepared) return;
|
if (!this.model.maxPrepared) return;
|
||||||
let numPrepared = this.numPrepared;
|
let numPrepared = this.numPrepared;
|
||||||
let maxPrepared = this.model.maxPreparedResult;
|
let maxPrepared = this.model.maxPrepared.value || 0;
|
||||||
return numPrepared !== maxPrepared
|
return numPrepared !== maxPrepared
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<v-list-item
|
<v-list-item
|
||||||
class="spell"
|
class="spell"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:disabled="disabled"
|
||||||
v-on="hasClickListener ? {click} : {}"
|
v-on="hasClickListener ? {click} : {}"
|
||||||
>
|
>
|
||||||
<v-list-item-avatar class="spell-avatar">
|
<v-list-item-avatar class="spell-avatar">
|
||||||
@@ -8,6 +10,7 @@
|
|||||||
class="mr-2"
|
class="mr-2"
|
||||||
:model="model"
|
:model="model"
|
||||||
:color="model.color"
|
:color="model.color"
|
||||||
|
:disabled="disabled"
|
||||||
/>
|
/>
|
||||||
</v-list-item-avatar>
|
</v-list-item-avatar>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
@@ -38,6 +41,7 @@
|
|||||||
v-else-if="showInfoButton"
|
v-else-if="showInfoButton"
|
||||||
icon
|
icon
|
||||||
class="info-icon"
|
class="info-icon"
|
||||||
|
:disabled="disabled"
|
||||||
:data-id="`spell-info-btn-${model._id}`"
|
:data-id="`spell-info-btn-${model._id}`"
|
||||||
@click.stop="$emit('show-info')"
|
@click.stop="$emit('show-info')"
|
||||||
>
|
>
|
||||||
@@ -53,13 +57,14 @@ import updateCreatureProperty from '/imports/api/creature/creatureProperties/met
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [treeNodeViewMixin],
|
mixins: [treeNodeViewMixin],
|
||||||
|
inject: {
|
||||||
|
context: { default: {} }
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
preparingSpells: Boolean,
|
preparingSpells: Boolean,
|
||||||
hideHandle: Boolean,
|
hideHandle: Boolean,
|
||||||
showInfoButton: Boolean,
|
showInfoButton: Boolean,
|
||||||
},
|
disabled: Boolean,
|
||||||
inject: {
|
|
||||||
context: { default: {} }
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasClickListener(){
|
hasClickListener(){
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
justify="center"
|
justify="center"
|
||||||
class="mb-3"
|
class="mb-3"
|
||||||
>
|
>
|
||||||
<v-col cols="1">
|
<v-col cols="12">
|
||||||
<icon-color-menu
|
<icon-color-menu
|
||||||
:model="model"
|
:model="model"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
|
|||||||
@@ -27,6 +27,15 @@
|
|||||||
@change="change('variableName', ...arguments)"
|
@change="change('variableName', ...arguments)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<text-field
|
||||||
|
v-if="context.isLibraryForm"
|
||||||
|
label="Condition"
|
||||||
|
hint="A caclulation to determine if this can be added to the character"
|
||||||
|
placeholder="Always active"
|
||||||
|
:value="model.slotFillerCondition"
|
||||||
|
:error-messages="errors.slotFillerCondition"
|
||||||
|
@change="change('slotFillerCondition', ...arguments)"
|
||||||
|
/>
|
||||||
|
|
||||||
<inline-computation-field
|
<inline-computation-field
|
||||||
label="Description"
|
label="Description"
|
||||||
@@ -54,6 +63,9 @@
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [propertyFormMixin],
|
mixins: [propertyFormMixin],
|
||||||
|
inject: {
|
||||||
|
context: { default: {} }
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
:error-messages="errors.name"
|
:error-messages="errors.name"
|
||||||
@change="change('name', ...arguments)"
|
@change="change('name', ...arguments)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<text-area
|
<text-area
|
||||||
label="Description"
|
label="Description"
|
||||||
:value="model.description"
|
:value="model.description"
|
||||||
@@ -74,12 +73,8 @@
|
|||||||
<script lang="js">
|
<script lang="js">
|
||||||
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
||||||
import PROPERTIES from '/imports/constants/PROPERTIES.js';
|
import PROPERTIES from '/imports/constants/PROPERTIES.js';
|
||||||
import CalculationErrorList from '/imports/ui/properties/forms/shared/CalculationErrorList.vue';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
|
||||||
CalculationErrorList,
|
|
||||||
},
|
|
||||||
mixins: [propertyFormMixin],
|
mixins: [propertyFormMixin],
|
||||||
inject: {
|
inject: {
|
||||||
context: { default: {} }
|
context: { default: {} }
|
||||||
|
|||||||
@@ -1,103 +1,227 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<div class="spell-form">
|
<div class="spell-form">
|
||||||
<div class="layout wrap justify-space-between">
|
<v-row
|
||||||
<smart-switch
|
justify="center"
|
||||||
label="Always prepared"
|
class="mb-3"
|
||||||
style="width: 200px; flex-grow: 0;"
|
>
|
||||||
class="mx-2"
|
<v-col cols="12">
|
||||||
:value="model.alwaysPrepared"
|
<icon-color-menu
|
||||||
:error-messages="errors.alwaysPrepared"
|
:model="model"
|
||||||
@change="change('alwaysPrepared', ...arguments)"
|
:errors="errors"
|
||||||
/>
|
@change="e => $emit('change', e)"
|
||||||
<smart-switch
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row dense>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
sm="6"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<smart-switch
|
||||||
|
label="Always prepared"
|
||||||
|
:value="model.alwaysPrepared"
|
||||||
|
:error-messages="errors.alwaysPrepared"
|
||||||
|
@change="change('alwaysPrepared', ...arguments)"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
v-show="!model.alwaysPrepared"
|
v-show="!model.alwaysPrepared"
|
||||||
label="Prepared"
|
cols="12"
|
||||||
style="width: 200px; flex-grow: 0;"
|
sm="6"
|
||||||
class="mx-2"
|
md="4"
|
||||||
:value="model.prepared"
|
>
|
||||||
:error-messages="errors.prepared"
|
<smart-switch
|
||||||
@change="change('prepared', ...arguments)"
|
label="Prepared"
|
||||||
/>
|
:value="model.prepared"
|
||||||
</div>
|
:error-messages="errors.prepared"
|
||||||
<text-field
|
@change="change('prepared', ...arguments)"
|
||||||
ref="focusFirst"
|
/>
|
||||||
label="Name"
|
</v-col>
|
||||||
:value="model.name"
|
<v-col
|
||||||
:error-messages="errors.name"
|
v-show="model.level"
|
||||||
@change="change('name', ...arguments)"
|
cols="12"
|
||||||
/>
|
sm="6"
|
||||||
<div class="layout wrap">
|
md="4"
|
||||||
<smart-select
|
>
|
||||||
label="Level"
|
<smart-switch
|
||||||
class="mx-1"
|
label="Cast without spell slots"
|
||||||
style="flex-basis: 300px;"
|
:value="model.castWithoutSpellSlots"
|
||||||
hint="The spell level"
|
:error-messages="errors.castWithoutSpellSlots"
|
||||||
:items="spellLevels"
|
@change="change('castWithoutSpellSlots', ...arguments)"
|
||||||
:value="model.level"
|
/>
|
||||||
:error-messages="errors.level"
|
</v-col>
|
||||||
@change="change('level', ...arguments)"
|
</v-row>
|
||||||
/>
|
<v-row dense>
|
||||||
<smart-select
|
<v-col
|
||||||
label="School"
|
cols="12"
|
||||||
class="mx-1"
|
md="6"
|
||||||
style="flex-basis: 300px;"
|
>
|
||||||
:items="magicSchools"
|
<text-field
|
||||||
:value="model.school"
|
ref="focusFirst"
|
||||||
:error-messages="errors.school"
|
label="Name"
|
||||||
@change="change('school', ...arguments)"
|
:value="model.name"
|
||||||
/>
|
:error-messages="errors.name"
|
||||||
</div>
|
@change="change('name', ...arguments)"
|
||||||
<text-field
|
/>
|
||||||
label="Casting Time"
|
</v-col>
|
||||||
:value="model.castingTime"
|
<v-col
|
||||||
:error-messages="errors.castingTime"
|
cols="12"
|
||||||
@change="change('castingTime', ...arguments)"
|
md="6"
|
||||||
/>
|
>
|
||||||
<text-field
|
<smart-select
|
||||||
label="Range"
|
label="Level"
|
||||||
:value="model.range"
|
hint="The spell level"
|
||||||
:error-messages="errors.range"
|
:items="spellLevels"
|
||||||
@change="change('range', ...arguments)"
|
:value="model.level"
|
||||||
/>
|
:error-messages="errors.level"
|
||||||
<div class="layout wrap justify-space-between">
|
@change="change('level', ...arguments)"
|
||||||
<smart-checkbox
|
/>
|
||||||
label="Verbal"
|
</v-col>
|
||||||
:value="model.verbal"
|
<v-col
|
||||||
:error-messages="errors.verbal"
|
cols="12"
|
||||||
@change="change('verbal', ...arguments)"
|
md="6"
|
||||||
/>
|
>
|
||||||
<smart-checkbox
|
<smart-select
|
||||||
label="Somatic"
|
label="School"
|
||||||
:value="model.somatic"
|
:items="magicSchools"
|
||||||
:error-messages="errors.somatic"
|
:value="model.school"
|
||||||
@change="change('somatic', ...arguments)"
|
:error-messages="errors.school"
|
||||||
/>
|
@change="change('school', ...arguments)"
|
||||||
<smart-checkbox
|
/>
|
||||||
label="Concentration"
|
</v-col>
|
||||||
:value="model.concentration"
|
<v-col
|
||||||
:error-messages="errors.concentration"
|
cols="12"
|
||||||
@change="change('concentration', ...arguments)"
|
md="6"
|
||||||
/>
|
>
|
||||||
<smart-checkbox
|
<text-field
|
||||||
label="Ritual"
|
label="Casting Time"
|
||||||
:value="model.ritual"
|
:value="model.castingTime"
|
||||||
:error-messages="errors.ritual"
|
:error-messages="errors.castingTime"
|
||||||
@change="change('ritual', ...arguments)"
|
@change="change('castingTime', ...arguments)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</v-col>
|
||||||
<text-field
|
<v-col
|
||||||
label="Material"
|
cols="12"
|
||||||
:value="model.material"
|
md="6"
|
||||||
:error-messages="errors.material"
|
>
|
||||||
@change="change('material', ...arguments)"
|
<text-field
|
||||||
/>
|
label="Range"
|
||||||
<text-field
|
:value="model.range"
|
||||||
label="Duration"
|
:error-messages="errors.range"
|
||||||
:value="model.duration"
|
@change="change('range', ...arguments)"
|
||||||
:error-messages="errors.duration"
|
/>
|
||||||
@change="change('duration', ...arguments)"
|
</v-col>
|
||||||
/>
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<text-field
|
||||||
|
label="Duration"
|
||||||
|
:value="model.duration"
|
||||||
|
:error-messages="errors.duration"
|
||||||
|
@change="change('duration', ...arguments)"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row class="mt-0">
|
||||||
|
<v-col
|
||||||
|
cols="6"
|
||||||
|
md="3"
|
||||||
|
class="pt-1"
|
||||||
|
>
|
||||||
|
<smart-checkbox
|
||||||
|
label="Verbal"
|
||||||
|
:value="model.verbal"
|
||||||
|
:error-messages="errors.verbal"
|
||||||
|
@change="change('verbal', ...arguments)"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
cols="6"
|
||||||
|
md="3"
|
||||||
|
class="pt-1"
|
||||||
|
>
|
||||||
|
<smart-checkbox
|
||||||
|
label="Somatic"
|
||||||
|
:value="model.somatic"
|
||||||
|
:error-messages="errors.somatic"
|
||||||
|
@change="change('somatic', ...arguments)"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
cols="6"
|
||||||
|
md="3"
|
||||||
|
class="pt-1"
|
||||||
|
>
|
||||||
|
<smart-checkbox
|
||||||
|
label="Concentration"
|
||||||
|
:value="model.concentration"
|
||||||
|
:error-messages="errors.concentration"
|
||||||
|
@change="change('concentration', ...arguments)"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
cols="6"
|
||||||
|
md="3"
|
||||||
|
class="pt-1"
|
||||||
|
>
|
||||||
|
<smart-checkbox
|
||||||
|
label="Ritual"
|
||||||
|
:value="model.ritual"
|
||||||
|
:error-messages="errors.ritual"
|
||||||
|
@change="change('ritual', ...arguments)"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row dense>
|
||||||
|
<v-col cols="12">
|
||||||
|
<text-field
|
||||||
|
label="Material"
|
||||||
|
:value="model.material"
|
||||||
|
:error-messages="errors.material"
|
||||||
|
@change="change('material', ...arguments)"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row dense>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<smart-select
|
||||||
|
label="Target"
|
||||||
|
:items="targetOptions"
|
||||||
|
:value="model.target"
|
||||||
|
:error-messages="errors.target"
|
||||||
|
:menu-props="{auto: true, lazy: true}"
|
||||||
|
@change="change('target', ...arguments)"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<v-slide-x-transition mode="out-in">
|
||||||
|
<v-switch
|
||||||
|
v-if="!isAttack"
|
||||||
|
label="Attack roll"
|
||||||
|
:value="attackSwitch"
|
||||||
|
@change="e => attackSwitch = e"
|
||||||
|
/>
|
||||||
|
<computed-field
|
||||||
|
v-else
|
||||||
|
label="To Hit"
|
||||||
|
prefix="1d20 + "
|
||||||
|
hint="The bonus to attack if this action has an attack roll"
|
||||||
|
:model="model.attackRoll"
|
||||||
|
:error-messages="errors.attackRoll"
|
||||||
|
@change="({path, value, ack}) =>
|
||||||
|
$emit('change', {path: ['attackRoll', ...path], value, ack})"
|
||||||
|
/>
|
||||||
|
</v-slide-x-transition>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
<inline-computation-field
|
<inline-computation-field
|
||||||
label="Description"
|
label="Description"
|
||||||
:model="model.description"
|
:model="model.description"
|
||||||
@@ -105,24 +229,69 @@
|
|||||||
@change="({path, value, ack}) =>
|
@change="({path, value, ack}) =>
|
||||||
$emit('change', {path: ['description', ...path], value, ack})"
|
$emit('change', {path: ['description', ...path], value, ack})"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<smart-combobox
|
|
||||||
label="Tags"
|
|
||||||
multiple
|
|
||||||
chips
|
|
||||||
deletable-chips
|
|
||||||
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
|
||||||
:value="model.tags"
|
|
||||||
:error-messages="errors.tags"
|
|
||||||
@change="change('tags', ...arguments)"
|
|
||||||
/>
|
|
||||||
<form-sections>
|
<form-sections>
|
||||||
|
<form-section name="Resources">
|
||||||
|
<resources-form
|
||||||
|
:model="model.resources"
|
||||||
|
@change="({path, value, ack}) => $emit('change', {path: ['resources', ...path], value, ack})"
|
||||||
|
@push="({path, value, ack}) => $emit('push', {path: ['resources', ...path], value, ack})"
|
||||||
|
@pull="({path, ack}) => $emit('pull', {path: ['resources', ...path], ack})"
|
||||||
|
/>
|
||||||
|
</form-section>
|
||||||
<form-section
|
<form-section
|
||||||
name="Casting"
|
name="Limit Uses"
|
||||||
>
|
>
|
||||||
<action-form
|
<v-row dense>
|
||||||
v-bind="$props"
|
<v-col
|
||||||
v-on="$listeners"
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<computed-field
|
||||||
|
label="Uses"
|
||||||
|
hint="How many times this action can be used before needing to be reset"
|
||||||
|
class="mr-2"
|
||||||
|
:model="model.uses"
|
||||||
|
:error-messages="errors.uses"
|
||||||
|
@change="({path, value, ack}) =>
|
||||||
|
$emit('change', {path: ['uses', ...path], value, ack})"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
md="6"
|
||||||
|
>
|
||||||
|
<text-field
|
||||||
|
label="Uses used"
|
||||||
|
type="number"
|
||||||
|
hint="How many times this action has already been used: should be 0 in most cases"
|
||||||
|
style="flex-basis: 300px;"
|
||||||
|
:value="model.usesUsed"
|
||||||
|
:error-messages="errors.uses"
|
||||||
|
@change="change('usesUsed', ...arguments)"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<smart-select
|
||||||
|
label="Reset"
|
||||||
|
clearable
|
||||||
|
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>
|
||||||
|
<form-section name="Advanced">
|
||||||
|
<smart-combobox
|
||||||
|
label="Tags"
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
|
deletable-chips
|
||||||
|
hint="Used to let slots find this property in a library, should otherwise be left blank"
|
||||||
|
:value="model.tags"
|
||||||
|
@change="change('tags', ...arguments)"
|
||||||
/>
|
/>
|
||||||
</form-section>
|
</form-section>
|
||||||
</form-sections>
|
</form-sections>
|
||||||
@@ -131,14 +300,16 @@
|
|||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import FormSection, { FormSections } from '/imports/ui/properties/forms/shared/FormSection.vue';
|
import FormSection, { FormSections } from '/imports/ui/properties/forms/shared/FormSection.vue';
|
||||||
import ActionForm from '/imports/ui/properties/forms/ActionForm.vue'
|
|
||||||
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
|
||||||
|
import IconColorMenu from '/imports/ui/properties/forms/shared/IconColorMenu.vue';
|
||||||
|
import ResourcesForm from '/imports/ui/properties/forms/ResourcesForm.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FormSections,
|
FormSections,
|
||||||
FormSection,
|
FormSection,
|
||||||
ActionForm,
|
IconColorMenu,
|
||||||
|
ResourcesForm,
|
||||||
},
|
},
|
||||||
mixins: [propertyFormMixin],
|
mixins: [propertyFormMixin],
|
||||||
data(){return {
|
data(){return {
|
||||||
@@ -202,13 +373,39 @@
|
|||||||
value: 9,
|
value: 9,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
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,
|
||||||
};},
|
};},
|
||||||
|
computed: {
|
||||||
|
isAttack(){
|
||||||
|
return this.attackSwitch || !!this.model.attackRoll?.calculation
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
.v-input--checkbox {
|
.v-input--checkbox {
|
||||||
flex-grow: 0;
|
margin-top: 0;
|
||||||
width: 200px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,45 +1,23 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<v-menu offset-y>
|
<div
|
||||||
<template #activator="{ on, attrs }">
|
class="d-flex justify-center flex-wrap"
|
||||||
<v-badge
|
>
|
||||||
icon="mdi-pencil"
|
<div class="mx-1">
|
||||||
overlap
|
<color-picker
|
||||||
>
|
label="Color"
|
||||||
<v-btn
|
:value="model.color"
|
||||||
icon
|
@input="value =>$emit('change', {path: ['color'], value})"
|
||||||
:color="model.color"
|
/>
|
||||||
outlined
|
</div>
|
||||||
v-bind="attrs"
|
<div class="mx-1">
|
||||||
v-on="on"
|
<icon-picker
|
||||||
>
|
label="Icon"
|
||||||
<property-icon
|
:value="model.icon"
|
||||||
:model="model"
|
:error-messages="errors.icon"
|
||||||
:color="model.color"
|
@change="(value, ack) =>$emit('change', {path: ['icon'], value, ack})"
|
||||||
/>
|
/>
|
||||||
</v-btn>
|
</div>
|
||||||
</v-badge>
|
</div>
|
||||||
</template>
|
|
||||||
<v-list>
|
|
||||||
<v-list-item>
|
|
||||||
<v-list-item-title>
|
|
||||||
<icon-picker
|
|
||||||
label="Icon"
|
|
||||||
:value="model.icon"
|
|
||||||
:error-messages="errors.icon"
|
|
||||||
@change="(value, ack) =>$emit('change', {path: ['icon'], value, ack})"
|
|
||||||
/>
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
<v-list-item>
|
|
||||||
<v-list-item-title>
|
|
||||||
<color-picker
|
|
||||||
:value="model.color"
|
|
||||||
@input="value =>$emit('change', {path: ['color'], value})"
|
|
||||||
/>
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
v-if="model.icon"
|
v-if="model.icon"
|
||||||
:shape="model.icon.shape"
|
:shape="model.icon.shape"
|
||||||
:color="color"
|
:color="color"
|
||||||
|
:class="{disabled}"
|
||||||
/>
|
/>
|
||||||
<v-icon
|
<v-icon
|
||||||
v-else
|
v-else
|
||||||
:color="color"
|
:color="color"
|
||||||
|
:class="{disabled}"
|
||||||
>
|
>
|
||||||
{{ icon }}
|
{{ icon }}
|
||||||
</v-icon>
|
</v-icon>
|
||||||
@@ -25,6 +27,7 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
},
|
},
|
||||||
|
disabled: Boolean,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
icon(){
|
icon(){
|
||||||
@@ -33,3 +36,9 @@ export default {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.svg-icon.disabled, .v-icon.disabled {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
:is="treeNodeView"
|
:is="treeNodeView"
|
||||||
:model="model"
|
:model="model"
|
||||||
:selected="selected"
|
:selected="selected"
|
||||||
|
:class="{
|
||||||
|
'inactive': model.inactive,
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -29,3 +32,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.inactive {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
>
|
>
|
||||||
<property-field
|
<property-field
|
||||||
v-if="context.creatureId"
|
v-if="context.creatureId"
|
||||||
name="Apply action"
|
:name="model.type === 'spell'? 'Cast spell' : 'Apply action'"
|
||||||
center
|
center
|
||||||
>
|
>
|
||||||
<v-btn
|
<v-btn
|
||||||
outlined
|
outlined
|
||||||
style="font-size: 18px;"
|
style="font-size: 18px;"
|
||||||
class="ma-2"
|
class="ma-2"
|
||||||
|
data-id="do-action-button"
|
||||||
:color="model.color || 'primary'"
|
:color="model.color || 'primary'"
|
||||||
icon
|
icon
|
||||||
:loading="doActionLoading"
|
:loading="doActionLoading"
|
||||||
@@ -109,12 +110,13 @@
|
|||||||
|
|
||||||
<script lang="js">
|
<script lang="js">
|
||||||
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js';
|
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js';
|
||||||
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
|
|
||||||
import doAction from '/imports/api/engine/actions/doAction.js';
|
import doAction from '/imports/api/engine/actions/doAction.js';
|
||||||
import AttributeConsumedView from '/imports/ui/properties/components/actions/AttributeConsumedView.vue';
|
import AttributeConsumedView from '/imports/ui/properties/components/actions/AttributeConsumedView.vue';
|
||||||
import ItemConsumedView from '/imports/ui/properties/components/actions/ItemConsumedView.vue';
|
import ItemConsumedView from '/imports/ui/properties/components/actions/ItemConsumedView.vue';
|
||||||
import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
|
import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
|
||||||
import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js';
|
import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js';
|
||||||
|
import doCastSpell from '/imports/api/engine/actions/doCastSpell.js';
|
||||||
|
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -173,13 +175,33 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
doAction(){
|
doAction(){
|
||||||
this.doActionLoading = true;
|
if (this.model.type === 'action'){
|
||||||
doAction.call({actionId: this.model._id}, error => {
|
this.doActionLoading = true;
|
||||||
this.doActionLoading = false;
|
doAction.call({actionId: this.model._id}, error => {
|
||||||
if (error){
|
this.doActionLoading = false;
|
||||||
console.error(error);
|
if (error){
|
||||||
}
|
snackbar({text: error.reason});
|
||||||
});
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (this.model.type === 'spell') {
|
||||||
|
this.$store.commit('pushDialogStack', {
|
||||||
|
component: 'cast-spell-with-slot-dialog',
|
||||||
|
elementId: 'do-action-button',
|
||||||
|
data: {
|
||||||
|
creatureId: this.context.creatureId,
|
||||||
|
spellId: this.model._id,
|
||||||
|
},
|
||||||
|
callback({spellId, slotId} = {}){
|
||||||
|
if (!spellId) return;
|
||||||
|
doCastSpell.call({spellId, slotId}, error => {
|
||||||
|
if (!error) return;
|
||||||
|
snackbar({text: error.reason});
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
resetUses(){
|
resetUses(){
|
||||||
updateCreatureProperty.call({
|
updateCreatureProperty.call({
|
||||||
|
|||||||
@@ -31,12 +31,9 @@
|
|||||||
tile
|
tile
|
||||||
color="primary"
|
color="primary"
|
||||||
:value="model.value"
|
:value="model.value"
|
||||||
|
:loading="damagePropertyLoading"
|
||||||
@change="damageProperty"
|
@change="damageProperty"
|
||||||
>
|
/>
|
||||||
<v-icon>
|
|
||||||
$vuetify.icons.abacus
|
|
||||||
</v-icon>
|
|
||||||
</increment-button>
|
|
||||||
</property-field>
|
</property-field>
|
||||||
<property-field
|
<property-field
|
||||||
v-if="model.modifier !== undefined"
|
v-if="model.modifier !== undefined"
|
||||||
@@ -144,6 +141,7 @@
|
|||||||
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||||
import IncrementButton from '/imports/ui/components/IncrementButton.vue';
|
import IncrementButton from '/imports/ui/components/IncrementButton.vue';
|
||||||
import getProficiencyIcon from '/imports/ui/utility/getProficiencyIcon.js';
|
import getProficiencyIcon from '/imports/ui/utility/getProficiencyIcon.js';
|
||||||
|
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -172,6 +170,7 @@
|
|||||||
0.5: 'Half proficiency bonus rounded up',
|
0.5: 'Half proficiency bonus rounded up',
|
||||||
2: 'Double proficiency bonus',
|
2: 'Double proficiency bonus',
|
||||||
},
|
},
|
||||||
|
damagePropertyLoading: false,
|
||||||
}},
|
}},
|
||||||
computed: {
|
computed: {
|
||||||
reset(){
|
reset(){
|
||||||
@@ -197,10 +196,17 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
damageProperty({type, value}) {
|
damageProperty({type, value}) {
|
||||||
|
this.damagePropertyLoading = true;
|
||||||
damageProperty.call({
|
damageProperty.call({
|
||||||
_id: this.model._id,
|
_id: this.model._id,
|
||||||
operation: type,
|
operation: type,
|
||||||
value: value
|
value: value
|
||||||
|
}, error => {
|
||||||
|
this.damagePropertyLoading = false;
|
||||||
|
if (error){
|
||||||
|
snackbar({text: error.reason});
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,12 @@
|
|||||||
mono
|
mono
|
||||||
:value="model.variableName"
|
:value="model.variableName"
|
||||||
/>
|
/>
|
||||||
|
<property-field
|
||||||
|
v-if="!context.creatureId"
|
||||||
|
name="Condition"
|
||||||
|
mono
|
||||||
|
:value="model.slotFillerCondition"
|
||||||
|
/>
|
||||||
<property-description
|
<property-description
|
||||||
name="Description"
|
name="Description"
|
||||||
:model="model.description"
|
:model="model.description"
|
||||||
|
|||||||
@@ -24,13 +24,15 @@
|
|||||||
name="Targeted tags"
|
name="Targeted tags"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<v-chip
|
<div class="d-flex flex-wrap">
|
||||||
v-for="(tag, index) in model.targetTags"
|
<v-chip
|
||||||
:key="index"
|
v-for="(tag, index) in model.targetTags"
|
||||||
class="ma-1"
|
:key="index"
|
||||||
>
|
class="ma-1"
|
||||||
{{ tag }}
|
>
|
||||||
</v-chip>
|
{{ tag }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-for="ex in model.extraTags"
|
v-for="ex in model.extraTags"
|
||||||
:key="ex._id"
|
:key="ex._id"
|
||||||
@@ -38,13 +40,15 @@
|
|||||||
<span class="ma-2">
|
<span class="ma-2">
|
||||||
{{ ex.operation }}
|
{{ ex.operation }}
|
||||||
</span>
|
</span>
|
||||||
<v-chip
|
<div class="d-flex flex-wrap">
|
||||||
v-for="(extraTag, index) in ex.tags"
|
<v-chip
|
||||||
:key="index"
|
v-for="(extraTag, index) in ex.tags"
|
||||||
class="ma-1"
|
:key="index"
|
||||||
>
|
class="ma-1"
|
||||||
{{ extraTag }}
|
>
|
||||||
</v-chip>
|
{{ extraTag }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</property-field>
|
</property-field>
|
||||||
@@ -52,13 +56,15 @@
|
|||||||
v-else
|
v-else
|
||||||
name="Stats"
|
name="Stats"
|
||||||
>
|
>
|
||||||
<v-chip
|
<div class="d-flex flex-wrap">
|
||||||
v-for="(stat, index) in model.stats"
|
<v-chip
|
||||||
:key="index"
|
v-for="(stat, index) in model.stats"
|
||||||
class="ma-1"
|
:key="index"
|
||||||
>
|
class="ma-1"
|
||||||
{{ stat }}
|
>
|
||||||
</v-chip>
|
{{ stat }}
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
</property-field>
|
</property-field>
|
||||||
<property-field
|
<property-field
|
||||||
v-if="model.operation === 'conditional'"
|
v-if="model.operation === 'conditional'"
|
||||||
|
|||||||
@@ -15,11 +15,10 @@
|
|||||||
large
|
large
|
||||||
outlined
|
outlined
|
||||||
color="primary"
|
color="primary"
|
||||||
|
:loading="incrementLoading"
|
||||||
:value="model.quantity"
|
:value="model.quantity"
|
||||||
@change="changeQuantity"
|
@change="changeQuantity"
|
||||||
>
|
/>
|
||||||
<v-icon>$vuetify.icons.abacus</v-icon>
|
|
||||||
</increment-button>
|
|
||||||
</property-field>
|
</property-field>
|
||||||
<property-field
|
<property-field
|
||||||
v-if="model.value !== undefined"
|
v-if="model.value !== undefined"
|
||||||
@@ -152,6 +151,7 @@ import CoinValue from '/imports/ui/components/CoinValue.vue';
|
|||||||
import IncrementButton from '/imports/ui/components/IncrementButton.vue';
|
import IncrementButton from '/imports/ui/components/IncrementButton.vue';
|
||||||
import adjustQuantity from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
import adjustQuantity from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
||||||
import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js';
|
import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js';
|
||||||
|
import {snackbar} from '/imports/ui/components/snackbars/SnackbarQueue.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components:{
|
components:{
|
||||||
@@ -162,6 +162,9 @@ export default {
|
|||||||
inject: {
|
inject: {
|
||||||
context: { default: {} }
|
context: { default: {} }
|
||||||
},
|
},
|
||||||
|
data(){return {
|
||||||
|
incrementLoading: false,
|
||||||
|
}},
|
||||||
computed:{
|
computed:{
|
||||||
totalValue(){
|
totalValue(){
|
||||||
return stripFloatingPointOddities(this.model.value * this.model.quantity);
|
return stripFloatingPointOddities(this.model.value * this.model.quantity);
|
||||||
@@ -182,12 +185,19 @@ export default {
|
|||||||
return SVG_ICONS[name];
|
return SVG_ICONS[name];
|
||||||
},
|
},
|
||||||
changeQuantity({type, value}) {
|
changeQuantity({type, value}) {
|
||||||
|
this.incrementLoading = true;
|
||||||
adjustQuantity.call({
|
adjustQuantity.call({
|
||||||
_id: this.model._id,
|
_id: this.model._id,
|
||||||
operation: type,
|
operation: type,
|
||||||
value: value
|
value: value
|
||||||
|
}, error => {
|
||||||
|
this.incrementLoading = false;
|
||||||
|
if (error){
|
||||||
|
snackbar({text: error.reason});
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -42,6 +42,11 @@
|
|||||||
name="Skill type"
|
name="Skill type"
|
||||||
:value="skillTypes[model.skillType]"
|
:value="skillTypes[model.skillType]"
|
||||||
/>
|
/>
|
||||||
|
<property-field
|
||||||
|
v-if="'passiveBonus' in model"
|
||||||
|
name="Passive score"
|
||||||
|
:value="passiveScore"
|
||||||
|
/>
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-row dense>
|
<v-row dense>
|
||||||
<property-description
|
<property-description
|
||||||
@@ -164,6 +169,9 @@ export default {
|
|||||||
icon(){
|
icon(){
|
||||||
return getProficiencyIcon(this.model.proficiency);
|
return getProficiencyIcon(this.model.proficiency);
|
||||||
},
|
},
|
||||||
|
passiveScore(){
|
||||||
|
return 10 + this.model.value + this.model.passiveBonus;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
numberToSignedString,
|
numberToSignedString,
|
||||||
@@ -207,6 +215,7 @@ export default {
|
|||||||
stats: this.model.variableName,
|
stats: this.model.variableName,
|
||||||
type: 'effect',
|
type: 'effect',
|
||||||
removed: {$ne: true},
|
removed: {$ne: true},
|
||||||
|
inactive: {$ne: true},
|
||||||
}).fetch();
|
}).fetch();
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
:value="model.slotQuantityFilled"
|
:value="model.slotQuantityFilled"
|
||||||
/>
|
/>
|
||||||
<property-field
|
<property-field
|
||||||
v-if="context.creatureId"
|
v-if="!context.creatureId"
|
||||||
name="Condition"
|
name="Condition"
|
||||||
mono
|
mono
|
||||||
:value="model.slotFillerCondition"
|
:value="model.slotFillerCondition"
|
||||||
@@ -28,10 +28,14 @@
|
|||||||
/>
|
/>
|
||||||
</property-field>
|
</property-field>
|
||||||
<property-field
|
<property-field
|
||||||
|
v-if="model.description"
|
||||||
name="Description"
|
name="Description"
|
||||||
:cols="{cols: 12}"
|
:cols="{cols: 12}"
|
||||||
:value="model.description"
|
>
|
||||||
/>
|
<markdown-text
|
||||||
|
:markdown="model.description"
|
||||||
|
/>
|
||||||
|
</property-field>
|
||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -39,8 +43,12 @@
|
|||||||
<script lang="js">
|
<script lang="js">
|
||||||
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js';
|
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js';
|
||||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||||
|
import MarkdownText from '/imports/ui/components/MarkdownText.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
MarkdownText,
|
||||||
|
},
|
||||||
mixins: [propertyViewerMixin],
|
mixins: [propertyViewerMixin],
|
||||||
inject: {
|
inject: {
|
||||||
context: { default: {} }
|
context: { default: {} }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template lang="html">
|
<template lang="html">
|
||||||
<markdown-text
|
<markdown-text
|
||||||
v-if="text"
|
v-if="text && model"
|
||||||
:markdown="model.value || model.text"
|
:markdown="model.value || model.text"
|
||||||
/>
|
/>
|
||||||
<property-field
|
<property-field
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
export default function numberToSignedString(number){
|
export default function numberToSignedString(number, spaced){
|
||||||
if (typeof number !== 'number') return number;
|
if (typeof number !== 'number') return number;
|
||||||
if (number === 0){
|
if (number === 0){
|
||||||
return '+0';
|
return spaced ? '+ 0' : '+0';
|
||||||
} else if (number > 0){
|
} else if (number > 0){
|
||||||
return `+${number}`;
|
return spaced ? `+ ${number}` : `+${number}`;
|
||||||
} else {
|
} else {
|
||||||
return `${number}`;
|
return spaced ? `- ${Math.abs(number) || number}` : `${number}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Vuetify from 'vuetify/lib';
|
import Vuetify from 'vuetify/lib';
|
||||||
import { Scroll } from 'vuetify/lib/directives';
|
import { Scroll, Ripple } from 'vuetify/lib/directives';
|
||||||
import SVG_ICONS from '/imports/constants/SVG_ICONS.js';
|
import SVG_ICONS from '/imports/constants/SVG_ICONS.js';
|
||||||
import SvgIconByName from '/imports/ui/icons/SvgIconByName.vue';
|
import SvgIconByName from '/imports/ui/icons/SvgIconByName.vue';
|
||||||
import themes from '/imports/ui/themes.js';
|
import themes from '/imports/ui/themes.js';
|
||||||
@@ -9,6 +9,7 @@ import minifyTheme from 'minify-css-string';
|
|||||||
Vue.use(Vuetify, {
|
Vue.use(Vuetify, {
|
||||||
directives: {
|
directives: {
|
||||||
Scroll,
|
Scroll,
|
||||||
|
Ripple,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
35
app/package-lock.json
generated
35
app/package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dicecloud",
|
"name": "dicecloud",
|
||||||
"version": "0.10.0",
|
"version": "2.0.33",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2399,15 +2399,23 @@
|
|||||||
"ngraph.events": "^1.2.1"
|
"ngraph.events": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ngraph.path": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ngraph.path/-/ngraph.path-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-yJZay4tP0wcjqkkf8zlMQ/T+JOgU+EWfdE4w4TG8OS94B12J/+Z44UOYxVJErE8E6/wFunX1hMZEB1/GHsBYHg=="
|
||||||
|
},
|
||||||
"node-addon-api": {
|
"node-addon-api": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.1.0.tgz",
|
||||||
"integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw=="
|
"integrity": "sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw=="
|
||||||
},
|
},
|
||||||
"node-fetch": {
|
"node-fetch": {
|
||||||
"version": "2.6.1",
|
"version": "2.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||||
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
|
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||||
|
"requires": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"nopt": {
|
"nopt": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
@@ -3041,6 +3049,11 @@
|
|||||||
"punycode": "^2.1.1"
|
"punycode": "^2.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
|
||||||
|
},
|
||||||
"tunnel-agent": {
|
"tunnel-agent": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||||
@@ -3216,6 +3229,20 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.2.tgz",
|
||||||
"integrity": "sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw=="
|
"integrity": "sha512-ETW44IqCgBpVomy520DT5jf8n0zoCac+sxWnn+hMe/CzaSejb/eVw2YToiXYX+Ex/AuHHia28vWTq4goAexFbw=="
|
||||||
},
|
},
|
||||||
|
"webidl-conversions": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
"integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE="
|
||||||
|
},
|
||||||
|
"whatwg-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
|
||||||
|
"requires": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"which": {
|
"which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dicecloud",
|
"name": "dicecloud",
|
||||||
"version": "0.10.0",
|
"version": "2.0.33",
|
||||||
"description": "Unofficial Online Realtime D&D 5e App",
|
"description": "Unofficial Online Realtime D&D 5e App",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"test": "meteor test --driver-package meteortesting:mocha --port 3001"
|
"test": "meteor test --driver-package meteortesting:mocha --port 3001"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "12.16.x",
|
"node": "14.0.x",
|
||||||
"npm": "6.13.x"
|
"npm": "6.13.x"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
"moo": "^0.5.1",
|
"moo": "^0.5.1",
|
||||||
"nearley": "^2.19.1",
|
"nearley": "^2.19.1",
|
||||||
"ngraph.graph": "^19.1.0",
|
"ngraph.graph": "^19.1.0",
|
||||||
|
"ngraph.path": "^1.4.0",
|
||||||
"qrcode": "^1.5.0",
|
"qrcode": "^1.5.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"simpl-schema": "^1.12.0",
|
"simpl-schema": "^1.12.0",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import '/imports/server/publications/index.js';
|
|||||||
import '/imports/server/cron/deleteSoftRemovedDocuments.js';
|
import '/imports/server/cron/deleteSoftRemovedDocuments.js';
|
||||||
import '/imports/api/parenting/organizeMethods.js';
|
import '/imports/api/parenting/organizeMethods.js';
|
||||||
import '/imports/api/users/patreon/updatePatreonOnLogin.js';
|
import '/imports/api/users/patreon/updatePatreonOnLogin.js';
|
||||||
|
import '/imports/api/engine/actions/index.js';
|
||||||
import '/imports/migrations/server/index.js';
|
import '/imports/migrations/server/index.js';
|
||||||
import '/imports/migrations/methods/index.js'
|
import '/imports/migrations/methods/index.js'
|
||||||
import '/imports/constants/MAINTENANCE_MODE.js';
|
import '/imports/constants/MAINTENANCE_MODE.js';
|
||||||
|
|||||||
Reference in New Issue
Block a user