Skip to content

On Hit Effect Component

Sarah Preston edited this page Sep 15, 2026 · 2 revisions

On Hit Effect Component

Overview

Some enemies should have a chance to apply a bonus effect — poison, stun, or similar — when they successfully land a hit, on top of the base damage already dealt. OnHitEffectComponent provides this: it subscribes to one or more configured attack events (e.g. meleeAttack and/or a future rangedAttackHit), rolls a probability on each hit, and on success applies a health-over-time (or instant) effect to the target. This mirrors the pattern used by Status Effects and Melee Attacks — a single configurable component reacting to an event, decoupled from the component that fired it.

On Hit Effect Component

public OnHitEffectComponent(List<String> hitEventNames, float triggerChance, int effectDuration, int effectHealthChange) {}

OnHitEffectComponent extends Component directly. It is responsible for:

  • Listening for every event name in hitEventNames on the owning entity.
  • Rolling a random value against triggerChance on each configured event.
  • On success, applying effectHealthChange to the target's CombatStatsComponent — instantly if effectDuration is 0, or gradually over effectDuration ticks otherwise.
  • Tracking any in-progress over-time effect itself and advancing it each update(), applying the exact remaining amount on the final tick so the total is precise even when effectHealthChange doesn't divide evenly across ticks.

Providing a null or empty hitEventNames list is invalid. Providing a triggerChance outside (0, 1] is invalid. Providing an effectDuration less than 0 is invalid.

Usages

This can be used for:

  • A base enemy's guaranteed-ish poison-on-hit, e.g. triggerChance = 0.3f over a handful of ticks
  • A rarer bonus effect, e.g. triggerChance = 0.1f, on the same or a different enemy
  • Enemies that should apply the same bonus effect on both melee and ranged hits, via a single instance configured with both event names

Example

new OnHitEffectComponent(List.of("meleeAttack"), 0.3f, 5, -10)

(providing hitEventNames = ["meleeAttack"], triggerChance = 0.3f, effectDuration = 5, and effectHealthChange = -10) creates an on-hit effect with a 30% chance per hit to apply -10 health to the target, spread across 5 ticks.

Design Note

OnHitEffectComponent does not attach a HealthEffectComponent to the target — Entity.addComponent() rejects additions made after Entity.create(), and a hit target has always already been created. Health changes are instead applied directly via CombatStatsComponent.addHealth(int), with the per-tick math mirroring HealthEffectComponent's own approach. A consequence worth knowing: because the component lives on the attacker rather than the target, an in-progress effect only keeps ticking while the attacker's own update() runs — if the attacker is removed mid-effect, the target will not receive the remaining ticks.

An entity can only carry one component of a given class, so an enemy needing two distinct on-hit effects (e.g. a common poison and a rare stun) cannot currently express that with two OnHitEffectComponent instances on the same entity.

Events

Configured hit events (hitEventNames)

OnHitEffectComponent does not fire an event of its own — it only listens. Any event listed in hitEventNames (e.g. meleeAttack, fired by MeleeAttackComponent after a successful hit) triggers onAttackHit(Entity target).

Configuration

Not currently constructed from an entity config field the way MeleeAttackComponent is — attached directly at spawn time with the desired hitEventNames/triggerChance/effectDuration/effectHealthChange values. Could be extended to a config-driven approach (e.g. an OnHitEffectConfig composition block, following the same nullable-composition pattern as MeleeAttackConfig) if per-enemy tuning via config becomes necessary.

OnHitEffectComponent Tests

Testing was done to ensure OnHitEffectComponent correctly reacts to a successful melee hit and applies its configured effect according to its trigger probability.

Setup

To set up these tests, an owner entity (carrying the OnHitEffectComponent under test) and a target entity (carrying the effect-receiving component relevant to the effectType under test, e.g. CombatStatsComponent) were created. The "meleeAttackHit" event was triggered directly on the owner in each test, bypassing MeleeAttackComponent entirely, so only OnHitEffectComponent's own logic was exercised.

Constructor Validation Tests

Tests were used to ensure the following situations were working:

  • A triggerChance outside the valid 0.0–1.0 range is rejected
  • An unrecognised effectType is rejected or falls back to the documented default behaviour
  • Valid effectType, triggerChance, and magnitude values construct the component successfully

Trigger Probability Tests

Tests were used to ensure the following situations were working:

  • A forced-success roll applies the configured effect to the target
  • A forced-failure roll does not apply any effect to the target
  • Poison-type effects are probability-gated the same as any other effect type (not guaranteed on every hit)

Multiple Effects Tests

Tests were used to ensure the following situations were working:

  • Two or more OnHitEffectComponent instances with different effectTypes attached to the same owner react independently to a single "meleeAttackHit" event
  • One effect triggering does not prevent or interfere with another effect's own roll

Event Wiring Tests

Tests were used to ensure the following situations were working:

  • create() correctly subscribes to "meleeAttackHit"
  • dispose() correctly unsubscribes, so a disposed component no longer reacts to further hits

Note on RandomSource design dependency

The Trigger Probability Tests above assume the component's implementation exposes an injectable/seedable source of randomness, so that "forced success" and "forced failure" can be tested deterministically rather than through repeated statistical runs. This is a design assumption flagged for confirmation against the actual implementation before these tests are written.

Clone this wiki locally