Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add chat damage breakdown, absorb hp/mp actions #212

Merged
merged 20 commits into from
Feb 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,7 @@
"Critical": "Critical",
"Failure": "Failure",
"Fumble": "Fumble",
"Breakdown": "Breakdown",
"ChatDifficulty": "Difficulty",
"DialogCheckTitle": "Configure Check",
"DialogCheckRoll": "Roll Check",
Expand Down Expand Up @@ -852,9 +853,9 @@
"ChatApplyMaxTargetWarning": "Too many actors targeted!",
"ChatApplyDamageTooltip": "Click to apply damage to selected tokens.<br>[Ctrl + Click] to Open Damage Customizer<br>[Shift + Click] to ignore Resistances.<br>[Ctrl + Shift] + Click to ignore Resistances and Immunities.",
"ChatApplyDamageNoActorsSelected": "No actors selected. Unable to apply damage.",
"ChatApplyDamageVulnerable": "<strong>{actor}</strong> was <strong>vulnerable</strong> to <strong>{type}</strong> and took <strong>{damage}</strong> damage from <strong>{from}</strong>",
"ChatApplyDamageNormal": "<strong>{actor}</strong> took <strong>{damage} {type}</strong> damage from <strong>{from}</strong>",
"ChatApplyDamageResistant": "<strong>{actor}</strong> was <strong>resistant</strong> to <strong>{type}</strong> and took <strong>{damage}</strong> damage from <strong>{from}</strong>",
"ChatApplyDamageVulnerable": "<strong>{actor}</strong> was <strong>vulnerable</strong> to <strong>{type}</strong> and lost <strong>{damage} hit points</strong> from <strong>{from}</strong>",
"ChatApplyDamageNormal": "<strong>{actor}</strong> was dealt <strong>{type}</strong> and lost <strong>{damage} hit points</strong> from <strong>{from}</strong>",
"ChatApplyDamageResistant": "<strong>{actor}</strong> was <strong>resistant</strong> to <strong>{type}</strong> and lost <strong>{damage} hit points</strong> from <strong>{from}</strong>",
"ChatApplyDamageResistantIgnored": "<strong>{actor}</strong> took <strong>{damage} {type}</strong> damage from <strong>{from} (Resistance ignored)</strong>",
"ChatApplyDamageImmune": "<strong>{actor}</strong> was <strong>immune</strong> to <strong>{type}</strong> and took <strong>{damage}</strong> damage from <strong>{from}</strong>",
"ChatApplyDamageImmuneIgnored": "<strong>{actor}</strong> took <strong>{damage} {type}</strong> damage from <strong>{from} (Immunity ignored)</strong>",
Expand All @@ -863,8 +864,12 @@
"ChatApplyResourceLossTooltip": "Click to spend the given resource on the actor who performed this action.",
"ChatApplyResourceLossSelected": "Spend Selected",
"ChatApplyResourceLossTargeted": "Spend Targeted",
"ChatToggleDamageBreakdown": "Toggle Damage Breakdown",
"ChatRevertDamage": "Revert applied damage",
"ChatRevertResourceGain": "Revert resource gain",
"ChatRevertResourceLoss": "Revert resource loss",
"ChatAbsorbHitPoints": "Recover half lost as HP",
"ChatAbsorbMindPoints": "Recover half lost as MP",
"ChatEvaluateAmountNoActor": "No reference to an actor provided",
"ChatEvaluateAmountNoItem": "No reference to an item provided",
"ChatEvaluateNoSkill": "The referenced skill was not found in the actor",
Expand Down Expand Up @@ -1256,6 +1261,19 @@
"ChangeWarning2": "Changing an Item's Fabula Ultima ID might break references to the Item! Proceed with caution and only if you are sure.",
"ChangeWarning3": "Do you want to proceed?",
"ChangeWarning1": "Proceed with caution!"
}
},
"DamagePipelineStepInitial": "Initial",
"DamagePipelineStepScaleIncomingDamage": "Scale Incoming Damage",
"DamagePipelineStepAffinity": "Affinity",
"DamagePipelineStepIncomingDamageAll": "Incoming Damage: All",
"DamagePipelineStepIncomingDamagePhysical": "Incoming Damage: Physical",
"DamagePipelineStepIncomingDamageAir": "Incoming Damage: Air",
"DamagePipelineStepIncomingDamageBolt": "Incoming Damage: Bolt",
"DamagePipelineStepIncomingDamageDark": "Incoming Damage: Dark",
"DamagePipelineStepIncomingDamageEarth": "Incoming Damage: Earth",
"DamagePipelineStepIncomingDamageFire": "Incoming Damage: Fire",
"DamagePipelineStepIncomingDamageIce": "Incoming Damage: Ice",
"DamagePipelineStepIncomingDamageLight": "Incoming Damage: Light",
"DamagePipelineStepIncomingDamagePoison": "Incoming Damage: Poison"
}
}
2 changes: 1 addition & 1 deletion module/checks/check-configuration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ class CheckConfigurer {
if (model.damageType) {
const damageTypeBonus = actor.system.bonuses.damage[model.damageType.value];
if (damageTypeBonus) {
this.addDamageBonus(`FU.DamageBonus${model.damageType.value}`, damageTypeBonus);
this.addDamageBonus(`FU.DamageBonus${model.damageType.value.capitalize()}`, damageTypeBonus);
}
}
// Attack Type
Expand Down
30 changes: 15 additions & 15 deletions module/documents/items/common/targeting-data-model.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Targeting } from '../../../helpers/targeting.mjs';
/**
* @property {TargetingRule} rule The type of targeting rule to use
* @property {Number} max The maximum number of targets
*/
export class TargetingDataModel extends foundry.abstract.DataModel {
static defineSchema() {
const { NumberField, StringField } = foundry.data.fields;
return {
rule: new StringField({ initial: Targeting.rule.self, choices: Object.keys(Targeting.rule), required: true }),
max: new NumberField({ initial: 0, min: 0, max: 3, integer: true, nullable: false }),
};
}
}
import { Targeting } from '../../../helpers/targeting.mjs';

