Compare commits

...

44 Commits

Author SHA1 Message Date
Stefan Zermatten
b876c2801d Greyed out inactive props in the tree 2022-03-04 12:51:48 +02:00
Stefan Zermatten
698c9c7bbf Fixed adjustment error when trying to adjust a property that isn't set 2022-03-04 12:51:28 +02:00
Stefan Zermatten
7544243640 Fixed buffs not crystalising variables correctly 2022-03-04 12:51:06 +02:00
Stefan Zermatten
4b4e3a8928 Improve hover highlight UI effects for cards in dark mode
In light mode a change in elevation changes the drop shadow, but this is 
all but invisible in dark mode, so I added a highlight to the cards when 
hovering to show that the card can be expanded with a click
2022-03-03 17:21:59 +02:00
Stefan Zermatten
92a588bfcc Added slotFillerCondition field to class levels, same as in slot filler 2022-03-03 16:19:54 +02:00
Stefan Zermatten
43e956eb6a Fixed skills not obeying baseValue correctly 2022-03-03 15:55:07 +02:00
Stefan Zermatten
c4429f5dd7 Item quantity adjustment button now shows loading while in progress 2022-03-03 15:54:44 +02:00
Stefan Zermatten
4edfe1bcb9 Fixed inventory calculation to include item quantities 2022-03-03 15:53:58 +02:00
Stefan Zermatten
473a9f0253 Inlined a bunch of discord webhook text to help format messages better 2022-02-28 16:55:15 +02:00
Stefan Zermatten
94cdca4f31 Fixed uses left not logging correctly in actions 2022-02-28 16:25:42 +02:00
Stefan Zermatten
10d0a3f763 Added attack roll back to spell form 2022-02-28 16:13:52 +02:00
Stefan Zermatten
afe6c044cd Fixed dependency graph not building correctly for resources consumed 2022-02-28 00:02:55 +02:00
Stefan Zermatten
e6c7d79d7d Overhauled spell casting UX 2022-02-27 22:14:32 +02:00
Stefan Zermatten
49fa9cc470 Fixed parser to allow $ and x.0.thing in variable references 2022-02-26 19:36:56 +02:00
Stefan Zermatten
3646c13355 Merge branch 'version-2-dev' into version-2 2022-02-26 17:35:58 +02:00
Stefan Zermatten
27665e0bdc Finished roll check and roll attack buttons from stats page 2022-02-26 17:35:26 +02:00
Stefan Zermatten
fea29e60b7 Fixed inactive effects showing up on skill detail view 2022-02-26 15:21:08 +02:00
Stefan Zermatten
653f05012a Reversed the order of the creature compute dependency graph traversal
By doing this the traversal happens mostly in tree order, which is a 
better assumption of starting point in cases where there are dependency 
loops
2022-02-26 14:58:38 +02:00
Stefan Zermatten
7ee4a22d77 Fixed error where dependency loops including classLevels break the sheet 2022-02-26 13:06:00 +02:00
Stefan Zermatten
59c69a46a8 Attacks can now be rolled with advantage from the stats tab
TODO the action viewer as well still
2022-02-25 13:44:09 +02:00
Stefan Zermatten
f79a6d98ec Updated meteor 2022-02-25 12:27:59 +02:00
Stefan Zermatten
0ffa736143 Fixed dbv1 migration to match applied data patches 2022-02-25 12:27:52 +02:00
Stefan Zermatten
f1b4071c46 Inline calculation fields now reduce 2022-02-25 12:27:26 +02:00
Stefan Zermatten
249ece352c Fixed missing slot filler description 2022-02-25 10:28:09 +02:00
Stefan Zermatten
4fe3f30090 Merge branch 'version-2' of https://github.com/ThaumRystra/DiceCloud into version-2 2022-02-24 10:59:16 +02:00
Stefan Zermatten
44d3fbc065 Fixed slot filler viewer not having markdown for the description 2022-02-24 10:59:02 +02:00
Stefan Zermatten
b1feb126df Fixed inventory weight and value fields 2022-02-24 02:39:39 +02:00
Stefan Zermatten
69f9636688 Fixed spell lists and class levels not computing inline calculations 2022-02-23 17:01:12 +02:00
Stefan Zermatten
5383804af7 Fixed error with damage failing to apply if existing damage was undefined 2022-02-23 16:17:34 +02:00
Stefan Zermatten
0b8c88daef Began work on buttons to make rolls from the sheet 2022-02-23 16:08:04 +02:00
Stefan Zermatten
5b6bff91a4 Added resolve function to allow users to force a calculation to reduce 2022-02-23 12:58:12 +02:00
Stefan Zermatten
52453b46e9 Fixed experience not appearing as a variable after computation 2022-02-23 11:44:59 +02:00
Stefan Zermatten
78c67a4fd6 Fixed incorrect use of parser toString in places 2022-02-23 11:07:02 +02:00
Stefan Zermatten
90b277e181 Fixed not operator !working 2022-02-22 19:16:03 +02:00
Stefan Zermatten
dc4d0416a2 Fixed spells disabled by toggles still appearing in spell lists 2022-02-22 19:07:40 +02:00
Stefan Zermatten
12a0dff43f Hacked over ddp error that was not updating removed field correctly 2022-02-22 18:31:06 +02:00
Stefan Zermatten
b9f79f1c51 Fixed buffs missing from stats page 2022-02-22 18:10:04 +02:00
Stefan Zermatten
92d32e7cf8 Fixed tag layout in effect viewer for many tags overlapping one another 2022-02-22 18:07:18 +02:00
Stefan Zermatten
80460ceaed Fixed not found calculation warnings showing [object Object]
They were using the wrong "toString" method
2022-02-22 18:02:57 +02:00
Stefan Zermatten
8f30c1419c Fixed slots and slot fillers not calculating their conditions correctly
Also fixes slot fullness calculation
2022-02-22 17:59:12 +02:00
Stefan Zermatten
4c6d70b084 Fixed . in effect stat targets breaking entire sheet 2022-02-22 17:30:45 +02:00
Stefan Zermatten
ee2b400ee6 Fixed spell list card not showing maxPrepared spells correctly 2022-02-22 16:38:50 +02:00
Stefan Zermatten
ef8aafc1a1 Fixed storagepath for production 2022-02-22 13:03:37 +02:00
Stefan Zermatten
b68637e525 Updated node version 2022-02-22 12:31:47 +02:00
96 changed files with 2208 additions and 727 deletions

