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.
Roll
ThreadLocalRandom.current().nextDouble(100)(a value in[0, 100)) against the attacker'sastralstats:critical_chancestat. If the roll is greater than the stat value, the handler returns and no crit happens.On success,
originalDamageis the event's current damage (e.getDamage()) at the point thisHIGHhandler runs. The critical damage isoriginalDamage × (critical_damage / 100), wherecritical_damageis the attacker'sastralstats:critical_damagestat.The event's
MAGICdamage modifier is set to that value:e.setDamage(EntityDamageEvent.DamageModifier.MAGIC, criticalDamage).
Stat key | Role |
|---|---|
| Roll threshold, percent ( |
| Multiplier applied to the pre-crit damage, expressed as a percent (e.g. a base of |
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.
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 |
|---|---|
| Percent taken off every hit. |
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 |
|---|---|
| Stores |
|
|
| Removes the player's timestamp immediately. |
CombatListener drives it from two events:
EntityDamageEventatEventPriority.HIGH(ignoreCancelled = true) — whenever the damaged entity is aPlayer, callsrecordDamage. This fires for any damage cause, not just PvP or mob damage.PlayerQuitEvent— callsclearCombat, 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 |
| Effect |
|---|---|---|---|---|
|
|
| No | Scales damage down by |
|
|
| No | Rolls |
|
|
| Yes | Records the combat timestamp. |
|
|
| Yes | Absorbs remaining damage from the |
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:
Add a blueprint YAML under
plugins/AstralStats/blueprints/for each key you want active —astralstats:critical_chance,astralstats:critical_damage, and/orastralstats: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: 100Run
/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.Set or adjust values per player with
/stats set <player> <statType> <value>(writes the stat's base value directly), or programmatically by attaching aStatModifierthroughStatsAPI.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,
statValuefallback behavior, unregistered stats.Resources — the
SHIELDresource, its regeneration formula, and shield absorption.Configuration —
combat-cooldowninconfig.yml.Commands —
/stats set,/stats reload,/stats modifiers add/addtimed.Developer API —
StatsAPI.addModifier/addTimedModifier.