diff --git a/app/imports/api/creature/creatures/Creatures.js b/app/imports/api/creature/creatures/Creatures.js index f13932ee..b94fe7e4 100644 --- a/app/imports/api/creature/creatures/Creatures.js +++ b/app/imports/api/creature/creatures/Creatures.js @@ -18,6 +18,11 @@ let CreatureSettingsSchema = new SimpleSchema({ type: Boolean, optional: true, }, + //hide rest buttons + hideRestButtons: { + type: Boolean, + optional: true, + }, // Swap around the modifier and stat swapStatAndModifier: { type: Boolean, diff --git a/app/imports/api/creature/creatures/methods/restCreature.js b/app/imports/api/creature/creatures/methods/restCreature.js index 49d228aa..7ca7c16c 100644 --- a/app/imports/api/creature/creatures/methods/restCreature.js +++ b/app/imports/api/creature/creatures/methods/restCreature.js @@ -49,7 +49,7 @@ const restCreature = new ValidatedMethod({ applyTriggers(afterTriggers, null, actionContext); // Insert log - actionContext.writeLog(); + actionContext.writeLog(); }, }); @@ -57,88 +57,124 @@ function doRestWork(restType, actionContext) { const creatureId = actionContext.creature._id; // Long rests reset short rest properties as well let resetFilter; - if (restType === 'shortRest'){ + if (restType === 'shortRest') { resetFilter = 'shortRest' } else { - resetFilter = {$in: ['shortRest', 'longRest']} + resetFilter = { $in: ['shortRest', 'longRest'] } } + resetProperties(creatureId, resetFilter, actionContext); + + // Reset half hit dice on a long rest, starting with the highest dice + if (restType === 'longRest') { + resetHitDice(creatureId, actionContext); + } +} + +export function resetProperties(creatureId, resetFilter, actionContext) { // Only apply to active properties - let filter = { + const filter = { 'ancestors.id': creatureId, reset: resetFilter, removed: { $ne: true }, inactive: { $ne: true }, }; // update all attribute's damage - filter.type = 'attribute'; - CreatureProperties.update(filter, { + const attributeFilter = { + ...filter, + type: 'attribute', + damage: { $ne: 0 }, + } + CreatureProperties.find(attributeFilter, { + fields: { name: 1, damage: 1 } + }).forEach(prop => { + actionContext.addLog({ + name: prop.name, + value: prop.damage >= 0 ? `Restored ${prop.damage}` : `Removed ${-prop.damage}` + }); + }); + CreatureProperties.update(attributeFilter, { $set: { damage: 0, dirty: true, } }, { - selector: {type: 'attribute'}, + selector: { type: 'attribute' }, multi: true, }); // Update all action-like properties' usesUsed - filter.type = {$in: [ - 'action', - 'attack', - 'spell' - ]}; - CreatureProperties.update(filter, { + const actionFilter = { + ...filter, + type: { + $in: ['action', 'spell'] + }, + usesUsed: { $ne: 0 }, + }; + CreatureProperties.find(actionFilter, { + fields: { name: 1, usesUsed: 1 } + }).forEach(prop => { + actionContext.addLog({ + name: prop.name, + value: prop.usesUsed >= 0 ? `Restored ${prop.usesUsed} uses` : `Removed ${-prop.usesUsed} uses` + }); + }); + CreatureProperties.update(actionFilter, { $set: { usesUsed: 0, dirty: true, } }, { - selector: {type: 'action'}, + selector: { type: 'action' }, multi: true, }); - // Reset half hit dice on a long rest, starting with the highest dice - if (restType === 'longRest'){ - let hitDice = CreatureProperties.find({ - 'ancestors.id': creatureId, - type: 'attribute', - attributeType: 'hitDice', - removed: {$ne: true}, - inactive: {$ne: true}, - }, { - fields: { - hitDiceSize: 1, - damage: 1, - total: 1, +} + +function resetHitDice(creatureId, actionContext) { + let hitDice = CreatureProperties.find({ + 'ancestors.id': creatureId, + type: 'attribute', + attributeType: 'hitDice', + removed: { $ne: true }, + inactive: { $ne: true }, + }, { + fields: { + name: 1, + hitDiceSize: 1, + damage: 1, + total: 1, + } + }).fetch(); + // Use a collator to do sorting in natural order + let collator = new Intl.Collator('en', { + numeric: true, sensitivity: 'base' + }); + // Get the hit dice in decending order of hitDiceSize + let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize) + hitDice.sort(compare); + // Get the total number of hit dice that can be recovered this rest + let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0); + let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5; + let recoverableHd = Math.max(Math.floor(totalHd * resetMultiplier), 1); + // recover each hit dice in turn until the recoverable amount is used up + let amountToRecover, resultingDamage; + hitDice.forEach(hd => { + if (!recoverableHd) return; + amountToRecover = Math.min(recoverableHd, hd.damage || 0); + if (!amountToRecover) return; + recoverableHd -= amountToRecover; + resultingDamage = hd.damage - amountToRecover; + actionContext.addLog({ + name: hd.name, + value: amountToRecover >= 0 ? `Restored ${amountToRecover} hit dice` : `Removed ${-amountToRecover} hit dice` + }); + CreatureProperties.update(hd._id, { + $set: { + damage: resultingDamage, + dirty: true, } - }).fetch(); - // Use a collator to do sorting in natural order - let collator = new Intl.Collator('en', { - numeric: true, sensitivity: 'base' + }, { + selector: { type: 'attribute' }, }); - // Get the hit dice in decending order of hitDiceSize - let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize) - hitDice.sort(compare); - // Get the total number of hit dice that can be recovered this rest - let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0); - let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5; - let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1); - // recover each hit dice in turn until the recoverable amount is used up - let amountToRecover, resultingDamage; - hitDice.forEach(hd => { - if (!recoverableHd) return; - amountToRecover = Math.min(recoverableHd, hd.damage || 0); - if (!amountToRecover) return; - recoverableHd -= amountToRecover; - resultingDamage = hd.damage - amountToRecover; - CreatureProperties.update(hd._id, { - $set: { - damage: resultingDamage, - dirty: true, - } - }, { - selector: {type: 'attribute'}, - }); - }); - } + }); } export default restCreature; diff --git a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js index 8d6bd185..becf0775 100644 --- a/app/imports/api/engine/actions/applyPropertyByType/applyAction.js +++ b/app/imports/api/engine/actions/applyPropertyByType/applyAction.js @@ -7,6 +7,7 @@ import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/met import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import numberToSignedString from '/imports/ui/utility/numberToSignedString.js'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; +import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js'; export default function applyAction(node, actionContext) { applyNodeTriggers(node, 'before', actionContext); @@ -16,7 +17,7 @@ export default function applyAction(node, actionContext) { // Log the name and summary let content = { name: prop.name }; - if (prop.summary?.text){ + if (prop.summary?.text) { recalculateInlineCalculations(prop.summary, actionContext); content.value = prop.summary.value; } @@ -29,24 +30,27 @@ export default function applyAction(node, actionContext) { const attack = prop.attackRoll || prop.attackRollBonus; // Attack if there is an attack roll - if (attack && attack.calculation){ - if (targets.length){ + if (attack && attack.calculation) { + if (targets.length) { targets.forEach(target => { - applyAttackToTarget({attack, target, actionContext}); + applyAttackToTarget({ attack, target, actionContext }); // Apply the children, but only to the current target actionContext.targets = [target]; applyChildren(node, actionContext); }); } else { - applyAttackWithoutTarget({attack, actionContext}); + applyAttackWithoutTarget({ attack, actionContext }); applyChildren(node, actionContext); } } else { applyChildren(node, actionContext); } + if (prop.actionType === 'event' && prop.variableName) { + resetProperties(actionContext.creature._id, prop.variableName, actionContext); + } } -function applyAttackWithoutTarget({attack, actionContext}){ +function applyAttackWithoutTarget({ attack, actionContext }) { delete actionContext.scope['$attackHit']; delete actionContext.scope['$attackMiss']; delete actionContext.scope['$criticalHit']; @@ -62,16 +66,16 @@ function applyAttackWithoutTarget({attack, actionContext}){ criticalMiss, } = rollAttack(attack, scope); let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit'; - if (scope['$attackAdvantage'] === 1){ + if (scope['$attackAdvantage'] === 1) { name += ' (Advantage)'; - } else if(scope['$attackAdvantage'] === -1){ + } else if (scope['$attackAdvantage'] === -1) { name += ' (Disadvantage)'; } - if (!criticalMiss){ - scope['$attackHit'] = {value: true} + if (!criticalMiss) { + scope['$attackHit'] = { value: true } } - if (!criticalHit){ - scope['$attackMiss'] = {value: true}; + if (!criticalHit) { + scope['$attackMiss'] = { value: true }; } actionContext.addLog({ @@ -81,7 +85,7 @@ function applyAttackWithoutTarget({attack, actionContext}){ }); } -function applyAttackToTarget({attack, target, actionContext}){ +function applyAttackToTarget({ attack, target, actionContext }) { const scope = actionContext.scope; delete scope['$attackHit']; delete scope['$attackMiss']; @@ -99,15 +103,15 @@ function applyAttackToTarget({attack, target, actionContext}){ criticalMiss, } = rollAttack(attack, scope); - if (target.variables.armor){ + if (target.variables.armor) { const armor = target.variables.armor.value; let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : - result > armor ? 'Hit!' : 'Miss!'; - if (scope['$attackAdvantage'] === 1){ + result > armor ? 'Hit!' : 'Miss!'; + if (scope['$attackAdvantage'] === 1) { name += ' (Advantage)'; - } else if(scope['$attackAdvantage'] === -1){ + } else if (scope['$attackAdvantage'] === -1) { name += ' (Disadvantage)'; } @@ -116,15 +120,15 @@ function applyAttackToTarget({attack, target, actionContext}){ value: `${resultPrefix}\n**${result}**`, inline: true, }); - if (criticalMiss || result < armor){ - scope['$attackMiss'] = {value: true}; + if (criticalMiss || result < armor) { + scope['$attackMiss'] = { value: true }; } else { - scope['$attackHit'] = {value: true}; + scope['$attackHit'] = { value: true }; } } else { actionContext.addLog({ name: 'Error', - value:'Target has no `armor`', + value: 'Target has no `armor`', }); actionContext.addLog({ name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit', @@ -134,10 +138,10 @@ function applyAttackToTarget({attack, target, actionContext}){ } } -function rollAttack(attack, scope){ +function rollAttack(attack, scope) { const rollModifierText = numberToSignedString(attack.value, true); let value, resultPrefix; - if (scope['$attackAdvantage'] === 1){ + if (scope['$attackAdvantage'] === 1) { const [a, b] = rollDice(2, 20); if (a >= b) { value = a; @@ -146,7 +150,7 @@ function rollAttack(attack, scope){ value = b; resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`; } - } else if (scope['$attackAdvantage'] === -1){ + } else if (scope['$attackAdvantage'] === -1) { const [a, b] = rollDice(2, 20); if (a <= b) { value = a; @@ -159,25 +163,25 @@ function rollAttack(attack, scope){ value = rollDice(1, 20)[0]; resultPrefix = `1d20 [${value}] ${rollModifierText}` } - scope['$attackRoll'] = {value}; + scope['$attackRoll'] = { value }; const result = value + attack.value; - const {criticalHit, criticalMiss} = applyCrits(value, scope); - return {resultPrefix, result, value, criticalHit, criticalMiss}; + const { criticalHit, criticalMiss } = applyCrits(value, scope); + return { resultPrefix, result, value, criticalHit, criticalMiss }; } -function applyCrits(value, scope){ +function applyCrits(value, scope) { let criticalHitTarget = scope.criticalHitTarget?.value || 20; let criticalHit = value >= criticalHitTarget; let criticalMiss; - if (criticalHit){ - scope['$criticalHit'] = {value: true}; + if (criticalHit) { + scope['$criticalHit'] = { value: true }; } else { criticalMiss = value === 1; - if (criticalMiss){ - scope['$criticalMiss'] = {value: true}; + if (criticalMiss) { + scope['$criticalMiss'] = { value: true }; } } - return {criticalHit, criticalMiss}; + return { criticalHit, criticalMiss }; } function applyChildren(node, actionContext) { @@ -185,9 +189,9 @@ function applyChildren(node, actionContext) { node.children.forEach(child => applyProperty(child, actionContext)); } -function spendResources(prop, actionContext){ +function spendResources(prop, actionContext) { // Check Uses - if (prop.usesLeft <= 0){ + if (prop.usesLeft <= 0) { if (!prop.silent) actionContext.addLog({ name: 'Error', value: `${prop.name || 'action'} does not have enough uses left`, @@ -195,7 +199,7 @@ function spendResources(prop, actionContext){ return true; } // Resources - if (prop.insufficientResources){ + if (prop.insufficientResources) { if (!prop.silent) actionContext.addLog({ name: 'Error', value: 'This creature doesn\'t have sufficient resources to perform this action', @@ -209,14 +213,14 @@ function spendResources(prop, actionContext){ try { prop.resources.itemsConsumed.forEach(itemConsumed => { recalculateCalculation(itemConsumed.quantity, actionContext); - if (!itemConsumed.itemId){ + if (!itemConsumed.itemId) { throw 'No ammo was selected for this prop'; } let item = CreatureProperties.findOne(itemConsumed.itemId); - if (!item || item.ancestors[0].id !== prop.ancestors[0].id){ + if (!item || item.ancestors[0].id !== prop.ancestors[0].id) { throw 'The prop\'s ammo was not found on the creature'; } - if (!item.equipped){ + if (!item.equipped) { throw 'The selected ammo is not equipped'; } if ( @@ -229,16 +233,16 @@ function spendResources(prop, actionContext){ value: itemConsumed.quantity.value, }); let logName = item.name; - if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1){ + if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1) { logName = item.plural || logName; } - if (itemConsumed.quantity.value > 0){ + if (itemConsumed.quantity.value > 0) { spendLog.push(logName + ': ' + itemConsumed.quantity.value); - } else if (itemConsumed.quantity.value < 0){ + } else if (itemConsumed.quantity.value < 0) { gainLog.push(logName + ': ' + -itemConsumed.quantity.value); } }); - } catch (e){ + } catch (e) { actionContext.addLog({ name: 'Error', value: e, @@ -251,9 +255,9 @@ function spendResources(prop, actionContext){ itemQuantityAdjustments.forEach(adjustQuantityWork); // Use uses - if (prop.usesLeft){ + if (prop.usesLeft) { CreatureProperties.update(prop._id, { - $inc: {usesUsed: 1} + $inc: { usesUsed: 1 } }, { selector: prop }); @@ -270,7 +274,7 @@ function spendResources(prop, actionContext){ if (!attConsumed.quantity?.value) return; let stat = actionContext.scope[attConsumed.variableName]; - if (!stat){ + if (!stat) { spendLog.push(stat.name + ': ' + ' not found'); return; } @@ -280,9 +284,9 @@ function spendResources(prop, actionContext){ value: attConsumed.quantity.value, actionContext, }); - if (attConsumed.quantity.value > 0){ + if (attConsumed.quantity.value > 0) { spendLog.push(stat.name + ': ' + attConsumed.quantity.value); - } else if (attConsumed.quantity.value < 0){ + } else if (attConsumed.quantity.value < 0) { gainLog.push(stat.name + ': ' + -attConsumed.quantity.value); } }); diff --git a/app/imports/api/engine/computation/computeComputation/computeByType.js b/app/imports/api/engine/computation/computeComputation/computeByType.js index 7597b976..0b5633e3 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType.js @@ -5,6 +5,7 @@ import skill from './computeByType/computeSkill.js'; import pointBuy from './computeByType/computePointBuy.js'; import propertySlot from './computeByType/computeSlot.js'; import container from './computeByType/computeContainer.js'; +import spellList from './computeByType/computeSpellList.js'; import _calculation from './computeByType/computeCalculation.js'; export default Object.freeze({ @@ -17,4 +18,5 @@ export default Object.freeze({ pointBuy, propertySlot, spell: action, + spellList, }); diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeSpellList.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeSpellList.js new file mode 100644 index 00000000..63dc6003 --- /dev/null +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeSpellList.js @@ -0,0 +1,6 @@ +export default function computeSpelllist(computation, node) { + const prop = node.data; + + const ability = computation.scope[prop.ability]; + prop.abilityMod = ability?.modifier || 0; +} \ No newline at end of file diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js index 3b1b987e..ae609923 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeVariable/aggregate/aggregateEffect.js @@ -1,6 +1,6 @@ import { pick } from 'lodash'; -export default function aggregateEffect({node, linkedNode, link}){ +export default function aggregateEffect({ node, linkedNode, link }) { if (link.data !== 'effect') return; // store the effect aggregator, its presence indicates that the variable is // targeted by effects @@ -38,6 +38,7 @@ export default function aggregateEffect({node, linkedNode, link}){ operation: linkedNode.data.operation, amount: effectAmount, type: linkedNode.data.type, + text: linkedNode.data.text, // ancestors: linkedNode.data.ancestors, }); @@ -45,14 +46,13 @@ export default function aggregateEffect({node, linkedNode, link}){ const aggregator = node.data.effectAggregator; // Get the result of the effect const result = linkedNode.data.amount?.value; - // Skip aggregating if the result is not resolved completely - if (typeof result === 'string' || result === undefined) return; + // Aggregate the effect based on its operation - switch(linkedNode.data.operation){ + switch (linkedNode.data.operation) { case 'base': // Take the largest base value - if (Number.isFinite(result)){ - if(Number.isFinite(aggregator.base)){ + if (Number.isFinite(result)) { + if (Number.isFinite(aggregator.base)) { aggregator.base = Math.max(aggregator.base, result); } else { aggregator.base = result; diff --git a/app/imports/api/properties/Actions.js b/app/imports/api/properties/Actions.js index 1ff4d6fd..99910081 100644 --- a/app/imports/api/properties/Actions.js +++ b/app/imports/api/properties/Actions.js @@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; import { storedIconsSchema } from '/imports/api/icons/Icons.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; /* * Actions are things a character can do @@ -24,9 +25,17 @@ let ActionSchema = createPropertySchema({ // long actions take longer than 1 round to cast actionType: { type: String, - allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long'], + allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long', 'event'], defaultValue: 'action', }, + // If the action type is an event, what is the variable name of that event? + variableName: { + type: String, + optional: true, + regEx: VARIABLE_NAME_REGEX, + min: 2, + max: STORAGE_LIMITS.variableName, + }, // Who is the action directed at target: { type: String, @@ -56,8 +65,10 @@ let ActionSchema = createPropertySchema({ // How this action's uses are reset automatically reset: { type: String, - allowedValues: ['longRest', 'shortRest'], optional: true, + regEx: VARIABLE_NAME_REGEX, + min: 2, + max: STORAGE_LIMITS.variableName, }, // Resources resources: { @@ -74,7 +85,7 @@ let ActionSchema = createPropertySchema({ 'resources.itemsConsumed.$._id': { type: String, regEx: SimpleSchema.RegEx.Id, - autoValue(){ + autoValue() { if (!this.isSet) return Random.id(); } }, @@ -101,7 +112,7 @@ let ActionSchema = createPropertySchema({ 'resources.attributesConsumed.$._id': { type: String, regEx: SimpleSchema.RegEx.Id, - autoValue(){ + autoValue() { if (!this.isSet) return Random.id(); } }, @@ -218,4 +229,4 @@ const ComputedActionSchema = new SimpleSchema() .extend(ActionSchema) .extend(ComputedOnlyActionSchema); -export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema}; +export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema }; diff --git a/app/imports/api/properties/Attributes.js b/app/imports/api/properties/Attributes.js index 1052e2e5..94e5d657 100644 --- a/app/imports/api/properties/Attributes.js +++ b/app/imports/api/properties/Attributes.js @@ -28,8 +28,7 @@ let AttributeSchema = createPropertySchema({ 'stat', // Speed, Armor Class 'modifier', // Proficiency Bonus, displayed as +x 'hitDice', // d12 hit dice - 'healthBar', // Hitpoints, Temporary Hitpoints, can take damage - 'bar', // Displayed as a health bar, can't take damage + 'healthBar', // Hitpoints, Temporary Hitpoints 'resource', // Rages, sorcery points 'spellSlot', // Level 1, 2, 3... spell slots 'utility', // Aren't displayed, Jump height, Carry capacity @@ -129,7 +128,9 @@ let AttributeSchema = createPropertySchema({ reset: { type: String, optional: true, - allowedValues: ['shortRest', 'longRest'], + regEx: VARIABLE_NAME_REGEX, + min: 2, + max: STORAGE_LIMITS.variableName, }, }); diff --git a/app/imports/api/properties/Folders.js b/app/imports/api/properties/Folders.js index 45055ce1..aa68e56f 100644 --- a/app/imports/api/properties/Folders.js +++ b/app/imports/api/properties/Folders.js @@ -7,6 +7,11 @@ let FolderSchema = new createPropertySchema({ name: { type: String, max: STORAGE_LIMITS.name, + optional: true, + }, + groupStats: { + type: Boolean, + optional: true, }, }); diff --git a/app/imports/api/properties/SpellLists.js b/app/imports/api/properties/SpellLists.js index 01434ea2..285832eb 100644 --- a/app/imports/api/properties/SpellLists.js +++ b/app/imports/api/properties/SpellLists.js @@ -17,6 +17,12 @@ let SpellListSchema = createPropertySchema({ type: 'fieldToCompute', optional: true, }, + // The variable name of the ability this spell relies on + ability: { + type: String, + optional: true, + max: STORAGE_LIMITS.variableName, + }, // Calculation of The attack roll bonus used by spell attacks in this list attackRollBonus: { type: 'fieldToCompute', @@ -38,6 +44,12 @@ const ComputedOnlySpellListSchema = createPropertySchema({ type: 'computedOnlyField', optional: true, }, + // Computed value determined by the ability + abilityMod: { + type: SimpleSchema.Integer, + optional: true, + removeBeforeCompute: true, + }, attackRollBonus: { type: 'computedOnlyField', optional: true, diff --git a/app/imports/constants/MAINTENANCE_MODE.js b/app/imports/constants/MAINTENANCE_MODE.js index 42045410..aece8399 100644 --- a/app/imports/constants/MAINTENANCE_MODE.js +++ b/app/imports/constants/MAINTENANCE_MODE.js @@ -1,15 +1,23 @@ import { Migrations } from 'meteor/percolate:migrations'; import SCHEMA_VERSION from '/imports/constants/SCHEMA_VERSION.js'; -if (Meteor.isServer){ - Meteor.startup(()=>{ +if (Meteor.isServer) { + Meteor.startup(() => { const dbVersion = Migrations.getVersion(); + // If there are no users, this is a new DB, set the version to latest + const aUser = Meteor.users.findOne({}); + const latestVersion = Migrations._list[Migrations._list.length - 1].version + if (!aUser && dbVersion !== latestVersion) { + Migrations._collection.update({ _id: 'control' }, { version: latestVersion }); + return; + } + // Otherwise put the app in maintenance mode if it's not the right version if ( !Meteor.settings.public.maintenanceMode && dbVersion !== undefined && SCHEMA_VERSION !== dbVersion - ){ - Meteor.settings.public.maintenanceMode = { + ) { + Meteor.settings.public.maintenanceMode = { reason: 'App data needs to be migrated to the latest version' }; } diff --git a/app/imports/ui/components/ResetSelector.vue b/app/imports/ui/components/ResetSelector.vue new file mode 100644 index 00000000..a9248ec8 --- /dev/null +++ b/app/imports/ui/components/ResetSelector.vue @@ -0,0 +1,46 @@ + + + diff --git a/app/imports/ui/creature/CreatureForm.vue b/app/imports/ui/creature/CreatureForm.vue index d2ea8eeb..1fdc4073 100644 --- a/app/imports/ui/creature/CreatureForm.vue +++ b/app/imports/ui/creature/CreatureForm.vue @@ -39,6 +39,11 @@ :input-value="model.settings.hideUnusedStats" @change="value => $emit('change', {path: ['settings','hideUnusedStats'], value: !!value})" /> +
- +
+ + + +
-
+ +
+
@@ -33,26 +65,14 @@ Buffs and conditions - - - - {{ buff.name }} - - - - - mdi-delete - - - + @remove="softRemove(buff._id)" + />
@@ -182,7 +202,6 @@ :model="spellSlot" :data-id="spellSlot._id" @click="clickProperty({_id: spellSlot._id})" - @cast="castSpellWithSlot(spellSlot._id)" />
f._id); + }, + healthBars() { + return getAttributeOfType(this.creature, this.folderIds, 'healthBar'); + }, abilities() { - return getAttributeOfType(this.creature, 'ability'); + return getAttributeOfType(this.creature, this.folderIds, 'ability'); }, stats() { - return getAttributeOfType(this.creature, 'stat'); + return getAttributeOfType(this.creature, this.folderIds, 'stat'); }, toggles() { return CreatureProperties.find({ - 'ancestors.id': this.creatureId, type: 'toggle', + 'ancestors.id': this.creatureId, + 'parent.id': { $nin: this.folderIds }, removed: { $ne: true }, deactivatedByAncestor: { $ne: true }, showUI: true, @@ -435,52 +473,56 @@ export default { }); }, modifiers() { - return getAttributeOfType(this.creature, 'modifier'); + return getAttributeOfType(this.creature, this.folderIds, 'modifier'); }, resources() { - return getAttributeOfType(this.creature, 'resource'); + return getAttributeOfType(this.creature, this.folderIds, 'resource'); }, spellSlots() { - return getAttributeOfType(this.creature, 'spellSlot'); + return getAttributeOfType(this.creature, this.folderIds, 'spellSlot'); }, hasSpells() { - const cursor = getProperties(this.creature, { + const cursor = getProperties(this.creature, this.folderIds, { type: 'spell', }) return cursor && cursor.count(); }, hitDice() { - return getAttributeOfType(this.creature, 'hitDice'); + return getAttributeOfType(this.creature, this.folderIds, 'hitDice'); }, checks() { - return getSkillOfType(this.creature, 'check'); + return getSkillOfType(this.creature, this.folderIds, 'check'); }, savingThrows() { - return getSkillOfType(this.creature, 'save'); + return getSkillOfType(this.creature, this.folderIds, 'save'); }, skills() { - return getSkillOfType(this.creature, 'skill'); + return getSkillOfType(this.creature, this.folderIds, 'skill'); }, tools() { - return getSkillOfType(this.creature, 'tool'); + return getSkillOfType(this.creature, this.folderIds, 'tool'); }, weapons() { - return getSkillOfType(this.creature, 'weapon'); + return getSkillOfType(this.creature, this.folderIds, 'weapon'); }, armors() { - return getSkillOfType(this.creature, 'armor'); + return getSkillOfType(this.creature, this.folderIds, 'armor'); }, languages() { - return getSkillOfType(this.creature, 'language'); + return getSkillOfType(this.creature, this.folderIds, 'language'); + }, + events() { + const events = getProperties(this.creature, this.folderIds, { type: 'action', actionType: 'event' }); + return uniqBy(events.fetch(), e => e.variableName); }, actions() { - return getProperties(this.creature, { type: 'action' }); + return getProperties(this.creature, this.folderIds, { type: 'action', actionType: { $ne: 'event' } }); }, appliedBuffs() { - return getProperties(this.creature, { type: 'buff' }); + return getProperties(this.creature, this.folderIds, { type: 'buff' }); }, multipliers() { - return getProperties(this.creature, { + return getProperties(this.creature, this.folderIds, { type: 'damageMultiplier' }, { sort: { value: 1, order: 1 } @@ -503,13 +545,23 @@ export default { }); }, incrementChange(_id, { type, value }) { - if (type === 'increment') { - damageProperty.call({ _id, operation: 'increment', value: -value }); - } + damageProperty.call({ + _id, + operation: type, + value: -value + }, error => { + if (error) { + snackbar({ text: error.reason || error.message || error.toString() }); + console.error(error); + } + }); }, softRemove(_id) { softRemoveProperty.call({ _id }, error => { - if (error) console.error(error); + if (error) { + snackbar({ text: error.reason || error.message || error.toString() }); + console.error(error); + } }); }, castSpell() { diff --git a/app/imports/ui/creature/character/printedCharacterSheet/CharacterSheetPrinted.vue b/app/imports/ui/creature/character/printedCharacterSheet/CharacterSheetPrinted.vue new file mode 100644 index 00000000..5a4c5b53 --- /dev/null +++ b/app/imports/ui/creature/character/printedCharacterSheet/CharacterSheetPrinted.vue @@ -0,0 +1,339 @@ + + + + + diff --git a/app/imports/ui/creature/character/printedCharacterSheet/PrintedInventory.vue b/app/imports/ui/creature/character/printedCharacterSheet/PrintedInventory.vue new file mode 100644 index 00000000..53694a81 --- /dev/null +++ b/app/imports/ui/creature/character/printedCharacterSheet/PrintedInventory.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/app/imports/ui/creature/character/printedCharacterSheet/PrintedSpells.vue b/app/imports/ui/creature/character/printedCharacterSheet/PrintedSpells.vue new file mode 100644 index 00000000..1d60374b --- /dev/null +++ b/app/imports/ui/creature/character/printedCharacterSheet/PrintedSpells.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/app/imports/ui/creature/character/printedCharacterSheet/PrintedStats.vue b/app/imports/ui/creature/character/printedCharacterSheet/PrintedStats.vue new file mode 100644 index 00000000..e3bad2a4 --- /dev/null +++ b/app/imports/ui/creature/character/printedCharacterSheet/PrintedStats.vue @@ -0,0 +1,639 @@ + + + + + diff --git a/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedAction.vue b/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedAction.vue new file mode 100644 index 00000000..5f1adf07 --- /dev/null +++ b/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedAction.vue @@ -0,0 +1,247 @@ + + + + + + + diff --git a/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedContainer.vue b/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedContainer.vue new file mode 100644 index 00000000..a15ed8c8 --- /dev/null +++ b/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedContainer.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedDamageMultipliers.vue b/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedDamageMultipliers.vue new file mode 100644 index 00000000..386aabcd --- /dev/null +++ b/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedDamageMultipliers.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedItem.vue b/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedItem.vue new file mode 100644 index 00000000..ccc5b85d --- /dev/null +++ b/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedItem.vue @@ -0,0 +1,173 @@ + + + + + diff --git a/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSkill.vue b/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSkill.vue new file mode 100644 index 00000000..2920634c --- /dev/null +++ b/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSkill.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSpell.vue b/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSpell.vue new file mode 100644 index 00000000..8367c2a0 --- /dev/null +++ b/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSpell.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSpellList.vue b/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSpellList.vue new file mode 100644 index 00000000..e158000f --- /dev/null +++ b/app/imports/ui/creature/character/printedCharacterSheet/components/PrintedSpellList.vue @@ -0,0 +1,45 @@ + + + \ No newline at end of file diff --git a/app/imports/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue b/app/imports/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue index 944c7642..60eea258 100644 --- a/app/imports/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue +++ b/app/imports/ui/creature/creatureProperties/AddCreaturePropertyDialog.vue @@ -16,6 +16,14 @@ flat @change="propertyHelpChanged" /> + + mdi-help + import LibraryNodes from '/imports/api/library/LibraryNodes.js'; import DialogBase from '/imports/ui/dialogStack/DialogBase.vue'; -import { getPropertyName } from '/imports/constants/PROPERTIES.js'; +import PROPERTIES, { getPropertyName } from '/imports/constants/PROPERTIES.js'; import TreeNodeView from '/imports/ui/properties/treeNodeViews/TreeNodeView.vue'; import LibraryNodeExpansionContent from '/imports/ui/library/LibraryNodeExpansionContent.vue'; import schemaFormMixin from '/imports/ui/properties/forms/shared/schemaFormMixin.js'; @@ -235,7 +243,11 @@ export default { }, toolbarColor(){ return getThemeColor('secondary'); - } + }, + docsPath() { + const propDef = PROPERTIES[this.type]; + return propDef && propDef.docsPath; + }, }, watch: { type(newType){ @@ -259,6 +271,15 @@ export default { }); }); }, + helpDialog() { + this.$store.commit('pushDialogStack', { + component: 'help-dialog', + elementId: 'help-button', + data: { + path: this.docsPath, + }, + }); + }, searchChanged(val, ack){ this._subs.searchLibraryNodes.setData('searchTerm', val); this._subs.searchLibraryNodes.setData('limit', undefined); diff --git a/app/imports/ui/library/LibraryNodeInsertForm.vue b/app/imports/ui/library/LibraryNodeInsertForm.vue index ebd94d49..c006dde6 100644 --- a/app/imports/ui/library/LibraryNodeInsertForm.vue +++ b/app/imports/ui/library/LibraryNodeInsertForm.vue @@ -12,6 +12,13 @@ :value="model.color" @input="value => change({path: ['color'], value})" /> + + mdi-help + diff --git a/app/imports/ui/properties/components/actions/EventButton.vue b/app/imports/ui/properties/components/actions/EventButton.vue new file mode 100644 index 00000000..29bdb0c6 --- /dev/null +++ b/app/imports/ui/properties/components/actions/EventButton.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/app/imports/ui/properties/components/attributes/AttributeCard.vue b/app/imports/ui/properties/components/attributes/AttributeCard.vue index 7b3e1492..f1fd03f9 100644 --- a/app/imports/ui/properties/components/attributes/AttributeCard.vue +++ b/app/imports/ui/properties/components/attributes/AttributeCard.vue @@ -5,54 +5,19 @@ @mouseover="hasClickListener ? hovering = true : undefined" @mouseleave="hasClickListener ? hovering = false : undefined" > -
- - - {{ computedValue }} - - - - {{ computedValue }} - - - {{ model.name }} - -
+ - - diff --git a/app/imports/ui/properties/components/attributes/AttributeCardContent.vue b/app/imports/ui/properties/components/attributes/AttributeCardContent.vue new file mode 100644 index 00000000..439ad70f --- /dev/null +++ b/app/imports/ui/properties/components/attributes/AttributeCardContent.vue @@ -0,0 +1,97 @@ + + + + + \ No newline at end of file diff --git a/app/imports/ui/properties/components/attributes/HealthBar.vue b/app/imports/ui/properties/components/attributes/HealthBar.vue index da4bbb7d..cc83302b 100644 --- a/app/imports/ui/properties/components/attributes/HealthBar.vue +++ b/app/imports/ui/properties/components/attributes/HealthBar.vue @@ -6,7 +6,7 @@ style="min-height: 42px;" :class="{ hover }" class="my-1 health-bar" - :data-id="_id" + :data-id="model._id" >
- {{ name }} + {{ model.name }}
- {{ value }} / {{ maxValue }} + {{ model.value }} / {{ model.total }}
- + - - -
- + @@ -104,31 +99,9 @@ export default { }, }, props: { - value: { - type: Number, - default: undefined, - }, - maxValue: { - type: Number, - default: undefined, - }, - name: { - type: String, - default: undefined, - }, - color: { - type: String, - default() { - return this.$vuetify.theme.currentTheme.primary - }, - }, - midColor: { - type: String, - default: undefined, - }, - lowColor: { - type: String, - default: undefined, + model: { + type: Object, + required: true, }, _id: String, }, @@ -136,24 +109,29 @@ export default { return { editing: false, hover: false, + x: 0, + y: 0, }; }, computed: { fillFraction() { - let fraction = this.value / this.maxValue; + let fraction = this.model.value / this.model.total; if (fraction < 0) fraction = 0; if (fraction > 1) fraction = 1; return fraction; }, + color() { + return this.model.color || this.$vuetify.theme.currentTheme.primary + }, barColor() { - const fraction = this.value / this.maxValue; + const fraction = this.model.value / this.model.total; if (!Number.isFinite(fraction)) return this.color; if (fraction > 0.5) { return this.color; - } else if (this.midColor && this.lowColor) { - return chroma.mix(this.lowColor, this.midColor, fraction * 2).hex(); - } else if (this.midColor) { - return this.midColor; + } else if (this.model.healthBarColorMid && this.model.healthBarColorLow) { + return chroma.mix(this.model.healthBarColorLow, this.model.healthBarColorMid, fraction * 2).hex(); + } else if (this.model.healthBarColorMid) { + return this.model.healthBarColorMid; } return this.color; }, @@ -166,7 +144,7 @@ export default { isTextLight() { return isDarkColor(this.barBackgroundColor); /* Change color at the halfway mark - const fraction = this.value / this.maxValue; + const fraction = this.model.value / this.model.total; if (fraction >= 0.5){ return isDarkColor(this.barColor); } else { @@ -176,8 +154,14 @@ export default { } }, methods: { - edit() { - this.editing = true; + edit(e) { + e.preventDefault() + this.editing = false; + this.x = e.clientX - 165; + this.y = e.clientY - 24; + this.$nextTick(() => { + this.editing = true + }); }, cancelEdit() { this.editing = false; @@ -199,6 +183,10 @@ export default { z-index: 7; position: relative; } + +.no-menu-shadow { + box-shadow: none !important; +} diff --git a/app/imports/ui/properties/components/attributes/ResourceCardContent.vue b/app/imports/ui/properties/components/attributes/ResourceCardContent.vue new file mode 100644 index 00000000..1f9de91f --- /dev/null +++ b/app/imports/ui/properties/components/attributes/ResourceCardContent.vue @@ -0,0 +1,89 @@ + + + + + \ No newline at end of file diff --git a/app/imports/ui/properties/components/attributes/SpellSlotListTile.vue b/app/imports/ui/properties/components/attributes/SpellSlotListTile.vue index 584fc765..f4a00384 100644 --- a/app/imports/ui/properties/components/attributes/SpellSlotListTile.vue +++ b/app/imports/ui/properties/components/attributes/SpellSlotListTile.vue @@ -61,7 +61,6 @@ export default { required: true, }, dark: Boolean, - hideCastButton: Boolean, disabled: Boolean, }, computed: { diff --git a/app/imports/ui/properties/components/buffs/BuffListItem.vue b/app/imports/ui/properties/components/buffs/BuffListItem.vue new file mode 100644 index 00000000..b122f33c --- /dev/null +++ b/app/imports/ui/properties/components/buffs/BuffListItem.vue @@ -0,0 +1,30 @@ + + + \ No newline at end of file diff --git a/app/imports/ui/properties/components/folders/FolderGroupCard.vue b/app/imports/ui/properties/components/folders/FolderGroupCard.vue new file mode 100644 index 00000000..a216c8a6 --- /dev/null +++ b/app/imports/ui/properties/components/folders/FolderGroupCard.vue @@ -0,0 +1,81 @@ + + + + + \ No newline at end of file diff --git a/app/imports/ui/properties/components/folders/folderGroupComponents/ActionGroupComponent.vue b/app/imports/ui/properties/components/folders/folderGroupComponents/ActionGroupComponent.vue new file mode 100644 index 00000000..7bcb81da --- /dev/null +++ b/app/imports/ui/properties/components/folders/folderGroupComponents/ActionGroupComponent.vue @@ -0,0 +1,35 @@ + + + \ No newline at end of file diff --git a/app/imports/ui/properties/components/folders/folderGroupComponents/AttributeGroupComponent.vue b/app/imports/ui/properties/components/folders/folderGroupComponents/AttributeGroupComponent.vue new file mode 100644 index 00000000..30677f39 --- /dev/null +++ b/app/imports/ui/properties/components/folders/folderGroupComponents/AttributeGroupComponent.vue @@ -0,0 +1,95 @@ + + + + + \ No newline at end of file diff --git a/app/imports/ui/properties/components/folders/propertyComponentIndex.js b/app/imports/ui/properties/components/folders/propertyComponentIndex.js new file mode 100644 index 00000000..b61d0181 --- /dev/null +++ b/app/imports/ui/properties/components/folders/propertyComponentIndex.js @@ -0,0 +1,61 @@ +import action from '/imports/ui/properties/components/folders/folderGroupComponents/ActionGroupComponent.vue'; +//import adjustment from ''; +import attribute from './folderGroupComponents/AttributeGroupComponent.vue'; +import buff from '/imports/ui/properties/components/buffs/BuffListItem.vue'; +//import buffRemover from ''; +//import branch from ''; +//import constant from ''; +import container from '/imports/ui/properties/components/inventory/ContainerCard.vue'; +//import classComponent from ''; +//import classLevel from ''; +//import damage from ''; +//import damageMultiplier from ''; +//import effect from ''; +import feature from '/imports/ui/properties/components/features/FeatureCard.vue'; +// import folder from ''; +import item from '/imports/ui/properties/components/inventory/ItemListTile.vue'; +import note from '/imports/ui/properties/components/persona/NoteCard.vue'; +//import pointBuy from ''; +//import proficiency from ''; +//import propertySlot from ''; +//import reference from ''; +//import roll from ''; +//import savingThrow from ''; +import skill from '/imports/ui/properties/components/skills/SkillListTile.vue'; +//import slotFiller from ''; +//import spellList from ''; +//import spell from ''; +import toggle from '/imports/ui/properties/components/toggles/ToggleCard.vue'; +//import trigger from ''; + +export default { + action, + //adjustment, + attribute, + buff, + //buffRemover, + //branch, + //constant, + container, + //class: classComponent, + //classLevel, + //damage, + //damageMultiplier, + //effect, + feature, + //folder, + item, + note, + //pointBuy, + //proficiency, + //propertySlot, + //reference, + //roll, + //savingThrow, + skill, + //slotFiller, + //spellList, + //spell, + toggle, + //trigger, +}; diff --git a/app/imports/ui/properties/components/inventory/ItemListTile.vue b/app/imports/ui/properties/components/inventory/ItemListTile.vue index 5dfa1b8e..72fb09c6 100644 --- a/app/imports/ui/properties/components/inventory/ItemListTile.vue +++ b/app/imports/ui/properties/components/inventory/ItemListTile.vue @@ -32,7 +32,7 @@ @change="changeQuantity" /> - +
+