import { includes, cloneDeep } from 'lodash'; // The computation memo is an in-memory data structure used only during the // computation process export default class ComputationMemo { constructor(props){ this.statsByVariableName = {}; this.extraStatsByVariableName = {}; this.statsById = {}; this.originalPropsById = {}; this.propsById = {}; this.skillsByAbility = {}; this.unassignedEffects = []; this.classLevelsById = {}; this.classes = {}; this.togglesById = {}; this.toggleIds = new Set(); // First note all the ids of all the toggles props.forEach((prop) => { if ( prop.type === 'toggle' ) { this.toggleIds.add(prop._id); } }); props.filter((prop) => { if ( prop.type === 'toggle' ) { this.addToggle(prop); } else { return true; } }).filter((prop) => { if ( prop.type === 'attribute' || prop.type === 'skill' ) { // Add all the stats this.addStat(prop); } else { return true; } }).forEach((prop) => { // Now add all effects and proficiencies if (prop.type === 'effect'){ this.addEffect(prop); } else if (prop.type === 'proficiency') { this.addProficiency(prop); } else if (prop.type === 'classLevel'){ this.addClassLevel(prop); } }); } registerProperty(prop){ this.originalPropsById[prop._id] = cloneDeep(prop); this.propsById[prop._id] = prop; prop.computationDetails = propDetails(prop); prop.ancestors.forEach(ancestor => { if (this.toggleIds.has(ancestor.id)){ prop.computationDetails.toggleAncestors.push(ancestor.id); } }); return prop; } /* storeHighestClassLevel(name, prop, isBaseClass){ // Only store the highest level classLevel let stat = this.statsByVariableName[name] if (!stat){ this.statsByVariableName[name] = prop; if (isBaseClass){ this.classes[name] = prop; } } else if (!has(stat, 'level')){ // Stat is overriden by an attribute return; } else if (stat.level < prop.level) { this.statsByVariableName[name] = prop; if (isBaseClass){ this.classes[name] = prop; } } this.updateLevel(); } updateLevel(){ let currentLevel = this.statsByVariableName['level']; if (!currentLevel){ currentLevel = { value: 0, computationDetails: { builtIn: true, computed: true, } }; this.statsByVariableName['level'] = currentLevel; } // bail out if overriden by an attribute if (!currentLevel.computationDetails.builtIn) return; let level = 0; for (let name in this.classes){ level += this.classes[name].level || 0; } this.statsByVariableName['level'].value = level; }*/ addToggle(prop){ prop = this.registerProperty(prop); this.togglesById[prop._id] = prop; } addClassLevel(prop){ prop = this.registerProperty(prop); this.classLevelsById[prop._id] = prop; } addStat(prop){ let variableName = prop.variableName; if (!variableName) return; let existingStat = this.statsByVariableName[variableName]; if (existingStat){ existingStat.computationDetails.idsOfSameName.push(prop._id); this.originalPropsById[prop._id] = cloneDeep(prop); if (prop.baseValueCalculation){ existingStat.computationDetails.effects.push({ operation: 'base', calculation: prop.baseValueCalculation, stats: [variableName], computationDetails: propDetailsByType.effect(), statBase: true, }); } if (prop.baseProficiency){ existingStat.computationDetails.proficiencies.push({ value: prop.baseProficiency, stats: [variableName], computationDetails: propDetailsByType.proficiency(), type: 'proficiency', statBase: true, }); } } else { prop = this.registerProperty(prop); this.statsById[prop._id] = prop; this.statsByVariableName[variableName] = prop; if ( prop.type === 'skill' && isSkillCheck(prop) && prop.ability ){ this.addSkillToAbility(prop, prop.ability) } } } addSkillToAbility(prop, ability){ if (!this.skillsByAbility[ability]){ this.skillsByAbility[ability] = []; } this.skillsByAbility[ability].push(prop); } addEffect(prop){ prop = this.registerProperty(prop); let targets = this.getEffectTargets(prop); targets.forEach(target => { if (target.computationDetails.effects){ target.computationDetails.effects.push(prop); } }); if (!targets.size){ this.unassignedEffects.push(prop); } } getEffectTargets(prop){ let targets = new Set(); if (!prop.stats) return targets; prop.stats.forEach((statName) => { let target = this.statsByVariableName[statName]; if (!target) return; targets.add(target); if (isSkillOperation(prop) && isAbility(target)){ let extras = this.skillsByAbility[statName] || []; extras.forEach(ex =>{ // Only pass on ability effects to skills and checks if (ex.skillType === 'skill' || ex.skillType === 'check'){ targets.add(ex) } }); } }); return targets; } addProficiency(prop){ prop = this.registerProperty(prop); let targets = this.getProficiencyTargets(prop); targets.forEach(target => { target.computationDetails.proficiencies.push(prop); }); } getProficiencyTargets(prop){ let targets = new Set(); if (!prop.stats) return targets; prop.stats.forEach(statName => { let target = this.statsByVariableName[statName]; if (!target) return; targets.add(target); if (isAbility(target)) { let extras = this.skillsByAbility[statName] || []; extras.forEach(ex =>{ // Only pass on ability proficiencies to skills and checks if (ex.skillType === 'skill' || ex.skillType === 'check'){ targets.add(ex) } }); } }); return targets; } } function isAbility(prop){ return prop.type === 'attribute' && prop.attributeType === 'ability' } function isSkillCheck(prop){ return includes(['skill', 'check', 'save', 'utility'], prop.skillType); } const skillOperations = [ 'advantage', 'disadvantage', 'passiveAdd', 'fail', 'conditional', 'rollBonus', ]; function isSkillOperation(prop){ return skillOperations.includes(prop.operation); } function propDetails(prop){ return propDetailsByType[prop.type]() || {}; } const propDetailsByType = { toggle(){ return { computed: false, busyComputing: false, toggleAncestors: [], disabledByToggle: false, }; }, attribute(){ return { computed: false, busyComputing: false, effects: [], toggleAncestors: [], disabledByToggle: false, idsOfSameName: [], }; }, skill(){ return { computed: false, busyComputing: false, effects: [], proficiencies: [], toggleAncestors: [], disabledByToggle: false, idsOfSameName: [], }; }, effect(){ return { computed: false, busyComputing: false, toggleAncestors: [], disabledByToggle: false, }; }, classLevel(){ return { computed: true, toggleAncestors: [], disabledByToggle: false, }; }, proficiency(){ return { toggleAncestors: [], disabledByToggle: false, }; }, }