diff --git a/app/imports/api/creature/experience/Experiences.js b/app/imports/api/creature/experience/Experiences.js index 5f3a8ea2..7c0bb002 100644 --- a/app/imports/api/creature/experience/Experiences.js +++ b/app/imports/api/creature/experience/Experiences.js @@ -61,7 +61,7 @@ const insertExperienceForCreature = function({experience, creatureId, userId}){ } experience.creatureId = creatureId; let id = Experiences.insert(experience); - recomputeCreatureById(creatureId); + computeCreature(creatureId); return id; }; @@ -135,7 +135,7 @@ const removeExperience = new ValidatedMethod({ } experience.creatureId = creatureId; let numRemoved = Experiences.remove(experienceId); - recomputeCreatureById(creatureId); + computeCreature(creatureId); return numRemoved; }, }); diff --git a/app/imports/api/engine/computation/buildCreatureComputation.js b/app/imports/api/engine/computation/buildCreatureComputation.js index e78b248c..8e81e314 100644 --- a/app/imports/api/engine/computation/buildCreatureComputation.js +++ b/app/imports/api/engine/computation/buildCreatureComputation.js @@ -2,6 +2,7 @@ import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js'; import CreatureProperties, { DenormalisedOnlyCreaturePropertySchema as denormSchema } 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 computedSchemas from '/imports/api/properties/computedPropertySchemasIndex.js'; import linkInventory from './buildComputation/linkInventory.js'; @@ -30,8 +31,9 @@ import removeSchemaFields from './buildComputation/removeSchemaFields.js'; */ export default function buildCreatureComputation(creatureId){ + const creature = getCreature(creatureId); const properties = getProperties(creatureId); - const computation = buildComputationFromProps(properties); + const computation = buildComputationFromProps(properties, creature); return computation; } @@ -44,7 +46,13 @@ function getProperties(creatureId){ }).fetch(); } -export function buildComputationFromProps(properties){ +function getCreature(creatureId){ + return Creatures.findOne(creatureId, { + denormalizedStats: 1, + }); +} + +export function buildComputationFromProps(properties, creature){ const computation = new CreatureComputation(properties); // 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 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 properties.forEach(prop => { diff --git a/app/imports/api/library/LibraryNodes.js b/app/imports/api/library/LibraryNodes.js index 5d04a0e6..283a8a68 100644 --- a/app/imports/api/library/LibraryNodes.js +++ b/app/imports/api/library/LibraryNodes.js @@ -19,6 +19,10 @@ import { restore } from '/imports/api/parenting/softRemove.js'; let LibraryNodes = new Mongo.Collection('libraryNodes'); let LibraryNodeSchema = new SimpleSchema({ + _id: { + type: String, + regEx: SimpleSchema.RegEx.Id, + }, type: { type: String, allowedValues: Object.keys(propertySchemasIndex), diff --git a/app/imports/migrations/server/dbv1/dbv1.js b/app/imports/migrations/server/dbv1/dbv1.js index 530f0774..9e140ef3 100644 --- a/app/imports/migrations/server/dbv1/dbv1.js +++ b/app/imports/migrations/server/dbv1/dbv1.js @@ -201,7 +201,7 @@ const transformsByPropType = { function getComputedPropertyTransforms(key, toKey){ if (!toKey) toKey = key; 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}Errors`, to: `${toKey}.errors`, up: trimErrors}, ]; @@ -209,7 +209,7 @@ function getComputedPropertyTransforms(key, toKey){ function getInlineComputationTransforms(key){ 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.$.result`, to: `${key}.inlineCalculations.$.value`}, ]; diff --git a/app/imports/migrations/server/dbv1/dbv1.test.js b/app/imports/migrations/server/dbv1/dbv1.test.js index 13954186..9a0887ac 100644 --- a/app/imports/migrations/server/dbv1/dbv1.test.js +++ b/app/imports/migrations/server/dbv1/dbv1.test.js @@ -1,118 +1,217 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; -import { migrateProperty } from './dbv1.js'; -import { assert } from 'chai'; +import { + migrateProperty +} from './dbv1.js'; +import { + assert +} from 'chai'; +import LibraryNodes from '/imports/api/library/LibraryNodes.js'; const exampleAction = { - '_id':'hY5MKZ4ivaoTRpNWy', - 'actionType':'bonus', - 'target':'singleTarget', - 'tags':[], - 'resources':{ - 'itemsConsumed':[], - 'attributesConsumed':[{ - '_id':'FaK6jXEj3pSe7mNuu', - 'quantity': '1', - 'variableName':'HunterTech', - 'statName':'Hunter\'s Technique', - 'available':5 - }], - }, - 'type':'action', - 'name':'Hexblade\\\'s Curse', - 'parent':{ - 'id':'JqtDmqa5Zd3xpts5G', - 'collection':'creatureProperties' - }, - 'ancestors':[ - { - 'collection':'creatures', - '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."}', - 'uses':'1', - 'usesResult':1, - 'reset':'shortRest', - 'usesUsed':0, - '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.', - 'color':'#8e24aa', - 'descriptionCalculations':[ - { - '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."', - '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.' - } - ], - 'summaryCalculations':[ - { - 'calculation':'warlock.level >14 ? "" : "the target dies, or"', - 'result':'the target dies, or' - }, - { - 'calculation':'proficiencyBonus', - 'result':'4' - }, - { - 'calculation':'warlock.level+charisma.modifier', - '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.' - } - ] + '_id': 'hY5MKZ4ivaoTRpNWy', + 'actionType': 'bonus', + 'target': 'singleTarget', + 'tags': [], + 'resources': { + 'itemsConsumed': [], + 'attributesConsumed': [{ + '_id': 'FaK6jXEj3pSe7mNuu', + 'quantity': '1', + 'variableName': 'HunterTech', + 'statName': 'Hunter\'s Technique', + 'available': 5 + }], + }, + 'type': 'action', + 'name': 'Hexblade\\\'s Curse', + 'parent': { + 'id': 'JqtDmqa5Zd3xpts5G', + 'collection': 'creatureProperties' + }, + 'ancestors': [{ + 'collection': 'creatures', + '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."}', + 'uses': '1', + 'usesResult': 1, + 'reset': 'shortRest', + 'usesUsed': 0, + '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.', + 'color': '#8e24aa', + 'descriptionCalculations': [{ + '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."', + '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.' + }], + 'summaryCalculations': [{ + 'calculation': 'warlock.level >14 ? "" : "the target dies, or"', + 'result': 'the target dies, or' + }, + { + 'calculation': 'proficiencyBonus', + 'result': '4' + }, + { + 'calculation': 'warlock.level+charisma.modifier', + '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 = { - _id:'idRWyoj5oxCv73feM', - name:'Hit Dice', - variableName:'clericHitDice', - attributeType:'hitDice', - type:'attribute', - hitDiceSize:'d8', - baseValueCalculation:'cleric.level', - parent:{'id':'8jSWKxvgQyKbunFtD','collection':'creatureProperties'}, - ancestors:[ - {'collection':'creatures','id':'m9sdCvs6iDf7qRaGv'}, - {'id':'8jSWKxvgQyKbunFtD','collection':'creatureProperties'} + _id: 'idRWyoj5oxCv73feM', + name: 'Hit Dice', + variableName: 'clericHitDice', + attributeType: 'hitDice', + type: 'attribute', + hitDiceSize: 'd8', + baseValueCalculation: 'cleric.level', + parent: { + 'id': '8jSWKxvgQyKbunFtD', + 'collection': 'creatureProperties' + }, + ancestors: [{ + 'collection': 'creatures', + 'id': 'm9sdCvs6iDf7qRaGv' + }, + { + 'id': '8jSWKxvgQyKbunFtD', + 'collection': 'creatureProperties' + } ], order: 84, value: 20, - tags:[], + tags: [], baseValue: 20, damage: 3, currentValue: 17, constitutionMod: 2, - dependencies: ['8jSWKxvgQyKbunFtD','qPP5yQXPxS7uhuXo3'] + dependencies: ['8jSWKxvgQyKbunFtD', 'qPP5yQXPxS7uhuXo3'] }; const expectedMigratedAttribute = { - _id:'idRWyoj5oxCv73feM', - name:'Hit Dice', - variableName:'clericHitDice', - attributeType:'hitDice', - type:'attribute', - hitDiceSize:'d8', + _id: 'idRWyoj5oxCv73feM', + name: 'Hit Dice', + variableName: 'clericHitDice', + attributeType: 'hitDice', + type: 'attribute', + hitDiceSize: 'd8', baseValue: { calculation: 'cleric.level', value: 20 }, - parent:{'id':'8jSWKxvgQyKbunFtD','collection':'creatureProperties'}, - ancestors:[ - {'collection':'creatures','id':'m9sdCvs6iDf7qRaGv'}, - {'id':'8jSWKxvgQyKbunFtD','collection':'creatureProperties'} + parent: { + 'id': '8jSWKxvgQyKbunFtD', + 'collection': 'creatureProperties' + }, + ancestors: [{ + 'collection': 'creatures', + 'id': 'm9sdCvs6iDf7qRaGv' + }, + { + 'id': '8jSWKxvgQyKbunFtD', + 'collection': 'creatureProperties' + } ], order: 84, total: 20, - tags:[], + tags: [], damage: 3, value: 17, constitutionMod: 2, } -describe('migrateProperty', function () { - it('Migrates actions reversibly', function () { - const action = {...exampleAction}; +const exampleAttack = { + '_id': 'vw23EnJwBRcXEJg7i', + '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({ collection: CreatureProperties, prop: action @@ -125,8 +224,10 @@ describe('migrateProperty', function () { assert.deepEqual(action, exampleAction, 'action should not be bashed'); assert.deepEqual(exampleAction, reversedAction, 'operation should be reversible'); }); - it ('Migrates attributes as expected', function(){ - const attribute = {...exampleAttribute}; + it('Migrates attributes as expected', function() { + const attribute = { + ...exampleAttribute + }; const newAttribute = migrateProperty({ collection: CreatureProperties, prop: attribute @@ -134,4 +235,15 @@ describe('migrateProperty', function () { assert.deepEqual(newAttribute, expectedMigratedAttribute, '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'); + }); }); diff --git a/app/package.json b/app/package.json index 06e60d59..23d4e20a 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "dicecloud", - "version": "2.0-beta.33", + "version": "2.0.33", "description": "Unofficial Online Realtime D&D 5e App", "license": "GPL-3.0", "repository": {