323 lines
11 KiB
TypeScript
323 lines
11 KiB
TypeScript
import { some, includes, difference, intersection } from 'lodash';
|
|
|
|
import { getConstantValueFromScope } from '/imports/api/creature/creatures/CreatureVariables';
|
|
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
|
import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
|
|
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
|
|
import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation';
|
|
import { PropTask } from '/imports/api/engine/action/tasks/Task';
|
|
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
|
|
import { isFiniteNode } from '/imports/parser/parseTree/constant';
|
|
import resolve from '/imports/parser/resolve';
|
|
import toString from '/imports/parser/toString';
|
|
import { getPropertiesOfType } from '/imports/api/engine/loadCreatures';
|
|
import applyTask from '/imports/api/engine/action/tasks/applyTask';
|
|
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
|
|
import getEffectivePropTags from '/imports/api/engine/computation/utility/getEffectivePropTags';
|
|
import Context from '/imports/parser/types/Context';
|
|
import applySavingThrowProperty from '/imports/api/engine/action/applyProperties/applySavingThrowProperty';
|
|
|
|
export default async function applyDamageProperty(
|
|
task: PropTask, action: EngineAction, result: TaskResult, inputProvider: InputProvider
|
|
) {
|
|
const prop = task.prop;
|
|
const scope = getEffectiveActionScope(action);
|
|
|
|
// Skip if there is no parse node to work with
|
|
if (!prop.amount?.parseNode) return;
|
|
|
|
// Choose target
|
|
const damageTargets = prop.target === 'self' ? [action.creatureId] : task.targetIds;
|
|
// Determine if the hit is critical
|
|
const criticalHit = await getConstantValueFromScope('~criticalHit', scope)
|
|
&& prop.damageType !== 'healing'; // Can't critically heal
|
|
// Double the damage rolls if the hit is critical
|
|
const context = new Context({
|
|
options: { doubleRolls: criticalHit },
|
|
});
|
|
|
|
// Gather all the lines we need to log into an array
|
|
const logValue: string[] = [];
|
|
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
|
|
|
|
// roll the dice only and store that string
|
|
await recalculateCalculation(prop.amount, action, 'compile', inputProvider);
|
|
const { result: rolled } = await resolve('roll', prop.amount.valueNode, scope, context, inputProvider);
|
|
if (rolled.parseType !== 'constant') {
|
|
logValue.push(toString(rolled));
|
|
}
|
|
result.appendParserContextErrors(context, damageTargets);
|
|
|
|
// Reset the errors so we don't log the same errors twice
|
|
context.errors = [];
|
|
|
|
// Resolve the roll to a final value
|
|
const { result: reduced } = await resolve('reduce', rolled, scope, context, inputProvider);
|
|
result.appendParserContextErrors(context, damageTargets);
|
|
|
|
// Store the result
|
|
let damage: number | undefined = undefined;
|
|
if (reduced.parseType === 'constant') {
|
|
prop.amount.value = reduced.value;
|
|
if (typeof reduced.value === 'number') {
|
|
damage = reduced.value;
|
|
}
|
|
} else if (reduced.parseType === 'error') {
|
|
prop.amount.value = null;
|
|
} else {
|
|
prop.amount.value = toString(reduced);
|
|
}
|
|
|
|
// If we didn't end up with damage of finite amount, give up
|
|
if (
|
|
typeof damage !== 'number'
|
|
|| !isFinite(damage)
|
|
) {
|
|
return applyDefaultAfterPropTasks(action, prop, damageTargets, inputProvider);
|
|
}
|
|
|
|
// Round the damage to a whole number
|
|
damage = Math.floor(damage);
|
|
scope['~damage'] = { value: damage };
|
|
|
|
// Convert extra damage into the stored type
|
|
const lastDamageType = await getConstantValueFromScope('~lastDamageType', scope);
|
|
if (prop.damageType === 'extra' && typeof lastDamageType === 'string') {
|
|
prop.damageType = lastDamageType;
|
|
}
|
|
// Store current damage type
|
|
if (prop.damageType !== 'healing') {
|
|
scope['~lastDamageType'] = { value: prop.damageType };
|
|
}
|
|
|
|
// Memoise the damage suffix for the log
|
|
const suffix = (criticalHit ? ' critical ' : ' ') +
|
|
prop.damageType +
|
|
(prop.damageType !== 'healing' ? ' damage ' : '');
|
|
|
|
// If there is a save, calculate the save damage
|
|
let damageOnSave, saveProp, saveRoll;
|
|
if (prop.save) {
|
|
if (prop.save.damageFunction?.calculation) {
|
|
await recalculateCalculation(prop.save.damageFunction, action, 'compile', inputProvider);
|
|
context.errors = [];
|
|
const { result: saveDamageRolled } = await resolve(
|
|
'roll', prop.save.damageFunction.valueNode, scope, context, inputProvider
|
|
);
|
|
saveRoll = toString(saveDamageRolled);
|
|
const { result: saveDamageResult } = await resolve(
|
|
'reduce', saveDamageRolled, scope, context, inputProvider
|
|
);
|
|
result.appendParserContextErrors(context, damageTargets);
|
|
// If we didn't end up with a constant of finite amount, give up
|
|
if (
|
|
!isFiniteNode(saveDamageResult)
|
|
) {
|
|
return applyDefaultAfterPropTasks(action, prop, damageTargets, inputProvider);
|
|
}
|
|
// Round the damage to a whole number
|
|
damageOnSave = Math.floor(saveDamageResult.value);
|
|
} else {
|
|
damageOnSave = Math.floor(damage / 2);
|
|
}
|
|
saveProp = {
|
|
node: {
|
|
...prop.save,
|
|
name: prop.save.stat,
|
|
silent: prop.silent,
|
|
},
|
|
children: [],
|
|
}
|
|
}
|
|
|
|
if (damageTargets && damageTargets.length) {
|
|
// Iterate through all the targets
|
|
for (const target of damageTargets) {
|
|
let damageToApply = damage || 0;
|
|
|
|
// If there is a saving throw, apply that first
|
|
if (prop.save) {
|
|
await applySavingThrowProperty({
|
|
prop: saveProp,
|
|
targetIds: task.targetIds,
|
|
}, action, result, inputProvider);
|
|
if (await getConstantValueFromScope('~saveSucceeded', scope)) {
|
|
// 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
|
|
damageToApply = applyDamageMultipliers({
|
|
target,
|
|
damage: damageToApply,
|
|
damageProp: prop,
|
|
logValue
|
|
});
|
|
|
|
// Deal the damage to the target
|
|
await dealDamage(
|
|
action, prop, result, inputProvider, target, prop.damageType, damageToApply
|
|
);
|
|
}
|
|
} else {
|
|
// There are no targets, just log the result
|
|
logValue.push(`**${damage}** ${suffix}`);
|
|
if (prop.save) {
|
|
await applySavingThrowProperty(saveProp, action, result, inputProvider);
|
|
await applySavingThrowProperty({
|
|
prop: saveProp,
|
|
targetIds: task.targetIds,
|
|
}, action, result, inputProvider);
|
|
logValue.push(`**${damageOnSave}** ${suffix} on a successful save`);
|
|
}
|
|
}
|
|
if (logValue.length) result.appendLog({
|
|
name: logName,
|
|
value: logValue.join('\n'),
|
|
inline: true,
|
|
silenced: prop.silent,
|
|
}, damageTargets);
|
|
return applyDefaultAfterPropTasks(action, prop, damageTargets, inputProvider);
|
|
}
|
|
|
|
function damageFunctionText(save) {
|
|
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;
|
|
|
|
const multiplier = target?.variables?.[damageType];
|
|
if (!multiplier) return damage;
|
|
|
|
const damageTypeText = damageType == 'healing' ? 'healing' : `${damageType} damage`;
|
|
|
|
if (
|
|
multiplier.immunity &&
|
|
some(multiplier.immunities, multiplierAppliesTo(damageProp, 'immunity'))
|
|
) {
|
|
logValue.push(`Immune to ${damageTypeText}`);
|
|
return 0;
|
|
} else {
|
|
if (
|
|
multiplier.resistance &&
|
|
some(multiplier.resistances, multiplierAppliesTo(damageProp, 'resistance'))
|
|
) {
|
|
logValue.push(`Resistant to ${damageTypeText}`);
|
|
damage = Math.floor(damage / 2);
|
|
}
|
|
if (
|
|
multiplier.vulnerability &&
|
|
some(multiplier.vulnerabilities, multiplierAppliesTo(damageProp, 'vulnerability'))
|
|
) {
|
|
logValue.push(`Vulnerable to ${damageTypeText}`);
|
|
damage = Math.floor(damage * 2);
|
|
}
|
|
}
|
|
return damage;
|
|
}
|
|
|
|
function multiplierAppliesTo(damageProp, multiplierType) {
|
|
return multiplier => {
|
|
// Apply the default 'ignore x' tags
|
|
const effectiveTags = getEffectivePropTags(damageProp);
|
|
if (includes(effectiveTags, `ignore ${multiplierType}`)) return false;
|
|
|
|
const hasRequiredTags = difference(
|
|
multiplier.includeTags, effectiveTags
|
|
).length === 0;
|
|
|
|
const hasNoExcludedTags = intersection(
|
|
multiplier.excludeTags, effectiveTags
|
|
).length === 0;
|
|
|
|
return hasRequiredTags && hasNoExcludedTags;
|
|
}
|
|
}
|
|
|
|
async function dealDamage(
|
|
action: EngineAction, prop: any, result: TaskResult, userInput: InputProvider,
|
|
targetId: string, damageType: string, amount: number
|
|
) {
|
|
// Get all the health bars and do damage to them
|
|
let healthBars = getPropertiesOfType(targetId, 'attribute');
|
|
|
|
// Keep only the healthbars that can take damage/healing
|
|
healthBars = healthBars.filter((bar) => {
|
|
if (bar.attributeType !== 'healthBar' || bar.inactive || bar.removed || bar.overridden) {
|
|
return false;
|
|
}
|
|
if (damageType === 'healing' && bar.healthBarNoHealing) {
|
|
return false;
|
|
}
|
|
if (damageType !== 'healing' && amount >= 0 && bar.healthBarNoDamage) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
// Sort healthbars by damage/healing order or tree order as a fallback
|
|
healthBars.sort((a, b) => {
|
|
let diff;
|
|
if (amount >= 0) {
|
|
diff = a.healthBarDamageOrder - b.healthBarDamageOrder;
|
|
} else {
|
|
diff = a.healthBarHealingOrder - b.healthBarHealingOrder;
|
|
}
|
|
if (Number.isFinite(diff)) {
|
|
return diff;
|
|
} else {
|
|
return a.order - b.order;
|
|
}
|
|
});
|
|
|
|
// Deal the damage to each healthbar in order until all damage is done
|
|
const totalDamage = amount;
|
|
let damageLeft = totalDamage;
|
|
if (damageType === 'healing') damageLeft = -totalDamage;
|
|
for (const healthBar of healthBars) {
|
|
if (damageLeft === 0) return;
|
|
// Do the damage
|
|
const damageAdded = await applyTask(action, {
|
|
targetIds: [targetId],
|
|
subtaskFn: 'damageProp',
|
|
params: {
|
|
operation: 'increment',
|
|
value: +damageLeft || 0,
|
|
targetProp: healthBar,
|
|
},
|
|
}, userInput);
|
|
|
|
damageLeft -= damageAdded;
|
|
// Prevent overflow
|
|
if (
|
|
damageType === 'healing' ?
|
|
healthBar.healthBarNoHealingOverflow :
|
|
healthBar.healthBarNoDamageOverflow
|
|
) {
|
|
damageLeft = 0;
|
|
}
|
|
}
|
|
return totalDamage;
|
|
}
|