Skip to content

Damage System ​

Damage type files live under damage_types/*.yml; each one lays out how a hit is resolved step by step, which parameters each stage takes, and how vanilla damage is taken over. Damage is not resolved by one fixed formula; it runs through the stages multi-stage pipeline (a chain of steps that stack onto each other). Each damage type can define its own attack, defense, critical, penetration, recovery, and custom expression stages

Damage type fields ​

FieldDescription
idDamage type id, such as physical, projectile, or spell
display_nameDisplay name. Defaults to id
aliasesOptional aliases
hard_lockDamage-type level flag meaning "this type only accepts the events listed in allowed_events". It is parsed and carried with the damage type definition. It is a different key from the top-level hard_lock_damage in config.yml, which has no runtime consumer
allowed_eventsVanilla event/cause allowlist. Empty means unrestricted
message / attacker_message / target_messageCombat messages
stagesOrdered calculation stages
recoveryPost-damage recovery or lifesteal calculation

Default damage types include physical, projectile, and spell

Stage pipeline ​

Stages run one after another in list order. Each stage reads attributes from the attacker, target, or context, then adds or subtracts its result onto the damage value

FieldDescription
idDebug id for the stage
kindFLAT_PERCENT or CUSTOM
sourceATTACKER, TARGET, or CONTEXT
modeADD or SUBTRACT
roleStage semantic role: NORMAL / CRITICAL / DEFENSE / BLOCK. Omitted values are inferred from the stage id: crit and critical → CRITICAL; defense and target_defense → DEFENSE; block and shield_block → BLOCK; anything else → NORMAL. Declare it explicitly when a custom id needs critical, defense, or block semantics
flat_attributesFlat contribution attributes
percent_attributesPercentage contribution attributes
chance_attributesChance attributes
multiplier_attributesMultiplier attributes
expressionCustom expression when kind=CUSTOM
variablesStage-scoped custom variables, usable by expression when kind=CUSTOM
min_result / max_resultResult clamps
min_chance / max_chanceChance clamps
min_multiplier / max_multiplierMultiplier clamps

CUSTOM expression variables ​

With kind: CUSTOM selected, you can use these variables in expression:

VariableDescription
%input% / %base%Damage value entering this stage
%flat%Aggregated flat_attributes value
%percent%Aggregated percent_attributes value
%chance%Aggregated chance_attributes chance
%multiplier%Aggregated multiplier_attributes multiplier
%crit%1 when this stage rolled a critical hit, otherwise 0
%roll%The random roll value used for this resolution
%target_blocking%1 when this hit counts as a valid shield block (including the facing check), otherwise 0. Always available in every stage expression; requests without a target entity fall back to 0

If the expression is empty or cannot be evaluated, the stage falls back to a default result: %input% unchanged when there are no chance attributes, otherwise the critical multiplier is applied

Stage custom variables ​

Besides the built-in variables, each stage can declare its own variables for use in its expression. A variable value can be a number, an expression, or a random numeric config:

yaml
  - id: overflow_soften
    kind: CUSTOM
    source: ATTACKER
    variables:
      soft_cap: 120
      overflow: "max(0, %input% - %soft_cap%)"
      jitter:
        min: 0.95
        max: 1.05
    expression: "(%input% - %overflow% * 0.5) * %jitter%"

Rules:

  • A variable may reference built-in variables (such as %input% and %multiplier%), damage context variables, and any variable declared before it
  • Variables resolve in declaration order, so overflow can reference the earlier soft_cap. Self-references are rejected
  • A variable overrides a built-in or context variable of the same name
  • Variables only apply to the stage that declares them; they are not passed to later stages
  • A random numeric config is rolled once per expression evaluation (all references in that stage see the same value), and re-rolled for every stage and every hit
  • If a variable fails to resolve (a non-numeric text value, or a reference to an undeclared variable), the whole expression fails and the stage result becomes 0. This is existing expression-engine behavior; verify with /ea trace after editing variables

Built-in physical pipeline ​

The actual bundled damage_types/physical.yml:

yaml
id: physical
display_name: "物理伤害"
aliases: [physical_damage, phys]
allowed_events:
  - ENTITY_ATTACK
  - ENTITY_SWEEP_ATTACK
hard_lock: true
stages:
  - id: attack
    kind: FLAT_PERCENT
    source: ATTACKER
    mode: ADD
    flat_attributes: [physical_attack]
    percent_attributes: [physical_damage_bonus]

  - id: crit
    kind: CUSTOM
    source: ATTACKER
    chance_attributes: [physical_crit_rate]
    multiplier_attributes: [physical_crit_damage]
    expression: "%input% * (1 + (%crit% * (%multiplier% / 100)))"

  - id: defense
    kind: CUSTOM
    source: TARGET
    flat_attributes: [physical_defense]
    expression: "max(0, %input% - %flat%)"

  - id: block
    kind: CUSTOM
    source: TARGET
    chance_attributes: [block_rate]
    multiplier_attributes: [block_reduction]
    expression: "%input% * (1 - (%target_blocking% * %crit% * (%multiplier% / 100)))"
    min_result: 0

recovery:
  source: ATTACKER
  resistance_source: TARGET
  flat_attributes: [percentage_lifesteal]
  percent_attributes: [lifesteal]
  resistance_attributes: [lifesteal_resistance]

attacker_message: "<gray>[<gold>%damage_type%</gold>]</gray> <green>你对 <white>%target%</white> 造成了 <yellow>%final_damage%</yellow> 点伤害</green>%critical_suffix%"

target_message: "<gray>[<gold>%damage_type%</gold>]</gray> <red>%attacker%</red> 对你造成了 <yellow>%final_damage%</yellow> 点伤害%critical_suffix%"

Stages run in list order: each stage's output becomes the next stage's input. To reduce chat spam, set attacker_message / target_message to an empty string and no message is sent

Recovery configuration ​

FieldTypeDefaultDescription
sourceenumATTACKERWhere recovery attributes are read from
resistance_sourceenumTARGETWhere resistance attributes are read from
flat_attributeslist<string>[]Flat recovery attributes
percent_attributeslist<string>[]Percentage recovery attributes, based on final damage
resistance_attributeslist<string>[]Resistance attributes that reduce recovery
expressionstring""Custom recovery expression
min_result / max_resultdouble—Recovery result clamps

Vanilla damage takeover modes ​

Global vanilla_event_damage.enabled controls the current vanilla event takeover mode:

  • enabled: true: recommended perfect takeover mode. Attribute does not cancel the vanilla damage event first. Instead, it resolves EA damage synchronously before the event returns and rewrites the final event damage in place. This preserves vanilla side effects such as burning zombies igniting players, iron golem knockback, shields, absorption hearts, thorns, death/stat handling, and other vanilla/plugin event effects
  • enabled: false: compatibility mode. The old behavior cancels the event and re-applies damage/feedback through Attribute. Use it only when a server needs the legacy behavior or is integrating with special combat plugins

Perfect takeover changes how damage is applied, not the configured damage-number pipeline. If the result is dodge, event cancellation, or final damage <= 0, the original event is cancelled for that hit. Attribute side effects such as lifesteal, combat messages, and attack cooldown handling are still applied by Attribute

Shield blocking ​

Vanilla shields go through EntityDamageEvent.DamageModifier.BLOCKING, which is independent of the EA damage pipeline. The global shield.mode decides who resolves the block

yaml
shield:
  mode: vanilla
  require_facing: true
  facing_angle_degrees: 180
FieldDefaultDescription
modevanillavanilla keeps the vanilla BLOCKING; attribute zeroes it and resolves the block in a damage stage instead
require_facingtrueOnly applies in attribute mode: whether the damage source must be in front of the target
facing_angle_degrees180Facing check cone in degrees. 180 matches the vanilla half-plane check

Difference between the two modes ​

mode: vanilla (default, preserves existing behavior): while blocking, the vanilla BLOCKING modifier removes the entire EA-calculated damage, so EA attack, critical, and penetration values have no effect while a shield is raised. In this mode the context variable target_blocking is always 0, and a block stage in the damage type leaves damage unchanged

mode: attribute: EA zeroes the vanilla BLOCKING and the block stage resolves the block from the target's block_rate and block_reduction, which allows percentage-based mitigation. The default baseline is block_rate: 100 / block_reduction: 100, equivalent to the vanilla full block, so switching modes alone does not change the feel. Lower block_reduction to get a percentage block

Both are ordinary EA attributes, so a shield can supply values through lore or PDC, and Gem, Strengthen, Forge, or set bonuses can contribute as usual

Attribute IDMeaning
block_rateChance for the block to trigger while a shield is raised. 0 means the block never triggers
block_reductionPercentage of damage removed once the block triggers. 100 equals the vanilla full block

The baselines live under default_profile.attributes in config.yml; item values stack on top, and the total is clamped by the attribute's own max_value

The block stage ​

The bundled physical.yml and projectile.yml both include this stage (vanilla shields block projectiles too); spell.yml does not, because vanilla shields do not block spells

yaml
  - id: block
    kind: CUSTOM
    source: TARGET
    chance_attributes: [block_rate]
    multiplier_attributes: [block_reduction]
    expression: "%input% * (1 - (%target_blocking% * %crit% * (%multiplier% / 100)))"
    min_result: 0

%target_blocking% is 1 or 0 depending on whether this hit counts as a valid raised shield (including the facing check). %crit% here means whether the block_rate chance was rolled successfully

Notes ​

  • When customizing the block stage, chance_attributes must keep block_rate with a non-zero baseline, otherwise %crit% is always 0 and blocking removes no damage at all
  • %target_blocking% is always available in every damage stage expression; damage requests without a target entity fall back to 0
  • Environmental damage has no source direction, so it never counts as a frontal block when require_facing: true
  • Blocking and critical share the same random roll, which is existing stage-pipeline behavior

Synthetic hit feedback ​

Synthetic feedback mainly matters in compatibility mode. In perfect takeover mode, vanilla itself produces knockback, hurt animation, and sound (the exact behavior depends on the damage source and entity). When you need it, synthetic_hit_feedback re-applies knockback and hurt sound so combat feedback stays close to the vanilla experience

yaml
synthetic_hit_feedback:
  knockback: true
  knockback_strength: 0.4
  hurt_sound: true
FieldDescription
knockbackWhether to add knockback feedback
knockback_strengthKnockback strength
hurt_soundWhether to play hurt sound feedback

Debugging ​

Use /ea debug and the module's damage debug output to inspect context variables, stage results, final damage, recovery, and event behavior