1
app/.gitignore vendored
View File

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

View File

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

View File

@@ -1 +1 @@
METEOR@2.6 METEOR@2.6.1

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
}, },
}); });

View File

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

View File

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

View File

@@ -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,
}); });
} }

View File

@@ -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,
}); });
} }

View File

@@ -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);
}); });
}); });
}); });

View File

@@ -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();
} }

View File

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

View File

@@ -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();
}); });

View File

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

View File

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

View 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;

View 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}**`,
});
}

View File

@@ -0,0 +1,2 @@
import './doCastSpell.js';
import './doCheck.js';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
}); });

View File

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

View File

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

View File

@@ -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);
} }
} }

View File

@@ -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);
} }

View File

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

View File

@@ -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'}],
}), }),
]; ];

View File

@@ -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);
} }

View File

@@ -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}});
}
}

View File

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

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ let SpellListSchema = createPropertySchema({
}, },
}); });
const ComputedOnlySpellListSchema = new SimpleSchema({ const ComputedOnlySpellListSchema = createPropertySchema({
description: { description: {
type: 'computedOnlyInlineCalculationField', type: 'computedOnlyInlineCalculationField',
optional: true, optional: true,

View File

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

View File

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

View File

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

View File

@@ -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');
});
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}),

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

View File

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

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

View File

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

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

View File

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

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

View File

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

View File

@@ -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 = {};

View File

@@ -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
); );
}, },
}, },

View File

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

View File

@@ -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);
}); });
}, },
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: {} }
}, },

View File

@@ -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(){

View File

@@ -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);
}
}); });
} }
}, },

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(){

View File

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

View File

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

View File

@@ -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: {} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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);
}
}); });
}, },
}, },

View File

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

View File

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

View File

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

View File

@@ -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 [];

View File

@@ -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: {} }

View File

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

View File

@@ -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}`;
} }
} }

View File

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

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

View File

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

View File

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