Action system improvements

- Actions/spells now display their summary, not their description
- All save branches and attack branches run when there are no targets
- Improved action logging
- Index branch lets you customise a choice of children to run
This commit is contained in:
Stefan Zermatten
2022-03-09 01:31:09 +02:00
parent c68667be9c
commit 788cbb182d
15 changed files with 201 additions and 53 deletions

View File

@@ -11,11 +11,11 @@ export default function applyAction(node, {creature, targets, scope, log}){
const prop = node.node;
if (prop.target === 'self') targets = [creature];
// Log the name and description
// Log the name and summary
let content = { name: prop.name };
if (prop.description?.text){
recalculateInlineCalculations(prop.description, scope, log);
content.value = prop.description.value;
if (prop.summary?.text){
recalculateInlineCalculations(prop.summary, scope, log);
content.value = prop.summary.value;
}
if (content.name || content.value){
log.content.push(content);
@@ -33,7 +33,7 @@ export default function applyAction(node, {creature, targets, scope, log}){
targets.forEach(target => {
applyAttackToTarget({attack, target, scope, log});
// Apply the children, but only to the current target
applyChildren(node, {targets: [target], scope, log});
applyChildren(node, {creature, targets: [target], scope, log});
});
} else {
applyAttackWithoutTarget({attack, scope, log});
@@ -65,6 +65,13 @@ function applyAttackWithoutTarget({attack, scope, log}){
} else if(scope['$attackAdvantage'] === -1){
name += ' (Disadvantage)';
}
if (!criticalMiss){
scope['$attackHit'] = {value: true}
}
if (!criticalHit){
scope['$attackMiss'] = {value: true};
}
log.content.push({
name,
value: `${resultPrefix}\n**${result}**`,
@@ -106,10 +113,10 @@ function applyAttackToTarget({attack, target, scope, log}){
value: `${resultPrefix}\n**${result}**`,
inline: true,
});
if ((result > armor) || (criticalHit)){
scope['$attackHit'] = true;
if (criticalMiss || result < armor){
scope['$attackMiss'] = {value: true};
} else {
scope['$attackMiss'] = true;
scope['$attackHit'] = {value: true};
}
} else {
log.content.push({
@@ -161,15 +168,10 @@ function applyCrits(value, scope){
let criticalMiss;
if (criticalHit){
scope['$criticalHit'] = {value: true};
scope['$attackHit'] = {value: true};
} else {
criticalMiss = value === 1;
if (criticalMiss){
scope['$criticalMiss'] = 1;
scope['$attackMiss'] = {value: true};
} else {
// Untargeted attacks hit by default
scope['$attackHit'] = {value: true}
scope['$criticalMiss'] = {value: true};
}
}
return {criticalHit, criticalMiss};

View File

@@ -16,17 +16,47 @@ export default function applyBranch(node, {
recalculateCalculation(prop.condition, scope, log);
if (prop.condition?.value) applyChildren();
break;
case 'index':
if (node.children.length){
recalculateCalculation(prop.condition, scope, log);
if (!isFinite(prop.condition?.value)) {
log.content.push({
name: 'Branch Error',
value: 'Index did not resolve into a valid number'
});
break;
}
let index = Math.floor(prop.condition?.value);
if (index < 1) index = 1;
if (index > node.children.length) index = node.children.length;
applyProperty(node.children[index - 1], {
creature, targets, scope, log
});
}
break;
case 'hit':
if (scope['$attackHit']?.value) applyChildren();
if (scope['$attackHit']?.value){
if (!targets.length) log.content.push({value: '**On hit**'});
applyChildren();
}
break;
case 'miss':
if (scope['$attackMiss']?.value) applyChildren();
if (scope['$attackMiss']?.value){
if (!targets.length) log.content.push({value: '**On miss**'});
applyChildren();
}
break;
case 'failedSave':
if (scope['$saveFailed']?.value) applyChildren();
if (scope['$saveFailed']?.value){
if (!targets.length) log.content.push({value: '**On failed save**'});
applyChildren();
}
break;
case 'successfulSave':
if (scope['$saveSucceeded']?.value) applyChildren();
if (scope['$saveSucceeded']?.value){
if (!targets.length) log.content.push({value: '**On save**',});
applyChildren();
}
break;
case 'random':
if (node.children.length){

View File

@@ -10,6 +10,7 @@ import { get } from 'lodash';
import resolve, { map, toString } from '/imports/parser/resolve.js';
import symbol from '/imports/parser/parseTree/symbol.js';
import logErrors from './shared/logErrors.js';
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
export default function applyBuff(node, {creature, targets, scope, log}){
@@ -32,7 +33,30 @@ export default function applyBuff(node, {creature, targets, scope, log}){
collection: prop.parent.collection,
};
buffTargets.forEach(target => {
// Apply the buff
copyNodeListToTarget(propList, target, oldParent);
//Log the buff
if (prop.name || prop.description?.value){
if (target._id === creature._id){
// Targeting self
log.content.push({
name: prop.name,
value: prop.description?.value,
});
} else {
// Targeting other
insertCreatureLog.call({
log: {
creatureId: target._id,
content: [{
name: prop.name,
value: prop.description?.value,
}],
}
});
}
}
});
// Don't apply the children of the buff, they get copied to the target instead

View File

@@ -36,15 +36,12 @@ export default function applyDamage(node, {
const logValue = [];
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// Compile the dice roll and store that string first
// const {result: compiled} = resolve('compiled', prop.amount.parseNode, scope, context);
// logValue.push(toString(compiled));
// logErrors(context.errors, log);
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.amount, log);
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
logValue.push(toString(rolled));
if (rolled.parseType !== 'constant'){
logValue.push(toString(rolled));
}
logErrors(context.errors, log);
// Reset the errors so we don't log the same errors twice
@@ -62,11 +59,10 @@ export default function applyDamage(node, {
} else {
prop.amount.value = toString(reduced);
}
let damage = +reduced.value;
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' && !isFinite(reduced.value)){
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
return applyChildren();
}
@@ -129,10 +125,10 @@ export default function applyDamage(node, {
function applyDamageMultipliers({target, damage, damageProp, logValue}){
const damageType = damageProp?.damageType;
if (!damageType) return;
if (!damageType) return damage;
const multiplier = target?.variables?.[damageType];
if (!multiplier) return;
if (!multiplier) return damage;
const damageTypeText = damageType == 'healing' ? 'healing': `${damageType} damage`;
@@ -157,8 +153,8 @@ function applyDamageMultipliers({target, damage, damageProp, logValue}){
logValue.push(`Vulnerable to ${damageTypeText}`);
damage = Math.floor(damage * 2);
}
return damage;
}
return damage;
}
function multiplierAppliesTo(damageProp){

View File

@@ -1,20 +1,58 @@
import applyProperty from '../applyProperty.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import logErrors from './shared/logErrors.js';
import applyEffectsToCalculationParseNode from '/imports/api/engine/actions/applyPropertyByType/shared/applyEffectsToCalculationParseNode.js';
import resolve, { toString } from '/imports/parser/resolve.js';
export default function applyRoll(node, {creature, targets, scope, log}){
const prop = node.node;
if (prop.roll?.calculation){
recalculateCalculation(prop.roll, scope, log);
const applyChildren = node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
if (isFinite(prop.roll.value)){
scope[prop.variableName] = prop.roll.value;
if (prop.roll?.calculation){
const logValue = [];
// roll the dice only and store that string
applyEffectsToCalculationParseNode(prop.roll, log);
const {result: rolled, context} = resolve('roll', prop.roll.parseNode, scope);
if (rolled.parseType !== 'constant'){
logValue.push(toString(rolled));
}
logErrors(context.errors, log);
// Reset the errors so we don't log the same errors twice
context.errors = [];
// Resolve the roll to a final value
const {result: reduced} = resolve('reduce', rolled, scope, context);
logErrors(context.errors, log);
// Store the result
if (reduced.parseType === 'constant'){
prop.roll.value = reduced.value;
} else if (reduced.parseType === 'error'){
prop.roll.value = null;
} else {
prop.roll.value = toString(reduced);
}
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' || !isFinite(reduced.value)){
return applyChildren();
}
const value = reduced.value;
scope[prop.variableName] = value;
logValue.push(`**${value}**`);
if (!prop.silent){
log.content.push({
name: prop.name,
value: logValue.join('\n'),
inline: true,
});
}
log.content.push({
name: prop.name,
value: prop.variableName + ' = ' + prop.roll.calculation + ' = ' + prop.roll.value,
inline: true,
});
}
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log

View File

@@ -22,10 +22,21 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
}
log.content.push({
name: prop.name,
value: ' DC ' + dc,
value: `DC **${dc}**`,
inline: true,
});
// If there are no save targets, apply all children as if the save both
// succeeeded and failed
if (!saveTargets?.length){
scope['$saveFailed'] = {value: true};
scope['$saveSucceeded'] = {value: true};
return node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
}
// Each target makes the saving throw
saveTargets.forEach(target => {
delete scope['$saveFailed'];
delete scope['$saveSucceeded'];
@@ -55,24 +66,24 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
resultPrefix = `Advantage: 1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
resultPrefix = `Advantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
} else {
value = b;
resultPrefix = `Advantage: 1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
resultPrefix = `Advantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (save.advantage === -1){
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
resultPrefix = `Disadvantage: 1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
resultPrefix = `Disadvantage\n1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText}`;
} else {
value = b;
resultPrefix = `Disadvantage: 1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
resultPrefix = `Disadvantage\n1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else {
values = rollDice(1, 20);
value = values[0];
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
resultPrefix = `1d20 [ ${value} ] ${rollModifierText}`
}
scope['$saveDiceRoll'] = {value};
const result = value + save.value || 0;
@@ -84,8 +95,8 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
scope['$saveFailed'] = {value: true};
}
log.content.push({
name: 'Save',
value: resultPrefix + result + (saveSuccess ? 'Passed' : 'Failed'),
name: saveSuccess ? 'Successful save' : 'Failed save',
value: resultPrefix + '\n**' + result + '**',
inline: true,
});
return applyChildren();

View File

@@ -29,8 +29,9 @@ export default function computeVariableAsSkill(computation, node, prop){
}
// Combine everything to get the final result
const statBase = node.data.baseValue;
const statBase = node.data.baseValue || 0;
const aggregator = node.data.effectAggregator;
const aggregatorBase = aggregator.base || 0;
// If there is no aggregator, determine if the prop can hide, then exit
if (!aggregator){
@@ -41,7 +42,7 @@ export default function computeVariableAsSkill(computation, node, prop){
return;
}
// Combine aggregator
const base = (statBase > aggregator.base ? statBase : aggregator.base) || 0;
const base = statBase > aggregatorBase ? statBase : aggregatorBase;
let result = (base + prop.abilityMod + profBonus + aggregator.add) * aggregator.mul;
if (result < aggregator.min) result = aggregator.min;
if (result > aggregator.max) result = aggregator.max;