/**
* @property {TargetingRule} rule The type of targeting rule to use
* @property {Number} max The maximum number of targets
*/
export class TargetingDataModel extends foundry.abstract.DataModel {
static defineSchema() {
const { NumberField, StringField } = foundry.data.fields;
return {
rule: new StringField({ initial: Targeting.rule.special, choices: Object.keys(Targeting.rule), required: true }),
max: new NumberField({ initial: 0, min: 0, max: 3, integer: true, nullable: false }),
};
}
}
30 changes: 25 additions & 5 deletions module/expressions/expressions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { FUItem } from '../documents/items/item.mjs';

/**
* @description Contains contextual objects used for evaluating expressions
* @property {FUActor} actor
* @property {FUActor} actor The source of the action
* @property {FUItem} item
* @property {FUActor[]} targets
* @remarks Do not serialize this class, as it references full objects. Instead store their uuids
* @remarks Do not serialize this class, as it references full objects. Instead, store their uuids
* and resolve them with the static constructor
*/
export class ExpressionContext {
Expand Down Expand Up @@ -68,6 +68,25 @@ export class ExpressionContext {
throw new Error(`No reference to an item provided while evaluating expression "${match}"`);
}
}

/**
* @description Resolves the actor or the target with the highest level
* @returns {FUActor}
*/
resolveActorOrHighestLevelTarget() {
if (this.actor) {
return this.actor;
} else {
if (this.targets.length > 0) {
return this.targets.reduce((prev, current) => {
return prev.system.level.value > current.system.level.value ? prev : current;
});
}
}

ui.notifications.warn('FU.ChatEvaluateAmountNoActor', { localize: true });
throw new Error(`No reference to an actor or targets provided while evaluating expression"`);
}
}

