Astral Realms Documentation Help

Combat Mechanics {id="combat-mechanics"}

AstralStats layers three combat mechanics on top of the stat and resource systems, each driven by a plain StatType value: critical strikes amplify outgoing damage, damage reduction and shield absorption both mitigate incoming damage, and a lightweight combat state tracker gates off-combat-only resource regeneration. All four are wired up as Bukkit listeners registered in AstralStats#onEnable; none of them require any plugin configuration beyond the stat blueprints that back them — see Stats & Modifiers for the blueprint format.

Critical strikes

CriticalDamageListener listens to EntityDamageByEntityEvent at EventPriority.HIGH. It only proceeds when the damage source's direct entity is a Player and the entity being hit is a LivingEntity — indirect damage (e.g. from a thrown projectile or another mob) does not roll a crit.

  1. Roll ThreadLocalRandom.current().nextDouble(100) (a value in [0, 100)) against the attacker's astralstats:critical_chance stat. If the roll is greater than the stat value, the handler returns and no crit happens.

  2. On success, originalDamage is the event's current damage (e.getDamage()) at the point this HIGH handler runs. The critical damage is originalDamage × (critical_damage / 100), where critical_damage is the attacker's astralstats:critical_damage stat.

  3. The event's MAGIC damage modifier is set to that value: e.setDamage(EntityDamageEvent.DamageModifier.MAGIC, criticalDamage).

Stat key

Role

astralstats:critical_chance

Roll threshold, percent (0100). The attacker crits when the [0, 100) roll lands at or below this value.

astralstats:critical_damage

Multiplier applied to the pre-crit damage, expressed as a percent (e.g. a base of 150 scales damage ×1.5).

No blueprint ships by default

Neither critical_chance nor critical_damage has a shipped blueprint file (the repository only ships attack-damage, attack-speed, shield/max-shield, and shield/shield-regeneration — see Overview). With no blueprint, StatService.statValue finds no registered stat for the player and falls back to -1.0. Since ThreadLocalRandom.nextDouble(100) always returns a value ≥ 0, the roll > -1 is always true, so the listener always returns early and critical strikes never fire until you add blueprints for both keys.

Damage reduction

DamageReductionListener listens to the broader EntityDamageEvent (any damage cause, not just entity-vs-entity) at the default EventPriority.NORMAL, and only acts on events where the damaged entity is a Player.

newDamage = damage × (1 - astralstats:damage_reduction / 100)

The handler reads the player's astralstats:damage_reduction stat and returns immediately — applying no reduction — when the value is <= 0. This also covers the unregistered default: with no blueprint, statValue returns -1.0, which is <= 0, so reduction is skipped exactly the same way it is for a player with a genuine 0 stat.

Stat key

Role

astralstats:damage_reduction

Percent taken off every hit. 100 would reduce damage to 0; the listener does nothing for values <= 0.

Shield absorption

The third mitigation layer is resource-based rather than stat-based: ShieldListener runs at EventPriority.HIGHEST (ignoreCancelled = true) — after critical damage and damage reduction have already adjusted the event — and drains the player's SHIELD resource to absorb the remaining damage before it reaches health. See Resources for the absorption formula, the SHIELD resource's blueprint keys, and its regeneration behavior.

Combat state

CombatService tracks a per-player "last damaged" timestamp (Map<UUID, Long>) independent of the stats/ resources systems:

Method

Behavior

recordDamage(UUID)

Stores System.currentTimeMillis() for the player.

isInCombat(UUID)

false if no timestamp is recorded. Otherwise compares elapsed time against combat-cooldown (config.yml); if the cooldown has passed the entry is removed and false is returned, otherwise true.

clearCombat(UUID)

Removes the player's timestamp immediately.

CombatListener drives it from two events:

  • EntityDamageEvent at EventPriority.HIGH (ignoreCancelled = true) — whenever the damaged entity is a Player, calls recordDamage. This fires for any damage cause, not just PvP or mob damage.

  • PlayerQuitEvent — calls clearCombat, so combat state never persists across a disconnect/reconnect.

The cooldown itself is combat-cooldown in config.yml (default 10s) — see Configuration.

Why it matters

Combat state is consumed by the resource system, not by any of the three mechanics above: PlayerResource.regeneration checks combat().isInCombat(uuid) && blueprint.offCombatOnly() and returns 0 (no regeneration this tick) when both are true. Out of the box, SHIELD is the only resource with off-combat-only: true in resources.yml, so it is the only one whose regeneration currently pauses while the player is in combat — see Resources for the full regeneration formula.

Listener order

All four listeners react to the same family of damage events, so their relative priority determines what each one sees. Bukkit fires handlers from LOWEST to MONITOR; within a shared priority, handlers run in the order they were registered (CriticalDamageListener before CombatListener here).

Listener

Event

Priority

ignoreCancelled

Effect

DamageReductionListener

EntityDamageEvent

NORMAL (default)

No

Scales damage down by damage_reduction%.

CriticalDamageListener

EntityDamageByEntityEvent

HIGH

No

Rolls critical_chance; on success overwrites the MAGIC damage modifier.

CombatListener

EntityDamageEvent

HIGH

Yes

Records the combat timestamp.

ShieldListener

EntityDamageEvent

HIGHEST

Yes

Absorbs remaining damage from the SHIELD resource.

Execution order is therefore: damage reduction first, then the critical roll and combat recording, then shield absorption last — each stage sees the damage value as adjusted by every stage before it.

Setup checklist

Critical strikes and damage reduction are inert on a fresh install because their backing stats have no blueprint. To enable them:

  1. Add a blueprint YAML under plugins/AstralStats/blueprints/ for each key you want active — astralstats:critical_chance, astralstats:critical_damage, and/or astralstats:damage_reduction. See Stats & Modifiers for the full blueprint schema; minimally:

    display-name: "<red>Critical chance" key: "astralstats:critical_chance" base: 5 min: 0 max: 100
  2. Run /stats reload (or restart) — it re-reads every blueprint file and re-registers each online player's stats from the refreshed blueprint map. See Commands.

  3. Set or adjust values per player with /stats set <player> <statType> <value> (writes the stat's base value directly), or programmatically by attaching a StatModifier through StatsAPI.addModifier(...)/StatsAPI.addTimedModifier(...). See Commands and Developer API.

A stat with no blueprint cannot be set by /stats set either — setStatBaseValue looks up the player's existing StatInstance for that key and does nothing if it was never registered. The command still reports success ("Set stat ... to ...", unless silent-command-success is enabled) even when the underlying stat doesn't exist, so a "successful" /stats set critical_chance ... on a server that never added the blueprint has no effect. The blueprint has to exist before any command or API call can affect that stat.

See also

  • Stats & Modifiers — blueprint format, statValue fallback behavior, unregistered stats.

  • Resources — the SHIELD resource, its regeneration formula, and shield absorption.

  • Configurationcombat-cooldown in config.yml.

  • Commands/stats set, /stats reload, /stats modifiers add/addtimed.

  • Developer APIStatsAPI.addModifier/addTimedModifier.

Last modified: 25 July 2026