Merge branch 'develop' into feature-nested-sets
This commit is contained in:
16
app/.github/pull_request_template.md
vendored
Normal file
16
app/.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Checklists
|
||||
|
||||
## Adding features
|
||||
|
||||
- [ ] My new pull request has zero code changes
|
||||
- [ ] I have described the feature I intend to work on
|
||||
- [ ] I have described how I intend to implement the feature
|
||||
- [ ] I will wait for comment from the project's maintainers before submitting code changes
|
||||
|
||||
## Fixing bugs
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have included a link to the relevant github issue or discord post in the description
|
||||
|
||||
# Description
|
||||
|
||||
`Detailed description of your changes`
|
||||
@@ -77,7 +77,8 @@ const duplicateProperty = new ValidatedMethod({
|
||||
});
|
||||
|
||||
// Give the docs new IDs without breaking internal references
|
||||
renewDocIds({ docArray: nodes });
|
||||
const allNodes = [property, ...nodes];
|
||||
renewDocIds({ docArray: allNodes });
|
||||
|
||||
// Order the root node
|
||||
property.order += 0.5;
|
||||
@@ -86,7 +87,7 @@ const duplicateProperty = new ValidatedMethod({
|
||||
property.dirty = true;
|
||||
|
||||
// Insert the properties
|
||||
CreatureProperties.batchInsert([property, ...nodes]);
|
||||
CreatureProperties.batchInsert(allNodes);
|
||||
|
||||
// Tree structure changed by inserts, reorder the tree
|
||||
reorderDocs({
|
||||
|
||||
@@ -21,7 +21,7 @@ const updateCreatureProperty = new ValidatedMethod({
|
||||
},
|
||||
mixins: [RateLimiterMixin],
|
||||
rateLimit: {
|
||||
numRequests: 5,
|
||||
numRequests: 12,
|
||||
timeInterval: 5000,
|
||||
},
|
||||
run({ _id, path, value }) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import action from './applyPropertyByType/applyAction.js';
|
||||
import ammo from './applyPropertyByType/applyItemAsAmmo.js'
|
||||
import adjustment from './applyPropertyByType/applyAdjustment.js';
|
||||
import branch from './applyPropertyByType/applyBranch.js';
|
||||
import buff from './applyPropertyByType/applyBuff.js';
|
||||
@@ -12,6 +13,7 @@ import toggle from './applyPropertyByType/applyToggle.js';
|
||||
|
||||
const applyPropertyByType = {
|
||||
action,
|
||||
ammo,
|
||||
adjustment,
|
||||
branch,
|
||||
buff,
|
||||
|
||||
@@ -4,13 +4,10 @@ import rollDice from '/imports/parser/rollDice.js';
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import applyChildren from '/imports/api/engine/actions/applyPropertyByType/shared/applyChildren.js';
|
||||
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
||||
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
|
||||
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js';
|
||||
import { getPropertyDecendants } from '/imports/api/engine/loadCreatures.js';
|
||||
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
|
||||
|
||||
export default function applyAction(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
@@ -174,7 +171,11 @@ function rollAttack(attack, scope) {
|
||||
}
|
||||
|
||||
function applyCrits(value, scope) {
|
||||
const criticalHitTarget = scope['~criticalHitTarget']?.value || 20;
|
||||
let scopeCrit = scope['~criticalHitTarget']?.value;
|
||||
if (scopeCrit?.parseType === 'constant') {
|
||||
scopeCrit = scopeCrit.value;
|
||||
}
|
||||
const criticalHitTarget = scopeCrit || 20;
|
||||
let criticalHit = value >= criticalHitTarget;
|
||||
let criticalMiss;
|
||||
if (criticalHit) {
|
||||
@@ -206,10 +207,9 @@ function spendResources(prop, actionContext) {
|
||||
return true;
|
||||
}
|
||||
// Items
|
||||
let itemQuantityAdjustments = [];
|
||||
let spendLog = [];
|
||||
let gainLog = [];
|
||||
let ammoChildren = [];
|
||||
const ammoToApply = [];
|
||||
try {
|
||||
prop.resources.itemsConsumed.forEach(itemConsumed => {
|
||||
recalculateCalculation(itemConsumed.quantity, actionContext);
|
||||
@@ -224,11 +224,6 @@ function spendResources(prop, actionContext) {
|
||||
!itemConsumed?.quantity?.value ||
|
||||
!isFinite(itemConsumed.quantity.value)
|
||||
) return;
|
||||
itemQuantityAdjustments.push({
|
||||
property: item,
|
||||
operation: 'increment',
|
||||
value: itemConsumed.quantity.value,
|
||||
});
|
||||
let logName = item.name;
|
||||
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1) {
|
||||
logName = item.plural || logName;
|
||||
@@ -238,7 +233,20 @@ function spendResources(prop, actionContext) {
|
||||
} else if (itemConsumed.quantity.value < 0) {
|
||||
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
|
||||
}
|
||||
ammoChildren.push(...getItemChildren(item, actionContext, prop));
|
||||
// So long as the item isn't an ancestor of the current prop apply it
|
||||
// If it was an ancestor this would be an infinite loop
|
||||
if (!hasAncestorRelationship(item, prop)) {
|
||||
ammoToApply.push({
|
||||
node: {
|
||||
...item,
|
||||
// Use ammo pseudo-type
|
||||
type: 'ammo',
|
||||
// Store the adjustment to be applied
|
||||
adjustment: itemConsumed.quantity.value,
|
||||
},
|
||||
children: []
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
actionContext.addLog({
|
||||
@@ -249,9 +257,6 @@ function spendResources(prop, actionContext) {
|
||||
return true;
|
||||
}
|
||||
// No more errors should be thrown after this line
|
||||
// Now that we have confirmed that there are no errors, do actual work
|
||||
//Items
|
||||
itemQuantityAdjustments.forEach(adjustQuantityWork);
|
||||
|
||||
// Use uses
|
||||
if (prop.usesLeft) {
|
||||
@@ -291,6 +296,11 @@ function spendResources(prop, actionContext) {
|
||||
}
|
||||
});
|
||||
|
||||
// Apply the ammo children
|
||||
ammoToApply.forEach(node => {
|
||||
applyProperty(node, actionContext);
|
||||
});
|
||||
|
||||
// Log all the spending
|
||||
if (gainLog.length && !prop.silent) actionContext.addLog({
|
||||
name: 'Gained',
|
||||
@@ -302,21 +312,6 @@ function spendResources(prop, actionContext) {
|
||||
value: spendLog.join('\n'),
|
||||
inline: true,
|
||||
});
|
||||
|
||||
// Apply the ammo children
|
||||
ammoChildren.forEach(prop => {
|
||||
applyProperty(prop, actionContext);
|
||||
});
|
||||
}
|
||||
|
||||
function getItemChildren(item, actionContext, prop) {
|
||||
// Skip if the prop or the item are ancestors of one another, otherwise infinite loop
|
||||
if (hasAncestorRelationship(item, prop)) return [];
|
||||
// Get the item children
|
||||
const itemProperties = getPropertyDecendants(actionContext.creature._id, item._id);
|
||||
// Tree them up
|
||||
const propertyForest = nodeArrayToTree(itemProperties);
|
||||
return propertyForest
|
||||
}
|
||||
|
||||
function hasAncestorRelationship(a, b) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '/imports/api/engine/loadCreatures.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags.js';
|
||||
import applySavingThrow from '/imports/api/engine/actions/applyPropertyByType/applySavingThrow.js';
|
||||
|
||||
export default function applyDamage(node, actionContext) {
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
@@ -36,7 +37,7 @@ export default function applyDamage(node, actionContext) {
|
||||
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
|
||||
|
||||
// roll the dice only and store that string
|
||||
applyEffectsToCalculationParseNode(prop.amount, actionContext.log);
|
||||
applyEffectsToCalculationParseNode(prop.amount, actionContext);
|
||||
const { result: rolled } = resolve('roll', prop.amount.parseNode, scope, context);
|
||||
if (rolled.parseType !== 'constant') {
|
||||
logValue.push(toString(rolled));
|
||||
@@ -67,6 +68,7 @@ export default function applyDamage(node, actionContext) {
|
||||
|
||||
// Round the damage to a whole number
|
||||
damage = Math.floor(damage);
|
||||
scope['~damage'] = damage;
|
||||
|
||||
// Convert extra damage into the stored type
|
||||
if (prop.damageType === 'extra' && scope['~lastDamageType']?.value) {
|
||||
@@ -82,24 +84,74 @@ export default function applyDamage(node, actionContext) {
|
||||
prop.damageType +
|
||||
(prop.damageType !== 'healing' ? ' damage ' : '');
|
||||
|
||||
// If there is a save, calculate the save damage
|
||||
let damageOnSave, saveNode, saveRoll;
|
||||
if (prop.save) {
|
||||
if (prop.save.damageFunction?.calculation) {
|
||||
applyEffectsToCalculationParseNode(prop.save.damageFunction, actionContext);
|
||||
let { result: saveDamageRolled } = resolve('roll', prop.save.damageFunction.parseNode, scope, context);
|
||||
saveRoll = toString(saveDamageRolled);
|
||||
let { result: saveDamageResult } = resolve('reduce', saveDamageRolled, scope, context);
|
||||
// If we didn't end up with a constant of finite amount, give up
|
||||
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)) {
|
||||
return applyChildren(node, actionContext);
|
||||
}
|
||||
damageOnSave = +saveDamageResult.value;
|
||||
// Round the damage to a whole number
|
||||
damageOnSave = Math.floor(damageOnSave);
|
||||
} else {
|
||||
damageOnSave = Math.floor(damage / 2);
|
||||
}
|
||||
saveNode = {
|
||||
node: {
|
||||
...prop.save,
|
||||
name: prop.save.stat,
|
||||
silent: prop.silent,
|
||||
},
|
||||
children: [],
|
||||
}
|
||||
}
|
||||
|
||||
if (damageTargets && damageTargets.length) {
|
||||
// Iterate through all the targets
|
||||
damageTargets.forEach(target => {
|
||||
actionContext.target = [target];
|
||||
let damageToApply = damage;
|
||||
|
||||
// If there is a saving throw, apply that first
|
||||
if (prop.save) {
|
||||
applySavingThrow(saveNode, actionContext);
|
||||
if (scope['~saveSucceeded']?.value) {
|
||||
// Log the total damage
|
||||
logValue.push(toString(reduced));
|
||||
// Log the save damage
|
||||
const damageText = damageFunctionText(prop.save);
|
||||
if (damageText) {
|
||||
logValue.push(damageText);
|
||||
} else {
|
||||
logValue.push(
|
||||
'**Damage on successful save**',
|
||||
prop.save.damageFunction.calculation,
|
||||
saveRoll
|
||||
);
|
||||
}
|
||||
damageToApply = damageOnSave;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply weaknesses/resistances/immunities
|
||||
damage = applyDamageMultipliers({
|
||||
damageToApply = applyDamageMultipliers({
|
||||
target,
|
||||
damage,
|
||||
damage: damageToApply,
|
||||
damageProp: prop,
|
||||
logValue
|
||||
});
|
||||
|
||||
actionContext.target = [target];
|
||||
// Deal the damage to the target
|
||||
let damageDealt = dealDamage({
|
||||
target,
|
||||
damageType: prop.damageType,
|
||||
amount: damage,
|
||||
amount: damageToApply,
|
||||
actionContext
|
||||
});
|
||||
|
||||
@@ -124,6 +176,10 @@ export default function applyDamage(node, actionContext) {
|
||||
} else {
|
||||
// There are no targets, just log the result
|
||||
logValue.push(`**${damage}** ${suffix}`);
|
||||
if (prop.save) {
|
||||
applySavingThrow(saveNode, actionContext);
|
||||
logValue.push(`**${damageOnSave}** ${suffix} on a successful save`);
|
||||
}
|
||||
}
|
||||
if (!prop.silent) actionContext.addLog({
|
||||
name: logName,
|
||||
@@ -133,6 +189,16 @@ export default function applyDamage(node, actionContext) {
|
||||
return applyChildren(node, actionContext);
|
||||
}
|
||||
|
||||
function damageFunctionText(save, scope, context, actionContext) {
|
||||
if (!save) return [];
|
||||
if (!save.damageFunction) {
|
||||
return '**Half damage on successful save**';
|
||||
}
|
||||
if (save.damageFunction.calculation == '0' || save.damageFunction.value === 0) {
|
||||
return '**No damage on successful save**'
|
||||
}
|
||||
}
|
||||
|
||||
function applyDamageMultipliers({ target, damage, damageProp, logValue }) {
|
||||
const damageType = damageProp?.damageType;
|
||||
if (!damageType) return damage;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { getPropertyDecendants } from '/imports/api/engine/loadCreatures.js';
|
||||
import applyProperty from '../applyProperty.js';
|
||||
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
|
||||
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
|
||||
import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/methods/adjustQuantity.js';
|
||||
|
||||
export default function applyItemAsAmmo(node, actionContext) {
|
||||
// The item node should come without children, since it is not part of the original action tree
|
||||
const prop = node.node;
|
||||
// Get all the item's descendant properties
|
||||
const properties = getPropertyDecendants(actionContext.creature._id, prop._id);
|
||||
properties.sort((a, b) => a.order - b.order);
|
||||
const propertyForest = nodeArrayToTree(properties);
|
||||
|
||||
// Apply the item
|
||||
applyNodeTriggers(node, 'before', actionContext);
|
||||
|
||||
// Do the quantity adjustment
|
||||
const itemProp = { ...prop, type: 'item' };
|
||||
delete itemProp.adjustment;
|
||||
adjustQuantityWork({
|
||||
property: itemProp,
|
||||
operation: 'increment',
|
||||
value: prop.adjustment,
|
||||
});
|
||||
|
||||
// Simulate the change to quantity
|
||||
prop.quantity -= prop.adjustment;
|
||||
|
||||
// Log the item name as a heading if it's not silent and has child properties to apply
|
||||
if (!prop.silent && propertyForest.length) {
|
||||
actionContext.addLog({
|
||||
name: prop.name || 'Ammo',
|
||||
inline: false,
|
||||
});
|
||||
}
|
||||
applyNodeTriggers(node, 'after', actionContext);
|
||||
|
||||
// Apply the item's children
|
||||
propertyForest.forEach(node => applyProperty(node, actionContext));
|
||||
applyNodeTriggers(node, 'afterChildren', actionContext);
|
||||
}
|
||||
@@ -9,8 +9,15 @@ export default function linkCalculationDependencies(dependencyGraph, prop, { pro
|
||||
};
|
||||
// Add this calculation to the dependency graph
|
||||
const calcNodeId = `${prop._id}.${calcObj._key}`;
|
||||
dependencyGraph.addNode(calcNodeId, calcObj);
|
||||
|
||||
// Skip empty calculations that aren't targeted by anything
|
||||
if (
|
||||
!calcObj.calculation
|
||||
&& !calcObj.effects
|
||||
&& !calcObj.proficiencies
|
||||
) return;
|
||||
|
||||
dependencyGraph.addNode(calcNodeId, calcObj);
|
||||
// Traverse the parsed calculation looking for variable names
|
||||
traverse(calcObj.parseNode, node => {
|
||||
// Skip nodes that aren't symbols or accessors
|
||||
|
||||
@@ -98,8 +98,10 @@ function linkAdjustment(dependencyGraph, prop) {
|
||||
|
||||
function linkAttribute(dependencyGraph, prop) {
|
||||
linkVariableName(dependencyGraph, prop);
|
||||
// Depends on spellSlotLevel
|
||||
// Spell slots depend on spellSlotLevel
|
||||
if (prop.type === 'spellSlot') {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'spellSlotLevel' });
|
||||
}
|
||||
|
||||
// Depends on base value
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
|
||||
@@ -159,7 +161,7 @@ function linkEffects(dependencyGraph, prop, computation) {
|
||||
// Otherwise target a field on that property
|
||||
const key = prop.targetField || getDefaultCalculationField(targetProp);
|
||||
const calcObj = get(targetProp, key);
|
||||
if (calcObj && calcObj.calculation) {
|
||||
if (calcObj) {
|
||||
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'effect');
|
||||
}
|
||||
}
|
||||
@@ -175,7 +177,7 @@ function linkEffects(dependencyGraph, prop, computation) {
|
||||
// Returns an array of IDs of the properties the effect targets
|
||||
export function getEffectTagTargets(effect, computation) {
|
||||
let targets = getTargetListFromTags(effect.targetTags, computation);
|
||||
let notIds = [];
|
||||
let notIds = [effect._id]; // Can't target itself
|
||||
if (effect.extraTags) {
|
||||
effect.extraTags.forEach(ex => {
|
||||
if (ex.operation === 'OR') {
|
||||
@@ -257,21 +259,23 @@ function linkDamageMultiplier(dependencyGraph, prop) {
|
||||
function linkPointBuy(dependencyGraph, prop) {
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'min' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'max' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'cost' });
|
||||
dependOnCalc({ dependencyGraph, prop, key: 'total' });
|
||||
prop.values?.forEach(row => {
|
||||
|
||||
prop.values?.forEach((row, index) => {
|
||||
// Get a unique id for the row because it might be shared among duplicated point buy tables
|
||||
// prop._id is forced unique by the database, so it can be used instead
|
||||
const uniqueRowId = prop._id + '_row_' + index;
|
||||
// Wrap the document in a new object so we don't bash it unintentionally
|
||||
const pointBuyRow = {
|
||||
...row,
|
||||
_id: uniqueRowId,
|
||||
type: 'pointBuyRow',
|
||||
tableName: prop.name,
|
||||
tableId: prop._id,
|
||||
}
|
||||
dependencyGraph.addNode(row._id, pointBuyRow);
|
||||
dependencyGraph.addNode(pointBuyRow._id, pointBuyRow);
|
||||
linkVariableName(dependencyGraph, pointBuyRow);
|
||||
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.min' });
|
||||
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.max' });
|
||||
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.cost' });
|
||||
dependencyGraph.addLink(pointBuyRow._id, prop._id, 'pointBuyRow');
|
||||
});
|
||||
if (prop.inactive) return;
|
||||
}
|
||||
@@ -297,7 +301,7 @@ function linkProficiencies(dependencyGraph, prop, computation) {
|
||||
// Otherwise target a field on that property
|
||||
const key = prop.targetField || getDefaultCalculationField(targetProp);
|
||||
const calcObj = get(targetProp, key);
|
||||
if (calcObj && calcObj.calculation) {
|
||||
if (calcObj) {
|
||||
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'proficiency');
|
||||
}
|
||||
}
|
||||
@@ -335,7 +339,7 @@ function linkSkill(dependencyGraph, prop, computation) {
|
||||
// other skill isn't supported
|
||||
const key = prop.targetField || getDefaultCalculationField(targetProp);
|
||||
const calcObj = get(targetProp, key);
|
||||
if (calcObj && calcObj.calculation) {
|
||||
if (calcObj) {
|
||||
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'proficiency');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
|
||||
import { prettifyParseError, parse } from '/imports/parser/parser.js';
|
||||
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
|
||||
import { get, unset } from 'lodash';
|
||||
import { get, set, unset } from 'lodash';
|
||||
import errorNode from '/imports/parser/parseTree/error.js';
|
||||
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
|
||||
|
||||
@@ -63,12 +63,21 @@ function parseAllCalculationFields(prop, schemas) {
|
||||
// For all fields matching they keys
|
||||
// supports `keys.$.with.$.arrays`
|
||||
applyFnToKey(prop, calcKey, (prop, key) => {
|
||||
const calcObj = get(prop, key);
|
||||
let calcObj = get(prop, key);
|
||||
// Create a calculation object if one doesn't exist, it will get deleted again later if
|
||||
// it's not used, but if an effect targets a calculated field, we should have one to target
|
||||
if (
|
||||
!calcObj
|
||||
&& subDocsExist(prop, key)
|
||||
) {
|
||||
calcObj = {};
|
||||
set(prop, key, calcObj);
|
||||
}
|
||||
// Sub document didn't exist, skip this field
|
||||
if (!calcObj) return;
|
||||
// Delete the whole calculation object if the calculation string isn't set
|
||||
// Keep a list of empty calculations for potential deletion if they aren't used
|
||||
if (!calcObj.calculation) {
|
||||
unset(prop, calcKey);
|
||||
return;
|
||||
prop._computationDetails.emptyCalculations.push(calcObj);
|
||||
}
|
||||
// Store a reference to all the calculations
|
||||
prop._computationDetails.calculations.push(calcObj);
|
||||
@@ -84,15 +93,31 @@ function parseAllCalculationFields(prop, schemas) {
|
||||
});
|
||||
}
|
||||
|
||||
function subDocsExist(prop, key) {
|
||||
const path = key.split('.');
|
||||
if (path.length < 2) return !!prop;
|
||||
path.pop();
|
||||
const subPath = path.join('.');
|
||||
return !!get(prop, subPath);
|
||||
}
|
||||
|
||||
export function removeEmptyCalculations(prop) {
|
||||
prop._computationDetails.emptyCalculations.forEach(calcObj => {
|
||||
if (!calcObj.effects?.length) {
|
||||
unset(prop, calcObj._key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseCalculation(calcObj) {
|
||||
const calcHash = cyrb53(calcObj.calculation);
|
||||
const calcHash = cyrb53(calcObj.calculation || '0');
|
||||
// If the cached parse calculation is equal to the calculation, skip
|
||||
if (calcHash === calcObj.hash) {
|
||||
return;
|
||||
}
|
||||
calcObj.hash = calcHash;
|
||||
try {
|
||||
calcObj.parseNode = parse(calcObj.calculation);
|
||||
calcObj.parseNode = parse(calcObj.calculation || '0');
|
||||
calcObj.parseError = null;
|
||||
} catch (e) {
|
||||
let error = {
|
||||
|
||||
@@ -75,6 +75,7 @@ export function buildComputationFromProps(properties, creature, variables) {
|
||||
// Add a place to store all the computation details
|
||||
prop._computationDetails = {
|
||||
calculations: [],
|
||||
emptyCalculations: [],
|
||||
inlineCalculations: [],
|
||||
toggleAncestors: [],
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export default function computeAction(computation, node) {
|
||||
const prop = node.data;
|
||||
if (prop.uses) {
|
||||
if (Number.isFinite(prop.uses?.value)) {
|
||||
prop.usesLeft = prop.uses.value - (prop.usesUsed || 0);
|
||||
if (!prop.usesLeft) {
|
||||
prop.insufficientResources = true;
|
||||
|
||||
@@ -3,8 +3,8 @@ import evaluateCalculation from '../../utility/evaluateCalculation.js';
|
||||
|
||||
export default function computePointBuy(computation, node) {
|
||||
const prop = node.data;
|
||||
const tableMin = prop.min?.value || null;
|
||||
const tableMax = prop.max?.value || null;
|
||||
const min = has(prop, 'min.value') ? prop.min.value : null;
|
||||
const max = has(prop, 'max.value') ? prop.max.value : null;
|
||||
prop.spent = 0;
|
||||
prop.values?.forEach(row => {
|
||||
// Clean up added properties
|
||||
@@ -14,9 +14,7 @@ export default function computePointBuy(computation, node) {
|
||||
|
||||
row.spent = 0;
|
||||
if (row.value === undefined) return;
|
||||
const min = has(row, 'min.value') ? row.min.value : tableMin;
|
||||
const max = has(row, 'max.value') ? row.max.value : tableMax;
|
||||
const costFunction = EJSON.clone(row.cost || prop.cost);
|
||||
const costFunction = EJSON.clone(prop.cost);
|
||||
if (costFunction) costFunction.parseLevel = 'reduce';
|
||||
|
||||
// Check min and max
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import computeToggles from '/imports/api/engine/computation/computeComputation/computeToggles.js';
|
||||
import computeByType from '/imports/api/engine/computation/computeComputation/computeByType.js';
|
||||
import embedInlineCalculations from './utility/embedInlineCalculations.js';
|
||||
import { removeEmptyCalculations } from './buildComputation/parseCalculationFields.js';
|
||||
import path from 'ngraph.path';
|
||||
|
||||
export default function computeCreatureComputation(computation){
|
||||
export default function computeCreatureComputation(computation) {
|
||||
const stack = [];
|
||||
// Computation scope of {variableName: prop}
|
||||
const graph = computation.dependencyGraph;
|
||||
@@ -20,12 +21,12 @@ export default function computeCreatureComputation(computation){
|
||||
stack.reverse();
|
||||
|
||||
// Depth first traversal of nodes
|
||||
while (stack.length){
|
||||
while (stack.length) {
|
||||
let top = stack[stack.length - 1];
|
||||
if (top._visited){
|
||||
if (top._visited) {
|
||||
// The object has already been computed, skip
|
||||
stack.pop();
|
||||
} else if (top._visitedChildren){
|
||||
} else if (top._visitedChildren) {
|
||||
// Mark the object as visited and remove from stack
|
||||
top._visited = true;
|
||||
stack.pop();
|
||||
@@ -42,14 +43,14 @@ export default function computeCreatureComputation(computation){
|
||||
computation.props.forEach(finalizeProp);
|
||||
}
|
||||
|
||||
function compute(computation, node){
|
||||
function compute(computation, node) {
|
||||
// Determine the prop's active status by its toggles
|
||||
computeToggles(computation, node);
|
||||
// Compute the property by type
|
||||
computeByType[node.data?.type || '_variable']?.(computation, node);
|
||||
}
|
||||
|
||||
function pushDependenciesToStack(nodeId, graph, stack, computation){
|
||||
function pushDependenciesToStack(nodeId, graph, stack, computation) {
|
||||
graph.forEachLinkedNode(nodeId, linkedNode => {
|
||||
if (linkedNode._visitedChildren && !linkedNode._visited) {
|
||||
// This is a dependency loop, find a path from the node to itself
|
||||
@@ -80,11 +81,13 @@ function pushDependenciesToStack(nodeId, graph, stack, computation){
|
||||
}, true);
|
||||
}
|
||||
|
||||
function finalizeProp(prop){
|
||||
function finalizeProp(prop) {
|
||||
// Embed the inline calculations
|
||||
prop._computationDetails?.inlineCalculations?.forEach(inlineCalcObj => {
|
||||
embedInlineCalculations(inlineCalcObj);
|
||||
});
|
||||
// Clean up the calculations that were never used
|
||||
removeEmptyCalculations(prop);
|
||||
// Clean up the computation details
|
||||
delete prop._computationDetails;
|
||||
}
|
||||
|
||||
@@ -67,12 +67,13 @@ const duplicateLibraryNode = new ValidatedMethod({
|
||||
});
|
||||
|
||||
// Give the docs new IDs without breaking internal references
|
||||
renewDocIds({ docArray: nodes });
|
||||
const allNodes = [libraryNode, ...nodes];
|
||||
renewDocIds({ docArray: allNodes });
|
||||
|
||||
// Order the root node
|
||||
libraryNode.order += 0.5;
|
||||
|
||||
LibraryNodes.batchInsert([libraryNode, ...nodes]);
|
||||
LibraryNodes.batchInsert(allNodes);
|
||||
|
||||
// Tree structure changed by inserts, reorder the tree
|
||||
reorderDocs({
|
||||
|
||||
@@ -33,10 +33,23 @@ const DamageSchema = createPropertySchema({
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
// remove the entire object if there is no saving throw
|
||||
save: {
|
||||
type: SavingThrowSchema,
|
||||
type: Object,
|
||||
optional: true,
|
||||
},
|
||||
// The computed DC
|
||||
'save.dc': {
|
||||
type: 'fieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
// The variable name of save to roll
|
||||
'save.stat': {
|
||||
type: String,
|
||||
optional: true,
|
||||
max: STORAGE_LIMITS.variableName,
|
||||
},
|
||||
// The damage to deal on a successful save
|
||||
'save.damageFunction': {
|
||||
type: 'fieldToCompute',
|
||||
optional: true,
|
||||
@@ -51,13 +64,18 @@ const ComputedOnlyDamageSchema = createPropertySchema({
|
||||
parseLevel: 'compile',
|
||||
},
|
||||
save: {
|
||||
type: ComputedOnlySavingThrowSchema,
|
||||
type: Object,
|
||||
optional: true,
|
||||
},
|
||||
'save.dc': {
|
||||
type: 'computedOnlyField',
|
||||
parseLevel: 'compile',
|
||||
optional: true,
|
||||
},
|
||||
'save.damageFunction': {
|
||||
type: 'computedOnlyField',
|
||||
optional: true,
|
||||
parseLevel: 'compile',
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -55,6 +55,11 @@ const ItemSchema = createPropertySchema({
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
// Prevent the property from showing up in the log
|
||||
silent: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
});
|
||||
|
||||
let ComputedOnlyItemSchema = createPropertySchema({
|
||||
|
||||
@@ -29,7 +29,7 @@ let PointBuySchema = createPropertySchema({
|
||||
'values.$._id': {
|
||||
type: String,
|
||||
regEx: SimpleSchema.RegEx.Id,
|
||||
autoValue(){
|
||||
autoValue() {
|
||||
if (!this.isSet) return Random.id();
|
||||
}
|
||||
},
|
||||
@@ -49,18 +49,6 @@ let PointBuySchema = createPropertySchema({
|
||||
type: Number,
|
||||
optional: true,
|
||||
},
|
||||
'values.$.min': {
|
||||
type: 'fieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
'values.$.max': {
|
||||
type: 'fieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
'values.$.cost': {
|
||||
type: 'fieldToCompute',
|
||||
optional: true,
|
||||
},
|
||||
min: {
|
||||
type: 'fieldToCompute',
|
||||
optional: true,
|
||||
@@ -102,19 +90,6 @@ const ComputedOnlyPointBuySchema = createPropertySchema({
|
||||
'values.$': {
|
||||
type: Object,
|
||||
},
|
||||
'values.$.min': {
|
||||
type: 'computedOnlyField',
|
||||
optional: true,
|
||||
},
|
||||
'values.$.max': {
|
||||
type: 'computedOnlyField',
|
||||
optional: true,
|
||||
},
|
||||
'values.$.cost': {
|
||||
type: 'computedOnlyField',
|
||||
optional: true,
|
||||
parseLevel: 'compile',
|
||||
},
|
||||
'values.$.spent': {
|
||||
type: Number,
|
||||
optional: true,
|
||||
|
||||
@@ -23,6 +23,7 @@ const timingOptions = {
|
||||
|
||||
const actionPropertyTypeOptions = {
|
||||
action: 'Action',
|
||||
ammo: 'Ammo used',
|
||||
adjustment: 'Attribute damage',
|
||||
branch: 'Branch',
|
||||
buff: 'Buff',
|
||||
|
||||
@@ -32,7 +32,10 @@
|
||||
light
|
||||
>
|
||||
<div class="page pa-3">
|
||||
<div class="title-block px-3 d-flex align-center">
|
||||
<div
|
||||
class="title-block px-3 d-flex align-center"
|
||||
style="page-break-after: avoid;"
|
||||
>
|
||||
<div class="logo-background" />
|
||||
<div class="creature-name mr-3">
|
||||
{{ creature.name }}
|
||||
@@ -59,14 +62,18 @@
|
||||
</div>
|
||||
<div
|
||||
class="text-right mt-3 mr-4"
|
||||
style="font-size: 8pt; margin-bottom: -4px;"
|
||||
style="font-size: 8pt; margin-bottom: -4px; page-break-after: avoid;"
|
||||
>
|
||||
{{ creatureUrl }}
|
||||
</div>
|
||||
<printed-stats :creature-id="creatureId" />
|
||||
<printed-inventory :creature-id="creatureId" />
|
||||
<printed-inventory
|
||||
:creature-id="creatureId"
|
||||
class="page-break-before"
|
||||
/>
|
||||
<printed-spells
|
||||
v-if="!creature.settings.hideSpellsTab"
|
||||
class="page-break-before"
|
||||
:creature-id="creatureId"
|
||||
/>
|
||||
</div>
|
||||
@@ -234,7 +241,7 @@ export default {
|
||||
.character-sheet-printed {
|
||||
background: white;
|
||||
color: black;
|
||||
font-size: 11pt;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
.character-sheet-printed * {
|
||||
@@ -247,17 +254,31 @@ export default {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.character-sheet-printed p {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.character-sheet-printed .double-border > .label:first-child {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.character-sheet-printed .column-layout, .character-sheet-printed .column-layout.wide-columns {
|
||||
position:relative;
|
||||
width: 100%;
|
||||
widows: 0;
|
||||
orphans: 0;
|
||||
-webkit-column-fill: balance-all;
|
||||
column-fill: balance-all;
|
||||
column-fill: balance;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.character-sheet-printed .column-layout {
|
||||
column-width: 200px;
|
||||
}
|
||||
|
||||
.character-sheet-printed .column-layout>div {
|
||||
position:relative;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.character-sheet-printed .column-layout > div > * {
|
||||
page-break-inside: avoid;
|
||||
@@ -267,9 +288,10 @@ export default {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.character-sheet-printed .creature-name {
|
||||
font-size: 24pt;
|
||||
font-size: 16pt;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.character-sheet-printed .logo-background {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
@@ -284,13 +306,20 @@ export default {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.character-sheet-printed .tree-node-title {
|
||||
min-height: unset !important;
|
||||
}
|
||||
|
||||
.character-sheet-printed .double-border {
|
||||
position: relative;
|
||||
padding: 11px 10px;
|
||||
border-style: solid;
|
||||
border-width: 11px 10px;
|
||||
border-image-source: url(/images/print/doubleLineImageBorder.png);
|
||||
border-image-slice: 110 126 fill;
|
||||
border-image-width: 16px;
|
||||
border-image-repeat: stretch;
|
||||
box-decoration-break: clone;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.character-sheet-printed .octagon-border {
|
||||
@@ -298,6 +327,8 @@ export default {
|
||||
padding: 4px 20px;
|
||||
border-image: url(/images/print/octagonBorder.png) 124 118 fill;
|
||||
border-image-width: 22px;
|
||||
box-decoration-break: clone;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.character-sheet-printed .span-all {
|
||||
@@ -311,7 +342,7 @@ export default {
|
||||
|
||||
.character-sheet-printed .stats .label {
|
||||
font-size: 10pt;
|
||||
font-variant: small-caps;
|
||||
font-variant: all-small-caps
|
||||
}
|
||||
|
||||
.character-sheet-printed .label {
|
||||
@@ -322,6 +353,15 @@ export default {
|
||||
|
||||
.character-sheet-printed .span-all {
|
||||
column-span: all;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.character-sheet-printed .page-break-before {
|
||||
page-break-before: always;
|
||||
}
|
||||
|
||||
.character-sheet-printed .avoid-page-break-after {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
@media screen {
|
||||
@@ -337,7 +377,7 @@ export default {
|
||||
@media print {
|
||||
@page {
|
||||
size: auto;
|
||||
margin: 8mm 8mm 8mm 8mm;
|
||||
margin: 8mm;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
<div
|
||||
class="inventory"
|
||||
>
|
||||
<column-layout wide-columns>
|
||||
<div class="span-all">
|
||||
<div class="double-border">
|
||||
<div class="double-border my-2">
|
||||
<div class="label text-center">
|
||||
Inventory
|
||||
</div>
|
||||
@@ -21,65 +19,56 @@
|
||||
:value="variables && variables.valueTotal && variables.valueTotal.value|| 0"
|
||||
/>
|
||||
</div>
|
||||
<div class="d-flex inventory-stat">
|
||||
<div
|
||||
v-if="variables.itemsAttuned && variables.itemsAttuned.value"
|
||||
class="d-flex inventory-stat"
|
||||
>
|
||||
<v-icon>$vuetify.icons.spell</v-icon>
|
||||
Items attuned:
|
||||
{{ variables.itemsAttuned && variables.itemsAttuned.value }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="span-all">
|
||||
<div class="octagon-border label text-center">
|
||||
<div class="double-border my-2">
|
||||
<div class="label text-center">
|
||||
Equipped
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
<column-layout wide-columns>
|
||||
<printed-item
|
||||
v-for="item in equippedItems"
|
||||
:key="item._id"
|
||||
>
|
||||
<printed-item
|
||||
class="double-border"
|
||||
:model="item"
|
||||
/>
|
||||
</column-layout>
|
||||
</div>
|
||||
<div class="span-all">
|
||||
<div class="octagon-border label text-center">
|
||||
<div class="double-border my-2">
|
||||
<div class="label text-center">
|
||||
Carried
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
<column-layout wide-columns>
|
||||
<printed-item
|
||||
v-for="item in carriedItems"
|
||||
:key="item._id"
|
||||
>
|
||||
<printed-item
|
||||
class="double-border"
|
||||
:model="item"
|
||||
/>
|
||||
</column-layout>
|
||||
</div>
|
||||
<template
|
||||
v-for="container in containersWithoutAncestorContainers"
|
||||
>
|
||||
<div
|
||||
v-for="container in containersWithoutAncestorContainers"
|
||||
:key="container._id"
|
||||
class="span-all"
|
||||
class="double-border my-2"
|
||||
>
|
||||
<printed-container
|
||||
class="octagon-border"
|
||||
:model="container"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
<column-layout wide-columns>
|
||||
<printed-item
|
||||
v-for="item in container.items"
|
||||
:key="item._id"
|
||||
>
|
||||
<printed-item
|
||||
class="double-border"
|
||||
:model="item"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</column-layout>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
@@ -90,7 +79,7 @@ import getParentRefByTag from '/imports/api/creature/creatureProperties/methods/
|
||||
import BUILT_IN_TAGS from '/imports/constants/BUILT_IN_TAGS.js';
|
||||
import CoinValue from '/imports/client/ui/components/CoinValue.vue';
|
||||
import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js';
|
||||
import PrintedItem from '/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedItem.vue';
|
||||
import PrintedLineItem from '/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedLineItem.vue';
|
||||
import PrintedContainer from '/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedContainer.vue';
|
||||
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables.js';
|
||||
|
||||
@@ -98,7 +87,7 @@ export default {
|
||||
components: {
|
||||
ColumnLayout,
|
||||
CoinValue,
|
||||
PrintedItem,
|
||||
PrintedItem: PrintedLineItem,
|
||||
PrintedContainer,
|
||||
},
|
||||
props: {
|
||||
@@ -233,15 +222,17 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
|
||||
.label {
|
||||
font-size: 14pt;
|
||||
font-size: 12pt;
|
||||
font-variant: small-caps;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.inventory .double-border {
|
||||
box-decoration-break: slice;
|
||||
}
|
||||
.inventory-stat {
|
||||
font-size: 12pt;
|
||||
font-size: 11pt;
|
||||
line-height: 32px;
|
||||
}
|
||||
.inventory-stat > .v-icon {
|
||||
|
||||
@@ -2,38 +2,39 @@
|
||||
<div
|
||||
class="spells"
|
||||
>
|
||||
<column-layout wide-columns>
|
||||
<div class="span-all">
|
||||
<div class="label text-center octagon-border">
|
||||
<div
|
||||
class="label text-center octagon-border my-2 avoid-page-break-after"
|
||||
>
|
||||
Spells
|
||||
</div>
|
||||
</div>
|
||||
<column-layout
|
||||
v-if="spellsWithoutList && spellsWithoutList.length"
|
||||
wide-columns
|
||||
>
|
||||
<div
|
||||
v-for="spell in spellsWithoutList"
|
||||
:key="spell._id"
|
||||
>
|
||||
<printed-spell :model="spell" />
|
||||
</div>
|
||||
<template
|
||||
v-for="spellList in spellListsWithoutAncestorSpellLists"
|
||||
>
|
||||
</column-layout>
|
||||
<div
|
||||
v-for="spellList in spellListsWithoutAncestorSpellLists"
|
||||
:key="spellList._id"
|
||||
class="span-all"
|
||||
>
|
||||
<printed-spell-list
|
||||
:model="spellList"
|
||||
/>
|
||||
</div>
|
||||
<column-layout wide-columns>
|
||||
<div
|
||||
v-for="spell in spellList.spells"
|
||||
:key="spell._id"
|
||||
>
|
||||
<printed-spell :model="spell" />
|
||||
</div>
|
||||
</template>
|
||||
</column-layout>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
<template lang="html">
|
||||
<div class="stats">
|
||||
<column-layout>
|
||||
<div
|
||||
class="d-flex wrap justify-space-between px-2 pt-3 pb-1"
|
||||
style="page-break-after: avoid"
|
||||
>
|
||||
<div
|
||||
v-for="ability in abilities"
|
||||
:key="ability._id"
|
||||
>
|
||||
<div
|
||||
class="ability"
|
||||
class="ability ma-0"
|
||||
>
|
||||
<div class="score">
|
||||
<div class="double-border top big-number">
|
||||
<div class="double-border top">
|
||||
<div
|
||||
class="label text-center mb-0"
|
||||
style="line-height: 16px"
|
||||
>
|
||||
{{ ability.name }}
|
||||
</div>
|
||||
<div class="big-number mb-1">
|
||||
<template v-if="creature.settings.swapScoresAndMods">
|
||||
{{ ability.total }}
|
||||
</template>
|
||||
@@ -17,6 +27,7 @@
|
||||
{{ numberToSignedString(ability.modifier) }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<template v-if="creature.settings.swapScoresAndMods">
|
||||
{{ numberToSignedString(ability.modifier) }}
|
||||
@@ -26,12 +37,10 @@
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="double-border name label">
|
||||
{{ ability.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<column-layout>
|
||||
<div
|
||||
v-for="toggle in toggles"
|
||||
:key="toggle._id"
|
||||
@@ -102,10 +111,15 @@
|
||||
:key="healthBar._id"
|
||||
>
|
||||
<div class="double-border">
|
||||
<div class="label">
|
||||
Total: {{ healthBar.total }}
|
||||
<div>
|
||||
<b>
|
||||
Total:
|
||||
</b>
|
||||
<span>
|
||||
{{ healthBar.total }}
|
||||
</span>
|
||||
</div>
|
||||
<div style="height: 60px;" />
|
||||
<div style="height: 40px;" />
|
||||
<div
|
||||
style="text-align: center;"
|
||||
class="label"
|
||||
@@ -125,9 +139,9 @@
|
||||
>
|
||||
<div class="double-border">
|
||||
<div>
|
||||
<span class="label">
|
||||
<b>
|
||||
Total:
|
||||
</span>
|
||||
</b>
|
||||
<span
|
||||
v-for="hitDie in hitDice"
|
||||
:key="hitDie._id"
|
||||
@@ -136,7 +150,7 @@
|
||||
{{ hitDie.total }}{{ hitDie.hitDiceSize }}
|
||||
</span>
|
||||
</div>
|
||||
<div style="height: 60px;" />
|
||||
<div style="height: 40px;" />
|
||||
<div
|
||||
style="text-align: center;"
|
||||
class="label"
|
||||
@@ -156,7 +170,7 @@
|
||||
>
|
||||
<div
|
||||
v-if="resource.total <= 8"
|
||||
class="label"
|
||||
class="label mb-0"
|
||||
>
|
||||
{{ resource.name }}
|
||||
</div>
|
||||
@@ -169,6 +183,7 @@
|
||||
<div
|
||||
v-if="resource.total <= 8"
|
||||
class="d-flex justify-end"
|
||||
style="margin-top: -4px"
|
||||
>
|
||||
<div
|
||||
v-for="i in resource.total"
|
||||
@@ -185,6 +200,77 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="savingThrows.length"
|
||||
>
|
||||
<div
|
||||
class="double-border"
|
||||
>
|
||||
<printed-skill
|
||||
v-for="save in savingThrows"
|
||||
:key="save._id"
|
||||
:model="save"
|
||||
:data-id="save._id"
|
||||
/>
|
||||
<div
|
||||
v-for="(effect) in saveConditionals"
|
||||
:key="effect._id"
|
||||
class="mt-2"
|
||||
>
|
||||
* {{ effect.text }}
|
||||
</div>
|
||||
<div class="label text-center">
|
||||
Saving Throws
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="skills.length"
|
||||
>
|
||||
<div
|
||||
class="double-border"
|
||||
>
|
||||
<printed-skill
|
||||
v-for="skill in skills"
|
||||
:key="skill._id"
|
||||
:model="skill"
|
||||
:data-id="skill._id"
|
||||
/>
|
||||
<div
|
||||
v-for="(effect) in skillConditionals"
|
||||
:key="effect._id"
|
||||
class="mt-2"
|
||||
>
|
||||
* {{ effect.text }}
|
||||
</div>
|
||||
<div class="label text-center">
|
||||
Skills
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="double-border"
|
||||
>
|
||||
<p>
|
||||
<b>Weapons:</b> {{ weapons.map(p => p.name).join(', ') }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Armor:</b> {{ armors.map(p => p.name).join(', ') }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Tools:</b> {{ tools.map(p => p.name).join(', ') }}
|
||||
</p>
|
||||
<p>
|
||||
<b>Languages:</b> {{ languages.map(p => p.name).join(', ') }}
|
||||
</p>
|
||||
<div class="label text-center">
|
||||
Proficiencies
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="spellSlots && spellSlots.length"
|
||||
>
|
||||
@@ -220,114 +306,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="savingThrows.length"
|
||||
>
|
||||
<div
|
||||
class="double-border"
|
||||
>
|
||||
<printed-skill
|
||||
v-for="save in savingThrows"
|
||||
:key="save._id"
|
||||
:model="save"
|
||||
:data-id="save._id"
|
||||
/>
|
||||
<div class="label text-center">
|
||||
Saving Throws
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="skills.length"
|
||||
>
|
||||
<div
|
||||
class="double-border"
|
||||
>
|
||||
<printed-skill
|
||||
v-for="skill in skills"
|
||||
:key="skill._id"
|
||||
:model="skill"
|
||||
:data-id="skill._id"
|
||||
/>
|
||||
<div class="label text-center">
|
||||
Skills
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="weapons && weapons.length"
|
||||
>
|
||||
<div
|
||||
class="double-border"
|
||||
>
|
||||
<printed-skill
|
||||
v-for="weapon in weapons"
|
||||
:key="weapon._id"
|
||||
hide-modifier
|
||||
:model="weapon"
|
||||
:data-id="weapon._id"
|
||||
/>
|
||||
<div class="label text-center">
|
||||
Weapons
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="armors && armors.length"
|
||||
>
|
||||
<div
|
||||
class="double-border"
|
||||
>
|
||||
<printed-skill
|
||||
v-for="armor in armors"
|
||||
:key="armor._id"
|
||||
hide-modifier
|
||||
:model="armor"
|
||||
:data-id="armor._id"
|
||||
/>
|
||||
<div class="label text-center">
|
||||
Armor
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="tools && tools.length"
|
||||
>
|
||||
<div
|
||||
class="double-border"
|
||||
>
|
||||
<printed-skill
|
||||
v-for="tool in tools"
|
||||
:key="tool._id"
|
||||
hide-modifier
|
||||
:model="tool"
|
||||
:data-id="tool._id"
|
||||
/>
|
||||
<div class="label text-center">
|
||||
Tools
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="languages && languages.length"
|
||||
>
|
||||
<div
|
||||
class="double-border"
|
||||
>
|
||||
<printed-skill
|
||||
v-for="language in languages"
|
||||
:key="language._id"
|
||||
hide-modifier
|
||||
:model="language"
|
||||
:data-id="language._id"
|
||||
/>
|
||||
<div class="label text-center">
|
||||
Languages
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="note in notes"
|
||||
:key="note._id"
|
||||
@@ -381,6 +359,7 @@ import numberToSignedString from '../../../../../api/utility/numberToSignedStrin
|
||||
import PrintedSkill from '/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedSkill.vue';
|
||||
import PrintedDamageMultipliers from '/imports/client/ui/creature/character/printedCharacterSheet/components/PrintedDamageMultipliers.vue';
|
||||
import PropertyDescription from '/imports/client/ui/properties/viewers/shared/PropertyDescription.vue';
|
||||
import { uniqBy } from 'lodash';
|
||||
|
||||
const getProperties = function (creature, filter, options = {
|
||||
sort: { order: 1 }
|
||||
@@ -483,9 +462,31 @@ export default {
|
||||
savingThrows() {
|
||||
return getSkillOfType(this.creature, 'save');
|
||||
},
|
||||
saveConditionals(){
|
||||
const conditionals = [];
|
||||
this.savingThrows?.forEach(prop => {
|
||||
prop?.effects?.forEach(effect => {
|
||||
if (effect.operation === 'conditional') {
|
||||
conditionals.push(effect);
|
||||
}
|
||||
});
|
||||
});
|
||||
return uniqBy(conditionals, '_id');
|
||||
},
|
||||
skills() {
|
||||
return getSkillOfType(this.creature, 'skill');
|
||||
},
|
||||
skillConditionals(){
|
||||
const conditionals = [];
|
||||
this.skills?.forEach(prop => {
|
||||
prop?.effects?.forEach(effect => {
|
||||
if (effect.operation === 'conditional') {
|
||||
conditionals.push(effect);
|
||||
}
|
||||
});
|
||||
});
|
||||
return uniqBy(conditionals, '_id');
|
||||
},
|
||||
tools() {
|
||||
return getSkillOfType(this.creature, 'tool');
|
||||
},
|
||||
@@ -517,7 +518,15 @@ export default {
|
||||
return getProperties(this.creature, { type: 'feature' });
|
||||
},
|
||||
notes(){
|
||||
return getProperties(this.creature, { type: 'note', summary: {$exists: true} });
|
||||
const allNoteIds = getProperties(this.creature, {
|
||||
type: 'note',
|
||||
}).map(note => note._id);
|
||||
const topLevelNotes = getProperties(this.creature, {
|
||||
type: 'note',
|
||||
summary: { $exists: true },
|
||||
'ancestor.id': {$nin: allNoteIds}
|
||||
});
|
||||
return topLevelNotes;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -557,10 +566,9 @@ export default {
|
||||
align-items: center;
|
||||
}
|
||||
.ability .top {
|
||||
min-width: 64px;
|
||||
min-width: 86px;
|
||||
text-align: center;
|
||||
margin-bottom: -10px;
|
||||
padding: 14px;
|
||||
margin: 4px 4px -10px;
|
||||
z-index: 1;
|
||||
}
|
||||
.ability .bottom {
|
||||
|
||||
@@ -3,35 +3,20 @@
|
||||
class="action-card"
|
||||
:class="cardClasses"
|
||||
>
|
||||
<div class="label text-center">
|
||||
{{ actionTypeName }}
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<div class="avatar">
|
||||
<div
|
||||
v-if="rollBonus"
|
||||
>
|
||||
<template v-if="rollBonus && !rollBonusTooLong">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<div class="roll-bonus">
|
||||
<template v-if="!onHitDamage && rollBonus">
|
||||
{{ rollBonus }}
|
||||
</template>
|
||||
<property-icon
|
||||
v-else
|
||||
:model="model"
|
||||
color="rgba(0,0,0,0.7)"
|
||||
/>
|
||||
</div>
|
||||
<property-icon
|
||||
v-else
|
||||
:model="model"
|
||||
color="rgba(0,0,0,0.7)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="action-header flex d-flex column justify-center pl-1"
|
||||
>
|
||||
<div class="action-title my-1">
|
||||
<div class="action-title text-center flex-grow-1">
|
||||
{{ model.name || propertyName }}
|
||||
</div>
|
||||
<div class="avatar">
|
||||
<property-icon
|
||||
:model="model"
|
||||
color="rgba(0,0,0,0.7)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -40,7 +25,7 @@
|
||||
>
|
||||
{{ model.uses }} uses
|
||||
</div>
|
||||
<div class="pb-3">
|
||||
<div>
|
||||
<div
|
||||
v-if="model.resources && model.resources.attributesConsumed.length ||
|
||||
model.resources.itemsConsumed.length"
|
||||
@@ -65,20 +50,41 @@
|
||||
<template v-if="model.summary">
|
||||
<markdown-text :markdown="model.summary.value || model.summary.text" />
|
||||
</template>
|
||||
<v-divider v-if="children && children.length" />
|
||||
<div
|
||||
v-if="onHitDamage"
|
||||
>
|
||||
<span class="damage">
|
||||
{{ rollBonus }}
|
||||
</span>
|
||||
<span>
|
||||
to hit
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="onHitDamage">
|
||||
<span class="damage">
|
||||
{{ onHitDamage.damage }}
|
||||
</span>
|
||||
<span>
|
||||
{{ onHitDamage.suffix }}
|
||||
</span>
|
||||
</div>
|
||||
<tree-node-list
|
||||
v-if="children && children.length"
|
||||
v-else-if="children && children.length"
|
||||
start-expanded
|
||||
show-external-details
|
||||
:children="children"
|
||||
@selected="e => $emit('sub-click', e)"
|
||||
/>
|
||||
</div>
|
||||
<div class="action-subtitle text-center">
|
||||
{{ actionTypeName }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
|
||||
import numberToSignedString from '../../../../../../api/utility/numberToSignedString.js';
|
||||
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
|
||||
import AttributeConsumedView from '/imports/client/ui/properties/components/actions/AttributeConsumedView.vue';
|
||||
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
|
||||
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
||||
@@ -87,6 +93,8 @@ import TreeNodeList from '/imports/client/ui/components/tree/TreeNodeList.vue';
|
||||
import { nodeArrayToTree } from '/imports/api/parenting/nodesToTree.js';
|
||||
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
|
||||
import { some } from 'lodash';
|
||||
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
|
||||
import resolve, { Context, toString } from '/imports/parser/resolve.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -148,7 +156,24 @@ export default {
|
||||
'free': 'Free Action',
|
||||
'long': 'Long Action'
|
||||
}[this.model.actionType] || this.model.actionType
|
||||
}
|
||||
},
|
||||
onHitDamage() {
|
||||
/**
|
||||
* Only match a property who has exactly one to-hit child with one damage under that
|
||||
*/
|
||||
if (this.children?.length !== 1) return;
|
||||
if (this.children[0]?.node?.type !== 'branch') return;
|
||||
if (this.children[0].children?.length !== 1) return;
|
||||
if (this.children[0].children[0]?.node?.type !== 'damage') return;
|
||||
if (this.children[0].children[0].children?.length !== 0) return;
|
||||
const damage = this.children[0].children[0]?.node;
|
||||
applyEffectsToCalculationParseNode(damage.amount);
|
||||
const { result } = resolve('compile', damage.amount.parseNode, {});
|
||||
return {
|
||||
damage: toString(result),
|
||||
suffix: damage.damageType + (damage.damageType !== 'healing' ? ' damage ' : '')
|
||||
};
|
||||
},
|
||||
},
|
||||
meteor: {
|
||||
children() {
|
||||
@@ -187,28 +212,39 @@ export default {
|
||||
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1),
|
||||
transform 0.075s ease;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
.roll-bonus {
|
||||
font-size: 18pt;
|
||||
text-align: center;
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
flex-basis: 24px;
|
||||
}
|
||||
.avatar {
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 10pt;
|
||||
font-variant: small-caps;
|
||||
font-variant: all-small-caps;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.damage {
|
||||
font-size: 12pt;
|
||||
font-weight: 500;
|
||||
}
|
||||
.action-title {
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
transition: .3s cubic-bezier(.25, .8, .5, 1);
|
||||
width: 100%;
|
||||
font-size: 12pt;
|
||||
font-weight: 600;
|
||||
min-height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-variant: all-small-caps;
|
||||
}
|
||||
|
||||
.action-subtitle {
|
||||
font-variant: all-small-caps;
|
||||
font-size: 11pt;
|
||||
}
|
||||
|
||||
.resources {
|
||||
|
||||
@@ -88,11 +88,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<property-description
|
||||
text
|
||||
:model="model.description"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<template lang="html">
|
||||
<div class="printed-line-item d-flex align-start mb-0">
|
||||
<div class="quantity">
|
||||
{{ model.quantity !== 1 ? model.quantity : undefined }}
|
||||
</div>
|
||||
<div class="text flex-grow-1">
|
||||
{{ title }}
|
||||
<template v-if="attunementText">
|
||||
({{ attunementText }})
|
||||
</template>
|
||||
</div>
|
||||
<div class="weight-value d-flex flex-column align-end">
|
||||
<div
|
||||
v-if="model.quantity !== 1"
|
||||
class="each d-flex align-center"
|
||||
>
|
||||
<coin-value
|
||||
v-if="model.value"
|
||||
class="value text-no-wrap"
|
||||
:value="model.value"
|
||||
/>
|
||||
<div
|
||||
class="weight text-no-wrap"
|
||||
>
|
||||
<template
|
||||
v-if="model.weight"
|
||||
>
|
||||
{{ model.weight }} lb
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="total d-flex align-center">
|
||||
<coin-value
|
||||
v-if="totalValue"
|
||||
class="value text-no-wrap"
|
||||
:value="totalValue"
|
||||
/>
|
||||
<div
|
||||
class="weight text-no-wrap"
|
||||
>
|
||||
<template
|
||||
v-if="model.weight"
|
||||
>
|
||||
{{ totalWeight }} lb
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import PROPERTIES from '/imports/constants/PROPERTIES.js';
|
||||
import stripFloatingPointOddities from '/imports/api/engine/computation/utility/stripFloatingPointOddities.js';
|
||||
import CoinValue from '/imports/client/ui/components/CoinValue.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CoinValue,
|
||||
},
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
let model = this.model;
|
||||
if (!model) return;
|
||||
if (model.quantity !== 1) {
|
||||
if (model.plural) {
|
||||
return model.plural;
|
||||
} else if (model.name) {
|
||||
return model.name;
|
||||
}
|
||||
} else if (model.name) {
|
||||
return model.name;
|
||||
}
|
||||
let prop = PROPERTIES[model.type]
|
||||
return prop && prop.name;
|
||||
},
|
||||
totalValue() {
|
||||
return stripFloatingPointOddities(this.model.value * this.model.quantity);
|
||||
},
|
||||
totalWeight() {
|
||||
return stripFloatingPointOddities(this.model.weight * this.model.quantity);
|
||||
},
|
||||
attunementText() {
|
||||
if (this.model.requiresAttunement) {
|
||||
if (this.model.attuned) return 'Attuned';
|
||||
return 'Requires attunement';
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.quantity {
|
||||
flex-basis: 32px;
|
||||
font-weight: 700;
|
||||
text-align: end;
|
||||
padding-right: 8px;
|
||||
}
|
||||
.each {
|
||||
font-weight: 300;
|
||||
}
|
||||
.total {
|
||||
font-weight: 500;
|
||||
}
|
||||
.value {
|
||||
min-width: 40px;
|
||||
text-align: end;
|
||||
}
|
||||
.weight {
|
||||
min-width: 40px;
|
||||
text-align: end;
|
||||
}
|
||||
</style>
|
||||
@@ -11,7 +11,7 @@
|
||||
:value="model.proficiency"
|
||||
class="prof-icon"
|
||||
/>
|
||||
<div class="prof-mod ml-2 mr-4 text-right">
|
||||
<div class="prof-mod mr-3 text-right">
|
||||
{{ displayedModifier }}
|
||||
</div>
|
||||
<v-icon
|
||||
@@ -88,7 +88,7 @@ export default {
|
||||
|
||||
<style lang="css" scoped>
|
||||
.printed-skill{
|
||||
min-height: 30px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.prof-icon {
|
||||
|
||||
@@ -2,35 +2,52 @@
|
||||
<div
|
||||
class="double-border"
|
||||
>
|
||||
<div class="d-flex align-center mb-2">
|
||||
<div class="spell-level">
|
||||
<div
|
||||
v-if="model.name"
|
||||
class="label"
|
||||
v-if="model.level"
|
||||
class="spell-level-number"
|
||||
>
|
||||
{{ model.name }}
|
||||
{{ romanLevel }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="spell-title text-center flex-grow-1">
|
||||
{{ model.name || propertyName }}
|
||||
</div>
|
||||
<div class="avatar">
|
||||
<property-icon
|
||||
:model="model"
|
||||
color="rgba(0,0,0,0.7)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="model.level">
|
||||
{{ levelText }} {{ model.school }} {{ model.ritual ? '(ritual)' : '' }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ model.school }} cantrip
|
||||
{{ model.school }} cantrip {{ model.ritual ? '(ritual)' : '' }}
|
||||
</div>
|
||||
<div
|
||||
v-if="rollBonus"
|
||||
>
|
||||
<b>To hit:</b> {{ rollBonus }}
|
||||
</div>
|
||||
<div>
|
||||
Casting Time: {{ model.castingTime }}
|
||||
<b>Casting time:</b> {{ model.castingTime }}
|
||||
</div>
|
||||
<div>
|
||||
Range: {{ model.range }}
|
||||
<b>Range:</b> {{ model.range }}
|
||||
</div>
|
||||
<div>
|
||||
Components: {{ spellComponents }}
|
||||
<b>Components:</b> {{ spellComponents }}
|
||||
</div>
|
||||
<div>
|
||||
Duration: {{ model.duration }}
|
||||
<div class="mb-4">
|
||||
<b>Duration:</b> {{ model.duration }}
|
||||
</div>
|
||||
<property-description
|
||||
text
|
||||
:model="model.summary"
|
||||
/>
|
||||
<v-divider class="my-2" />
|
||||
<property-description
|
||||
text
|
||||
:model="model.description"
|
||||
@@ -39,7 +56,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="js">
|
||||
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
|
||||
import PropertyDescription from '/imports/client/ui/properties/viewers/shared/PropertyDescription.vue';
|
||||
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
|
||||
import romanize from '/imports/client/ui/utility/romanize.js';
|
||||
|
||||
const levelText = [
|
||||
'cantrip', '1st-level', '2nd-level', '3rd-level', '4th-level', '5th-level',
|
||||
@@ -48,6 +68,7 @@ const levelText = [
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PropertyIcon,
|
||||
PropertyDescription,
|
||||
},
|
||||
props: {
|
||||
@@ -58,15 +79,21 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
levelText() {
|
||||
return levelText[this.model.level]
|
||||
return levelText[this.model.level] || `level ${this.model.level}`;
|
||||
},
|
||||
romanLevel() {
|
||||
return romanize(this.model.level) || this.model.level;
|
||||
},
|
||||
rollBonus() {
|
||||
if (!this.model.attackRoll) return;
|
||||
return numberToSignedString(this.model.attackRoll.value);
|
||||
},
|
||||
spellComponents() {
|
||||
let components = [];
|
||||
if (this.model.ritual) components.push('Ritual');
|
||||
if (this.model.concentration) components.push('Concentration');
|
||||
if (this.model.verbal) components.push('Verbal');
|
||||
if (this.model.somatic) components.push('Somatic');
|
||||
if (this.model.material) components.push(`Material (${this.model.material})`);
|
||||
if (this.model.concentration) components.push('C');
|
||||
if (this.model.verbal) components.push('V');
|
||||
if (this.model.somatic) components.push('S');
|
||||
if (this.model.material) components.push(`M (${this.model.material})`);
|
||||
return components.join(', ');
|
||||
},
|
||||
}
|
||||
@@ -74,9 +101,24 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.label {
|
||||
.spell-level {
|
||||
width: 24px;
|
||||
}
|
||||
.spell-level-number {
|
||||
font-size: 18pt;
|
||||
}
|
||||
.avatar {
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.spell-title {
|
||||
font-size: 14pt;
|
||||
font-variant: all-small-caps;
|
||||
font-weight: 600;
|
||||
min-height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-variant: all-small-caps;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="octagon-border">
|
||||
<div
|
||||
class="octagon-border my-1"
|
||||
style="page-break-after: avoid;"
|
||||
>
|
||||
<div class="label text-center">
|
||||
{{ model.name }}
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
@change="change('damageType', ...arguments)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-col cols="12">
|
||||
<smart-toggle
|
||||
label="Target creature"
|
||||
:value="model.target"
|
||||
@@ -42,14 +42,80 @@
|
||||
:error-messages="errors.target"
|
||||
@change="change('target', ...arguments)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<smart-switch
|
||||
class="mt-0"
|
||||
label="Saving throw"
|
||||
:value="!!model.save"
|
||||
:error-messages="errors.save"
|
||||
@change="(val, ack) => $emit('change', {
|
||||
path: ['save'],
|
||||
value: val ? {} : undefined,
|
||||
ack
|
||||
})"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-expand-transition>
|
||||
<v-row
|
||||
v-if="model.save"
|
||||
dense
|
||||
>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<computed-field
|
||||
label="DC"
|
||||
hint="Saving throw DC"
|
||||
:model="model.save.dc"
|
||||
:error-messages="errors['save.dc']"
|
||||
@change="({path, value, ack}) =>
|
||||
$emit('change', {path: ['save', 'dc', ...path], value, ack})"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<smart-combobox
|
||||
label="Save"
|
||||
hint="Which stat the saving throw targets"
|
||||
:value="model.save.stat"
|
||||
:items="saveList"
|
||||
:error-messages="errors['save.stat']"
|
||||
@change="(value, ack) =>
|
||||
$emit('change', {path: ['save', 'stat'], value, ack})"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<computed-field
|
||||
v-if="!!model.save"
|
||||
label="Damage on successful save"
|
||||
hint="Use "~damage" to reference the damage that would normally be dealt"
|
||||
placeholder="Half damage"
|
||||
persistent-placeholder
|
||||
:model="model.save.damageFunction"
|
||||
:error-messages="errors['save.damageFunction']"
|
||||
@change="({path, value, ack}) =>
|
||||
$emit('change', {path: ['save', 'damageFunction', ...path], value, ack})"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expand-transition>
|
||||
<form-sections type="damage">
|
||||
<form-section name="Log">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<smart-switch
|
||||
label="Don't show in log"
|
||||
:value="model.silent"
|
||||
:error-messages="errors.silent"
|
||||
@change="change('silent', ...arguments)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</form-section>
|
||||
<slot />
|
||||
</form-sections>
|
||||
@@ -60,9 +126,10 @@
|
||||
import DAMAGE_TYPES from '/imports/constants/DAMAGE_TYPES.js';
|
||||
import propertyFormMixin from '/imports/client/ui/properties/forms/shared/propertyFormMixin.js';
|
||||
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
||||
import saveListMixin from '/imports/client/ui/properties/forms/shared/lists/saveListMixin.js';
|
||||
|
||||
export default {
|
||||
mixins: [propertyFormMixin],
|
||||
mixins: [propertyFormMixin, saveListMixin],
|
||||
props: {
|
||||
parentTarget: {
|
||||
type: String,
|
||||
@@ -102,6 +169,13 @@ export default {
|
||||
return hints[this.model.target];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
saveChange({ path, value, ack }) {
|
||||
console.log({ path, value, ack });
|
||||
this.$emit('change', {path: [ 'save', ...path ], value, ack})
|
||||
this.$emit('change', {path: [ 'silent' ], value: true, ack})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -83,6 +83,11 @@
|
||||
<form-sections type="item">
|
||||
<form-section
|
||||
name="Behavior"
|
||||
>
|
||||
<v-row dense>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<smart-switch
|
||||
label="Show increment button"
|
||||
@@ -90,6 +95,19 @@
|
||||
:error-messages="errors.showIncrement"
|
||||
@change="change('showIncrement', ...arguments)"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<smart-switch
|
||||
label="Don't show in log"
|
||||
:value="model.silent"
|
||||
:error-messages="errors.silent"
|
||||
@change="change('silent', ...arguments)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</form-section>
|
||||
<form-section
|
||||
name="Attunement"
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
md="6"
|
||||
>
|
||||
<smart-toggle
|
||||
label="Target creature"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template lang="html">
|
||||
<div>
|
||||
<div class="layout align-center justify-start" style="height:40px;">
|
||||
<div
|
||||
class="layout align-center justify-start"
|
||||
style="height:40px;"
|
||||
>
|
||||
<v-icon
|
||||
v-if="!hideIcon"
|
||||
class="mr-2"
|
||||
@@ -15,19 +18,24 @@
|
||||
<span v-if="model.target === 'self'">to self</span>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="showExternalDetails">
|
||||
<div
|
||||
v-if="showExternalDetails"
|
||||
v-for="effect in model.amount.effects">
|
||||
<div v-if="effect.amount.value !== 0"
|
||||
style="position:relative; top:-15px; left:5px; height:25px;">
|
||||
<inline-effect
|
||||
hide-breadcrumbs
|
||||
v-for="effect in (model.amount && model.amount.effects)"
|
||||
:key="effect._id"
|
||||
>
|
||||
<div
|
||||
v-if="effect.amount.value !== 0"
|
||||
style="position:relative; top:-15px; left:5px; height:25px;"
|
||||
>
|
||||
<inline-effect
|
||||
:key="effect._id"
|
||||
hide-breadcrumbs
|
||||
:data-id="effect._id"
|
||||
:model="effect"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,8 +45,8 @@ import { getPropertyIcon } from '/imports/constants/PROPERTIES.js';
|
||||
import InlineEffect from '../components/effects/InlineEffect.vue';
|
||||
|
||||
export default {
|
||||
mixins: [treeNodeViewMixin],
|
||||
components: {InlineEffect},
|
||||
mixins: [treeNodeViewMixin],
|
||||
computed: {
|
||||
icon() {
|
||||
if (this.model.damageType === 'healing') {
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
:value="reset"
|
||||
/>
|
||||
<property-field
|
||||
v-if="model.resources.conditions.length"
|
||||
v-if="model.resources.conditions && model.resources.conditions.length"
|
||||
name="Conditions"
|
||||
>
|
||||
<div style="width: 100%;">
|
||||
|
||||
@@ -16,6 +16,23 @@
|
||||
name="Target"
|
||||
value="Self"
|
||||
/>
|
||||
<template v-if="model.save">
|
||||
<property-field
|
||||
name="DC"
|
||||
large
|
||||
center
|
||||
:calculation="model.save.dc"
|
||||
/>
|
||||
<property-field
|
||||
name="Save"
|
||||
mono
|
||||
:value="model.save.stat"
|
||||
/>
|
||||
<property-field
|
||||
name="On a successful saving throw"
|
||||
v-bind="saveDamage"
|
||||
/>
|
||||
</template>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
@@ -30,6 +47,16 @@ export default {
|
||||
if (this.model.damageType === 'healing') return this.model.damageType;
|
||||
return `${this.model.damageType} damage`
|
||||
},
|
||||
saveDamage() {
|
||||
if (!this.model.save) return;
|
||||
if (!this.model.save.damageFunction?.calculation) {
|
||||
return { value: 'Half damage' };
|
||||
}
|
||||
if (this.model.save.damageFunction.calculation == '0' || this.model.save.damageFunction.value === 0) {
|
||||
return { value: 'No damage' };
|
||||
}
|
||||
return { calculation: this.model.save.damageFunction };
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
31
app/imports/client/ui/utility/romanize.js
Normal file
31
app/imports/client/ui/utility/romanize.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const roman = {
|
||||
Ⅿ: 1000,
|
||||
ⅭⅯ: 900,
|
||||
Ⅾ: 500,
|
||||
ⅭⅮ: 400,
|
||||
Ⅽ: 100,
|
||||
ⅩⅭ: 90,
|
||||
Ⅼ: 50,
|
||||
XL: 40,
|
||||
Ⅻ: 12,
|
||||
Ⅺ: 11,
|
||||
Ⅹ: 10,
|
||||
Ⅸ: 9,
|
||||
Ⅷ: 8,
|
||||
Ⅶ: 7,
|
||||
Ⅵ: 6,
|
||||
Ⅴ: 5,
|
||||
Ⅳ: 4,
|
||||
Ⅲ: 3,
|
||||
Ⅱ: 2,
|
||||
Ⅰ: 1
|
||||
};
|
||||
export default function romanize(num) {
|
||||
var str = '';
|
||||
for (var i of Object.keys(roman)) {
|
||||
var q = Math.floor(num / roman[i]);
|
||||
num -= q * roman[i];
|
||||
str += i.repeat(q);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import constant from './constant.js';
|
||||
import { toString } from '../resolve.js';
|
||||
|
||||
const accessor = {
|
||||
create({name, path}) {
|
||||
create({ name, path }) {
|
||||
return {
|
||||
parseType: 'accessor',
|
||||
path,
|
||||
@@ -19,12 +19,12 @@ const accessor = {
|
||||
});
|
||||
let valueType = Array.isArray(value) ? 'array' : typeof value;
|
||||
// If the accessor returns an objet, get the object's value instead
|
||||
while (valueType === 'object'){
|
||||
while (valueType === 'object') {
|
||||
value = value.value;
|
||||
valueType = Array.isArray(value) ? 'array' : typeof value;
|
||||
}
|
||||
// Return a parse node based on the type returned
|
||||
if (valueType === 'string' || valueType === 'number' || valueType === 'boolean'){
|
||||
if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') {
|
||||
return {
|
||||
result: constant.create({
|
||||
value,
|
||||
@@ -65,10 +65,9 @@ const accessor = {
|
||||
};
|
||||
}
|
||||
},
|
||||
reduce(node, scope, context){
|
||||
reduce(node, scope, context) {
|
||||
let { result } = accessor.compile(node, scope, context);
|
||||
if (result.parseType === 'accessor'){
|
||||
context.error(`${toString(result)} not found, set to 0`);
|
||||
if (result.parseType === 'accessor') {
|
||||
return {
|
||||
result: constant.create({
|
||||
value: 0,
|
||||
@@ -76,10 +75,10 @@ const accessor = {
|
||||
context
|
||||
};
|
||||
} else {
|
||||
return {result, context};
|
||||
return { result, context };
|
||||
}
|
||||
},
|
||||
toString(node){
|
||||
toString(node) {
|
||||
return `${node.name}.${node.path.join('.')}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,58 +2,54 @@ import resolve, { toString } from '../resolve.js';
|
||||
import constant from './constant.js';
|
||||
|
||||
const symbol = {
|
||||
create({name}){
|
||||
create({ name }) {
|
||||
return {
|
||||
parseType: 'symbol',
|
||||
name,
|
||||
};
|
||||
},
|
||||
toString(node){
|
||||
toString(node) {
|
||||
return `${node.name}`
|
||||
},
|
||||
compile(node, scope, context, calledFromReduce = false){
|
||||
compile(node, scope, context, calledFromReduce = false) {
|
||||
let value = scope && scope[node.name];
|
||||
let type = typeof value;
|
||||
// For objects, default to their .value
|
||||
if (type === 'object'){
|
||||
if (type === 'object') {
|
||||
value = value.value;
|
||||
type = typeof value;
|
||||
}
|
||||
// For parse nodes, compile and return
|
||||
if (value?.parseType){
|
||||
if (calledFromReduce){
|
||||
if (value?.parseType) {
|
||||
if (calledFromReduce) {
|
||||
return resolve('reduce', value, scope, context);
|
||||
} else {
|
||||
return resolve('compile', value, scope, context);
|
||||
}
|
||||
}
|
||||
if (type === 'string' || type === 'number' || type === 'boolean'){
|
||||
if (type === 'string' || type === 'number' || type === 'boolean') {
|
||||
return {
|
||||
result: constant.create({value}),
|
||||
result: constant.create({ value }),
|
||||
context,
|
||||
};
|
||||
} else if (type === 'undefined'){
|
||||
} else if (type === 'undefined') {
|
||||
return {
|
||||
result: symbol.create({name: node.name}),
|
||||
result: symbol.create({ name: node.name }),
|
||||
context,
|
||||
};
|
||||
} else {
|
||||
throw new Meteor.Error(`Unexpected case: ${node.name} resolved to ${value}`);
|
||||
}
|
||||
},
|
||||
reduce(node, scope, context){
|
||||
let {result} = symbol.compile(node, scope, context, true);
|
||||
if (result.parseType === 'symbol'){
|
||||
context.error({
|
||||
type: 'info',
|
||||
message: `${toString(result)} not found, set to 0`
|
||||
});
|
||||
reduce(node, scope, context) {
|
||||
let { result } = symbol.compile(node, scope, context, true);
|
||||
if (result.parseType === 'symbol') {
|
||||
return {
|
||||
result: constant.create({value: 0}),
|
||||
result: constant.create({ value: 0 }),
|
||||
context,
|
||||
};
|
||||
} else {
|
||||
return {result, context};
|
||||
return { result, context };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user