English
Configuration Directory
EmakiAttribute keeps its configuration files in a single directory. Below, each config.yml field is explained along with the order to troubleshoot after a change. Configuration consists of the main config, attribute definitions, damage types, conditions, lore formats, and attribute balance data. Start by defining a stable attribute naming convention, then expand damage types and equipment sources
Default directory structure
text
plugins/EmakiAttribute/
├── config.yml
├── attribute_balance.yml
├── attributes/
├── damage_types/
├── conditions/
├── lore_formats/
├── presets/
├── gui/
├── lang/
└── data/| Path | Purpose |
|---|---|
config.yml | Main config: default resources, damage interception, regeneration, scaling curves, and vanilla damage mappings |
attribute_balance.yml | Attribute score and balance semantics parameters |
attributes/ | Individual attribute definitions |
damage_types/ | Damage pipelines, such as physical, projectile, and spell |
conditions/ | Attribute read conditions (PDC read rules) |
lore_formats/ | Attribute display format templates |
presets/ | Attribute preset definitions |
gui/ | Attribute points GUI configuration (attribute_points.yml) |
lang/ | Language files; bundled zh_CN.yml and en_US.yml |
data/ | Runtime data, for example data/parent_attributes/<UUID>.yml; usually not edited manually |
Suggested edit order
- Define attribute ids, display names, value kinds, and caps in
attributes/ - Define how legacy lore is read or displayed in
lore_formats/ - Configure damage stages in
damage_types/ - Write attributes from equipment, gems, strengthening, forging, or item modules
- Run
/ea lint, then/ea reloadand/ea resync <player>after every change
Main config fields
The table below covers every top-level key in config.yml. The nested sections (default_profile, shield, synthetic_hit_feedback, damage_indicator, scaling_curves, allowed_damage_causes) are listed under their own sections
Bundled defaults (top-level keys only; the full contents of default_profile, shield, synthetic_hit_feedback, damage_indicator, and allowed_damage_causes are in their own sections):
yaml
version: "4.8.7"
language: "zh_CN"
release_default_data: true
attribute_sources:
read_lore_attributes: true
read_pdc_attributes: true
require_lore_pdc_match: false
hard_lock_damage: true
default_damage_type: physical
projectile_damage_type: projectile
vanilla_event_damage:
enabled: true
damage_type: physical
same_signature_ignores_invulnerability:
enabled: true
window_ms: 500
attack_speed:
scope: global
regen_interval_ticks: 20
sync_delay_ticks: 1
health_display_scaling:
enabled: false
target: 20.0
default_profile: # see "Default resources and attributes"
shield: # see "Shield blocking"
synthetic_hit_feedback: # see "Synthetic hit feedback"
damage_indicator: # see "Damage indicators"
scaling_curves: {} # empty map by default, so no decay applies
allowed_damage_causes: # see "Vanilla damage mapping"; 33 cause mappings ship by default| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
version | string | No | "4.8.7" | — | Config version maintained by default resource sync; do not edit by hand |
language | string | No | "zh_CN" | — | Language file name under lang/<language>.yml |
release_default_data | boolean | No | true | true / false | When disabled, missing bundled data files (attributes, damage types, conditions, lore formats, presets, GUI) are no longer released |
attribute_sources.read_lore_attributes | boolean | No | true | true / false | Whether item attributes are parsed from Lore |
attribute_sources.read_pdc_attributes | boolean | No | true | true / false | Whether item attributes are read from PDC |
attribute_sources.require_lore_pdc_match | boolean | No | false | true / false | When enabled, an attribute must exist on both sides with equal values and is counted once; otherwise it is rejected |
hard_lock_damage | boolean | No | true | true / false | Legacy compatibility key: it is read by the config parser but has no runtime consumer, so changing it changes nothing. Unmapped vanilla damage is governed by vanilla_event_damage |
default_damage_type | string | No | physical | — | Fallback damage type when none is specified; should point to a loaded damage type id, otherwise /ea lint reports it |
projectile_damage_type | string | No | projectile | — | Damage type id used when a projectile hit resolves against the attacker snapshot cached at launch; /ea lint reports it when unloaded |
vanilla_event_damage.enabled | boolean | No | true | true / false | When enabled, the vanilla event is preserved and its final damage is rewritten in place (perfect takeover); when disabled, the event is cancelled and EA re-applies damage (compatibility mode) |
vanilla_event_damage.damage_type | string | No | same as default_damage_type (physical) | — | Damage type id used for unmapped vanilla damage; only used while this switch is enabled, and /ea lint reports it when unloaded |
same_signature_ignores_invulnerability.enabled | boolean | No | true | true / false | Whether multi-hit attacks from the same attacker under the same snapshot signature and damage type bypass the vanilla invulnerability window |
same_signature_ignores_invulnerability.window_ms | long | No | 500 | ≥ 0 | Batch window in milliseconds |
attack_speed.scope | string | No | global | global / attribute_only | Attack-speed cooldown scope; any other value is treated as global. global manages every attack and derives cooldowns from Bukkit ATTACK_SPEED; attribute_only intercepts only items that declare an EA attack speed value in Lore/PDC |
regen_interval_ticks | integer | No | 20 | ≥ 1 | Resource regeneration interval in ticks; 20 ticks = 1 second |
sync_delay_ticks | integer | No | 1 | ≥ 0 | Delay in ticks before attributes sync to Bukkit |
health_display_scaling.enabled | boolean | No | false | true / false | Whether the client health bar is compressed; real health and damage are unaffected |
health_display_scaling.target | double | No | 20.0 | finite and > 0 | Target scale of the client health bar; 20 is one row of hearts. Invalid or ≤ 0 falls back to 20.0 |
default_profile | object | No | see "Default resources and attributes" | — | Default attribute and resource baselines |
shield | object | No | see "Shield blocking" | — | Shield block ownership |
synthetic_hit_feedback | object | No | see "Synthetic hit feedback" | — | Knockback and hurt sound feedback after vanilla damage interception |
damage_indicator | object | No | see "Damage indicators" | — | Floating damage indicators |
scaling_curves | map | No | empty (no decay) | — | Diminishing-return rules after thresholds; see Scaling Curves |
allowed_damage_causes | list | No | see "Vanilla damage mapping" | — | Allowlist mapping Bukkit DamageCause values to attribute damage types |
Example: read item attributes from PDC only, and keep perfect takeover
yaml
attribute_sources:
read_lore_attributes: false
read_pdc_attributes: true
vanilla_event_damage:
enabled: true
damage_type: physicalItem attribute source policy
attribute_sources controls whether item attributes are read from Lore, PDC, or both, and whether both sources must validate each other:
yaml
attribute_sources:
read_lore_attributes: true
read_pdc_attributes: true
require_lore_pdc_match: falseThe default keeps the legacy behavior: every enabled Lore and PDC source contributes to the item snapshot. Once you enable require_lore_pdc_match, values are compared per attribute ID:
- Present in both sources with equal values: count the attribute once
- Present in both sources with different values: reject the attribute
- Present in only one source: reject the attribute
- Fixed and spread/range values use the same matching rule
Note that match mode requires both read switches. Disabling either one makes the precheck emit a warning, and item attributes then cannot form a matched pair. If both sources are disabled, default, temporary, and other entity-level sources remain available, but equipped items contribute no Lore/PDC attributes
Default resources and attributes
default_profile defines the resources and attribute baselines a player has on initialization:
yaml
default_profile:
resources:
health:
display_name: "生命值"
default_max: 20.0
min_max: 1.0
max_max: 2048.0
sync_to_bukkit: true
full_on_init: true
mana:
display_name: "法力值"
default_max: 100.0
min_max: 0.0
max_max: 99999.0
sync_to_bukkit: false
full_on_init: true
attributes:
physical_attack: 0.0
physical_defense: 0.0
health_regen: 0.1
mana_regen: 0.1default_profile.resources.<id>
Each section key is the resource id. Fields:
| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
<resource id> (section key) | string | No | section key | — | Resource id, also used as the definition's id (such as health or mana) |
display_name | string | No | same as resource id | — | Display name |
default_max | double | No | 0 | finite number (invalid falls back to 0) | Default maximum |
min_max | double | No | 0 | finite number | Lower bound of the maximum; clamped first, so the maximum never drops below it |
max_max | double | No | Double.MAX_VALUE | finite number | Upper bound of the maximum |
sync_to_bukkit | boolean | No | false | true / false | Whether the resource syncs to Bukkit (health needs true). A false resource such as mana keeps PDC as the authority |
full_on_init | boolean | No | true | true / false | Whether the resource is filled on initialization |
regen_per_second | double | No | 0 | ≥ 0 | Natural regeneration per second; applied against regen_interval_ticks |
is_health | boolean | No | false | true / false | Whether this resource is marked as the health resource |
default_profile also recognizes the optional section-level keys id (default default), priority (default 1000000), and description (default empty); the bundled config.yml does not write them
default_profile.attributes.<attribute_id>
| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
<attribute id> | double | No | 0 | finite number (invalid falls back to 0) | Baseline injected for that attribute on initialization; attributes not listed receive no default |
Bundled default resources:
health: default max 20, synced to Bukkit, min max 1, max max 2048mana: default max 100, not synced to Bukkit, min max 0, max max 99999
Quick split: resources are the changeable pools such as health and mana, while attributes serve equipment, gems, strengthening, skills, and damage calculation. A resource with sync_to_bukkit: true mirrors to its Bukkit counterpart, which is how health stays in sync
yaml
# Add a rage resource that regenerates 5 points per second
default_profile:
resources:
rage:
display_name: "Rage"
default_max: 100.0
min_max: 0.0
max_max: 100.0
sync_to_bukkit: false
full_on_init: false
regen_per_second: 5.0Shield blocking
shield decides whether the block is resolved by vanilla or by EA. See "Shield blocking" in Damage System
yaml
shield:
mode: vanilla
require_facing: true
facing_angle_degrees: 180| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
mode | string | No | vanilla | vanilla / attribute | vanilla keeps the vanilla BLOCKING; attribute zeroes it and resolves the block in the damage type's block stage. Any other value is treated as vanilla |
require_facing | boolean | No | true | true / false | Only applies in attribute mode: whether the source must be in front of the target. Environmental damage has no source direction and never counts as a frontal block |
facing_angle_degrees | double | No | 180 | 0 – 180 (clamped into range) | Facing check angle in degrees; 180 is the half-plane centered on the view direction |
Example: switch to percentage blocking
yaml
shield:
mode: attribute
require_facing: true
facing_angle_degrees: 180
# then lower default_profile.attributes.block_reduction (for example to 40) to get 40% mitigationSynthetic hit feedback
synthetic_hit_feedback only re-applies knockback and hurt sound in compatibility mode (vanilla_event_damage.enabled: false)
yaml
synthetic_hit_feedback:
knockback: true
knockback_strength: 0.4
hurt_sound: true| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
knockback | boolean | No | true | true / false | Whether to add knockback feedback |
knockback_strength | double | No | 0.4 | ≥ 0 (negative is treated as 0) | Knockback strength; higher values move the target further |
hurt_sound | boolean | No | true | true / false | Whether to play hurt sound feedback |
Example: disable synthetic knockback when a custom knockback plugin is present
yaml
synthetic_hit_feedback:
knockback: false
hurt_sound: trueDamage indicators
damage_indicator controls the floating damage numbers. Which render backend they use comes from EmakiCoreLib's display.backend: with the packet backend only the players selected by visible_to can see them; with the bukkit backend the indicator is a real entity visible to every nearby player and visible_to has no effect
yaml
damage_indicator:
enabled: true
merge_window_ms: 500
max_per_target_per_second: 4
visible_to: "involved"
ignored_causes:
- FIRE_TICK
- POISON
- WITHER
- DROWNING
- FREEZE
- SUFFOCATION
- STARVATION
lifetime_ticks: 24
spawn:
offset:
x: 0.0
y: 2.0
z: 0.0
random_offset:
x: 0.4
y: 0.15
z: 0.4
motion:
enabled: true
step_ticks: 2
speed: 0.30
gravity: -0.030
direction:
mode: "random"
fixed:
x: 0.0
y: 1.0
z: 0.0
pitch:
min: 55.0
max: 85.0
yaw:
min: -180.0
max: 180.0
scale:
pop_from: 0.6
pop_ticks: 3
shrink_to: 0.0
shrink_ticks: 6
triggers:
normal:
enabled: true
critical:
enabled: true
heal:
enabled: true
dodge:
enabled: true| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
enabled | boolean | No | true | true / false | Whether damage indicators are enabled |
merge_window_ms | integer | No | 500 | ≥ 0 | Multiple hits on the same target inside this window (milliseconds) merge into one indicator; 0 disables merging, and a new hit inside the window restarts the indicator from its spawn point |
max_per_target_per_second | integer | No | 4 | ≥ 0 | Maximum indicators per target per second; 0 means unlimited |
visible_to | string | No | involved | involved / all | Only applies to the packet backend: involved shows the indicator to the attacker and victim only, all to every nearby player; any other value is treated as all |
ignored_causes | list<string> | No | 7 entries (FIRE_TICK, POISON, WITHER, DROWNING, FREEZE, SUFFOCATION, STARVATION) | any Bukkit DamageCause name | Damage causes that spawn no indicator; an empty list falls back to the default |
lifetime_ticks | integer | No | 24 | ≥ 1 | Lifetime and total arc duration in ticks; expired indicators are reclaimed by CoreLib |
spawn.offset.x / .y / .z | double | No | 0.0 / 2.0 / 0.0 | — | Fixed offset in blocks relative to the target's feet |
spawn.random_offset.x / .y / .z | double | No | 0.4 / 0.15 / 0.4 | — | Random scatter per axis in blocks, applied as ±value; 0 disables scatter on that axis |
motion.enabled | boolean | No | true | true / false | Whether the arc motion is enabled |
motion.step_ticks | integer | No | 2 | ≥ 1 | Keyframe interval in ticks, which is also the client interpolation duration; 1 is smoothest, larger values send fewer packets and look more segmented |
motion.speed | double | No | 0.30 | ≥ 0 | Initial launch speed in blocks per tick |
motion.gravity | double | No | -0.030 | — | Vertical acceleration in blocks per tick²; negative falls, controlling how much the arc curves |
motion.direction.mode | string | No | random | random / fixed / away_from_attacker | Launch direction mode; any other value is treated as random |
motion.direction.fixed.x / .y / .z | double | No | 0.0 / 1.0 / 0.0 | — | Direction vector used when mode: fixed |
motion.direction.pitch.min / .max | double | No | 55.0 / 85.0 | — (min > max is swapped automatically) | Elevation range in degrees, where 90 is straight up |
motion.direction.yaw.min / .max | double | No | -180.0 / 180.0 | — (min > max is swapped automatically) | Horizontal angle range in degrees |
motion.scale.pop_from | double | No | 0.6 | ≥ 0 | Entry scale; grows linearly to 1.0 over the first pop_ticks |
motion.scale.pop_ticks | integer | No | 3 | ≥ 0 | Entry growth duration in ticks |
motion.scale.shrink_to | double | No | 0.0 | ≥ 0 | Exit scale target; shrinks linearly from 1.0 over the last shrink_ticks; 0 shrinks to invisible |
motion.scale.shrink_ticks | integer | No | 6 | ≥ 0 | Exit shrink duration in ticks |
triggers.<normal|critical|heal|dodge> | object or boolean | No | each enabled: true | — | The four triggers. Only the keys that change need to be written (enabled, lifetime_ticks, spawn.*, motion.*); unwritten keys inherit the defaults above. A boolean shorthand sets only enabled. dodge additionally requires dodge resolution to be enabled; vanilla combat does not perform dodge checks by default |
Example: a faster critical indicator plus spam suppression
yaml
damage_indicator:
enabled: true
max_per_target_per_second: 2
triggers:
critical:
enabled: true
motion:
speed: 0.42
gravity: -0.036
scale:
pop_from: 0.35
pop_ticks: 4Vanilla damage mapping
allowed_damage_causes decides which Bukkit DamageCause values enter attribute resolution:
yaml
allowed_damage_causes:
- cause: FALL
damage_type: physical
enabled: true
- cause: LAVA
damage_type: physical
enabled: true| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
cause | string | Yes | — | Bukkit EntityDamageEvent.DamageCause name | Vanilla cause to map; an entry without it is ignored |
damage_type | string | No | default_damage_type | — | Damage type id from damage_types/*.yml; /ea lint reports it when unloaded |
damage | double | No | the vanilla event damage when omitted | ≥ 0 (negative is treated as 0) | Base damage before the attribute pipeline. When omitted, the vanilla value already computed by the server is used, which suits fall, explosion, burning, drowning, and other height- or state-dependent sources |
enabled | boolean | No | true | true / false | Whether this mapping is active; false means it never matches |
context.<key> (or any other key) | any | No | — | — | Injected into this hit's damage context for stage expressions and message templates. cause, damage_type, damage, enabled, and context are reserved; every other key becomes a context variable |
Example: route fall damage through the physical pipeline using the vanilla value, and pin lava damage to a fixed base
yaml
allowed_damage_causes:
- cause: FALL
damage_type: physical
enabled: true # no damage, so the vanilla fall damage is used
- cause: LAVA
damage_type: spell
damage: 8 # override with a fixed base of 8
enabled: trueUnmapped vanilla damage is governed by vanilla_event_damage, not by hard_lock_damage: when enabled: true it enters the attribute pipeline as vanilla_event_damage.damage_type; when enabled: false only the causes allowed in this table enter EA and everything else keeps vanilla behavior
Scaling curves
scaling_curves configures diminishing returns above a threshold and is an empty map by default (no decay):
yaml
scaling_curves:
physical_attack_cap:
attribute: "physical_attack"
threshold: 500
curve_type: "logarithmic"
factor: 100| Field | Type | Required | Default | Range | Description |
|---|---|---|---|---|---|
<rule id> (section key) | object | No | — | — | Rule id; when attribute is omitted the key itself is used as the attribute id |
attribute | string | No | section key | — | Attribute id affected by this rule; a rule pointing at an unknown attribute has no effect |
threshold | number | No | 0 | ≥ 0 (negative is treated as 0) | Value where diminishing returns begin; at or below it the value is unchanged |
curve_type | string | No | logarithmic | logarithmic / sqrt / piecewise_linear (alias linear) | Curve function; unrecognized values fall back to logarithmic and are reported by the config precheck |
factor | number | No | 1 | logarithmic / sqrt: > 0 (≤ 0 is treated as 1); piecewise_linear / linear: ≥ 0 | Curve factor: a divisor for the first two, a retention ratio for the last, where 0 is valid and drops the whole excess |
Full curve semantics and tuning advice are in Scaling Curves
Troubleshooting
- Attribute not displayed: check that the attribute file loaded, that the lore format matches, and that equipping the item triggered a refresh
- Attribute has no effect: check that the attribute ID matches the damage type, condition, or external module
- Unexpected environmental damage: check
vanilla_event_damageandallowed_damage_causes(hard_lock_damagehas no runtime effect and does not need investigating) - Health out of sync: check that the resource declares
sync_to_bukkit: true. Thehealthresource ceiling uses the modifiedMAX_HEALTH: the EA-computed value is written to the base, andmax_health_vanilla, health-boost potions, and third-party plugin modifiers stack on top without conflicting with each other. If the cap differs from expectations, enable theresourcedebug channel and inspect the health-cap resolution and max-health modifier lines to locate each modifier source - External healing reverted: the
healthcurrent value follows the player entity's health, so another plugin or command setting health directly is not wiped by the next sync. If values still fall back, enable theresourcedebug channel and compare the old current value with the engine baseline value on the natural regeneration line: a persistent gap means the resource is not synced to Bukkit, or health is being rewritten by another plugin within the same tick - Value inflation: configure
scaling_curvesfor core damage, defense, and critical attributes