Merge branch 'develop' into feature-nested-sets

This commit is contained in:
Thaum Rystra
2023-09-28 10:25:27 +02:00
39 changed files with 1020 additions and 471 deletions

16
app/.github/pull_request_template.md vendored Normal file
View 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`

View File

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

View File

@@ -21,7 +21,7 @@ const updateCreatureProperty = new ValidatedMethod({
},
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
numRequests: 12,
timeInterval: 5000,
},
run({ _id, path, value }) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
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) {
@@ -85,6 +86,8 @@ function finalizeProp(prop){
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;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,6 +23,7 @@ const timingOptions = {
const actionPropertyTypeOptions = {
action: 'Action',
ammo: 'Ammo used',
adjustment: 'Attribute damage',
branch: 'Branch',
buff: 'Buff',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -88,11 +88,6 @@
</div>
</div>
</div>
<property-description
text
:model="model.description"
/>
</div>
</template>

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 &quot;~damage&quot; 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>

View File

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

View File

@@ -29,7 +29,6 @@
</v-col>
<v-col
cols="12"
md="6"
>
<smart-toggle
label="Target creature"

View File

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

View File

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

View File

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

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

View File

@@ -68,7 +68,6 @@ const accessor = {
reduce(node, scope, context) {
let { result } = accessor.compile(node, scope, context);
if (result.parseType === 'accessor') {
context.error(`${toString(result)} not found, set to 0`);
return {
result: constant.create({
value: 0,

View File

@@ -44,10 +44,6 @@ const symbol = {
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`
});
return {
result: constant.create({ value: 0 }),
context,