Repository navigation
Lightning Attack
A ranged enemy attack that strikes the player from above. A bolt spawns in the sky above where the player is standing, falls straight down, and on contact freezes the player in place for a couple of seconds and applies damage as well as playing a zap sound effect. It is not blocked by floors.
- An enemy's
RangedAttackTaskis created withProjectileType.LIGHTNING. While the target is within range, it triggers the"rangedAttack"event every frame with(target, projectileType). -
RangedAttackComponent.attemptAttackruns its cooldown, range and target checks. If they pass, it switches on the projectile type and callsArrowFactory.createLightning(...), then registers the bolt with the entity service. - The bolt falls under
ProjectileComponent, which delegates movement toVerticalMovementStrategy(a constant downward velocity on a kinematic body). - When the bolt's hitbox overlaps the player,
ProjectileHitComponentapplies damage and fires"projectileHit"with the player as the payload, then fires"projectileExpired"to end the bolt's flight. -
LightningFreezeComponentlistens for"projectileHit", plays the zap sound, and triggers"applySpeedEffect"on the player with(freezeTicks, 0f). - The player's
SpeedEffectComponenthandles that event. It registers a0speed multiplier withPlayerActions, and removes it again oncefreezeTicksticks have elapsed. -
ProjectileComponentdespawns the bolt.
public static Entity createLightning(
Vector2 targetPosition,
float skyOffset,
float speed,
int damage,
int freezeTicks,
short targetLayer)A ProjectileMovementStrategy that sets a constant velocity of (0, -speed) once in start(...). update(...) is a no-op because the kinematic body keeps its velocity. Throws IllegalArgumentException for a non-positive speed.
Attached to the bolt. Listens for "projectileHit" and, on a hit, plays the zap sound and triggers "applySpeedEffect" on the target with (freezeTicks, 0f). It does not need to know whether the target is actually a player. If the target has no SpeedEffectComponent, nothing happens.
A permanent component on the player that listens for "applySpeedEffect". Each application creates its own tracked effect with its own timer and its own key in PlayerActions, so:
- Multiple effects stack multiplicatively.
- Each one reverts independently when its own timer ends.
- A second lightning strike while frozen does not cancel or shorten the first.
A duration of 0 is permanent and never reverts. A multiplier of 0 pauses movement, below 1 slows, above 1 speeds up.
RangedAttackTask carries a ProjectileType and passes it along in the "rangedAttack" event. RangedAttackComponent chooses what to spawn based on it. Cooldown resets when a shot is fired, not when it lands, so a missed bolt does not cause the enemy to fire every frame.
Give the enemy a RangedAttackComponent as usual, and give its AI task ProjectileType.LIGHTNING:
new RangedAttackTask(player, priority, range, ProjectileType.LIGHTNING)The player entity must have SpeedEffectComponent (and PlayerActions) added at creation:
.addComponent(new SpeedEffectComponent())Entity components can only be added before the entity is created, which is why the effect components are permanent and event-driven rather than added on demand.
To spawn a bolt directly, without an enemy:
Entity bolt = ArrowFactory.createLightning(player.getCenterPosition(), 8f, 12f, 0, 120, PhysicsLayer.PLAYER);
ServiceLocator.getEntityService().register(bolt);-
SpeedEffectComponentTest: validation, per-effect keys and timers, stacking, revert timing, cleanup on dispose -
ProjectileComponentTest: range and event-driven despawn (with a mockedGdx.appso the deferred runnable can be captured and run) -
RangedAttackComponentTest: cooldown, range and firing, including the projectile-type argument -
LightningFreezeComponentTest: freeze on hit, unfreeze after the configured ticks, zap sound -
ArrowFactoryLightningTest: spawn position and scale
Several of these tests replace Gdx.app with a mock and must restore the real one in @AfterEach, otherwise later tests that rely on the headless application will silently stop running their postRunnable callbacks.
https://pixabay.com/sound-effects/film-special-effects-electric-zap-001-6374/