// DSL supported by the inline amount expression
Expand Down Expand Up @@ -219,7 +238,7 @@ function evaluateMacros(expression, context) {
return skill.system.level.value;
}
case 'step':
return stepByLevel(context.actor, splitArgs[0], splitArgs[1], splitArgs[2]);
return stepByLevel(context, splitArgs[0], splitArgs[1], splitArgs[2]);
default:
throw new Error(`Unsupported macro ${name}`);
}
Expand All @@ -229,12 +248,13 @@ function evaluateMacros(expression, context) {

/**
* @description Given 3 amounts, picks the one for this characters' level
* @param {FUActor} actor
* @param {ExpressionContext} context
* @param {Number} first
* @param {Number} second
* @param {Number} third
*/
function stepByLevel(actor, first, second, third) {
function stepByLevel(context, first, second, third) {
const actor = context.resolveActorOrHighestLevelTarget();
const tier = ImprovisedEffect.getCharacterTier(actor.system.level.value);
switch (tier) {
case 0:
Expand Down
41 changes: 41 additions & 0 deletions module/helpers/chat-message-helper.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { SYSTEM } from './config.mjs';

/**
* @description Registers a context menu item for the chat messages with the specified flag
* @param {String} flag The flag to check against, from those within {@link Flags}
* @param {String} name The localized name for the item name
* @param {String} iconClass The css class of the icon to use
* @param {Promise<ChatMessage, void>} callback The function to execute for the item
*/
function registerContextMenuItem(flag, name, iconClass, callback) {
const hook = (html, options) => {
options.unshift({
name: name,
icon: `<i class="${iconClass}"></i>`,
group: SYSTEM,
condition: (li) => {
const messageId = li.data('messageId');
/** @type ChatMessage | undefined */
const message = game.messages.get(messageId);
return message.getFlag(SYSTEM, flag);
},
callback: async (li) => {
const messageId = li.data('messageId');
/** @type ChatMessage | undefined */
const message = game.messages.get(messageId);
if (message) {
const damage = message.getFlag(SYSTEM, flag);
if (damage) {
callback(message);
}
}
},
});
};

Hooks.on('getChatLogEntryContext', hook);
}

export const ChatMessageHelper = Object.freeze({
registerContextMenuItem,
});
3 changes: 3 additions & 0 deletions module/helpers/flags.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export const Flags = Object.freeze({
SupportCheck: 'Supporter',
GroupCheckSupporters: 'GroupCheckSupporters',
Item: 'Item',
Damage: 'Damage',
Source: 'Source',
ResourceGain: 'ResourceGain',
ResourceLoss: 'ResourceLoss',
UseMetaCurrency: 'UseMetaCurrency',
Targets: 'Targets',
Expand Down
9 changes: 6 additions & 3 deletions module/helpers/inline-damage.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FU } from './config.mjs';
import { targetHandler } from './target-handler.mjs';
import { InlineHelper } from './inline-helper.mjs';
import { InlineHelper, InlineSourceInfo } from './inline-helper.mjs';
import { ExpressionContext, Expressions } from '../expressions/expressions.mjs';
import { DamagePipeline, DamageRequest } from '../pipelines/damage-pipeline.mjs';

Expand Down Expand Up @@ -79,7 +79,7 @@ function activateListeners(document, html) {
const sourceInfo = InlineHelper.determineSource(document, this);
const data = {
type: INLINE_DAMAGE,
sourceInfo: sourceInfo,
_sourceInfo: sourceInfo,
damageType: this.dataset.type,
amount: this.dataset.amount,
};
Expand All @@ -89,11 +89,14 @@ function activateListeners(document, html) {
}

// TODO: Implement
function onDropActor(actor, sheet, { type, damageType, amount, sourceInfo, ignore }) {
function onDropActor(actor, sheet, { type, damageType, amount, _sourceInfo, ignore }) {
if (type === INLINE_DAMAGE) {
// Need to rebuild the class after it was deserialized
const sourceInfo = InlineSourceInfo.fromObject(_sourceInfo);
const context = ExpressionContext.fromUuid(sourceInfo.actorUuid, sourceInfo.itemUuid, [actor]);
const _amount = Expressions.evaluate(amount, context);
const baseDamageInfo = { type: damageType, total: _amount, modifierTotal: 0 };

const request = new DamageRequest(sourceInfo, [actor], baseDamageInfo);
DamagePipeline.process(request);
return false;
Expand Down
21 changes: 21 additions & 0 deletions module/helpers/inline-helper.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,25 @@ export class InlineSourceInfo {
return new InlineSourceInfo(item.name, actor.uuid, item.uuid);
}

/**
* @param {String} actorUuid
* @param {String} itemUuid
* @return {InlineSourceInfo}
*/
static resolveName(actorUuid, itemUuid) {
const resolvedModel = fromUuidSync(itemUuid ?? actorUuid);
return new InlineSourceInfo(resolvedModel.name, actorUuid, itemUuid);
}

/**
* @description Used for reconstruction during deserialization
* @param {Object} obj An object containing the properties of this class
* @returns {InlineSourceInfo}
*/
static fromObject(obj) {
return new InlineSourceInfo(obj.name, obj.actorUuid, obj.itemUuid);
}

/**
* @returns {FUActor|null}
*/
Expand Down Expand Up @@ -58,6 +77,8 @@ export class InlineSourceInfo {
}
return null;
}

static none = Object.freeze(new InlineSourceInfo('Unknown'));
}

/**
Expand Down
Loading