Lightning Attack - UQcsse3200/2026-studio-1 GitHub Wiki
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.
How it works
- 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.
Components
ArrowFactory.createLightning
public static Entity createLightning(
Vector2 targetPosition,
float skyOffset,
float speed,
int damage,
int freezeTicks,
short targetLayer)
VerticalMovementStrategy
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.
LightningFreezeComponent
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.
SpeedEffectComponent (on the player)
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.
RangedAttackComponent / RangedAttackTask
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.
Using it on an enemy
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);
Tests
SpeedEffectComponentTest: validation, per-effect keys and timers, stacking, revert timing, cleanup on disposeProjectileComponentTest: 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 argumentLightningFreezeComponentTest: freeze on hit, unfreeze after the configured ticks, zap soundArrowFactoryLightningTest: 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.
Attributions
Zap sound
https://pixabay.com/sound-effects/film-special-effects-electric-zap-001-6374/