Repository navigation
Trap system
The trap system adds fixed, walkable hazards. A trap detects the player through a physics sensor, applies an existing status effect once, and plays its consumption animation.
Two traps are implemented:
| Trap | Gameplay effect | Visual behaviour |
|---|---|---|
| Freeze | Applies FrozenEffect for 5,000 ms. The target is immobilised and its effective stat multipliers are zeroed; freezing does not make it immune to incoming damage. |
A small idle spike grows, holds and melts after activation. |
| Burn | Applies the existing Burning effect, configured for 1 damage per tick, a 1,000 ms cooldown and a 10,000 ms lifetime. |
Fire ignites and loops while armed, then extinguishes after activation. |
Both target the player only. Each trap entity can activate once; the status effect it applies may continue after the trap's artwork disappears. An instant-damage/spike-cut trap is not part of this implementation.
This document focuses on the six classes that construct traps, apply their effects and render their animations. Existing engine and status-effect classes are discussed only where these classes depend on them.
| Class | Responsibility |
|---|---|
TrapFactory |
Assembles matching effect, visual and physics components into a trap entity. |
TrapEffectComponent |
Abstract base class that validates player contact and implements one-shot activation. |
FreezeTrapEffectComponent |
Supplies the concrete application of the five-second frozen effect. |
BurnTrapEffectComponent |
Supplies the concrete application of the existing burning effect. |
IceTrapRenderComponent |
Displays the warning spike and triggered growth/hold/melt animation. |
FireTrapRenderComponent |
Displays ignition, looping fire and triggered extinguishing. |
The class diagram follows the notation introduced in Week 4b — Class Modelling: class names, attributes and operations are separated; members show visibility and types; relationships distinguish inheritance, composition, association and dependency. Only relevant members are shown. Component, RenderComponent, Entity, HitboxComponent and the status-effect classes are existing supporting classes, not new trap classes.
classDiagram
direction TB
class TrapFactory {
-TrapFactory()
+createFreezeTrap() : Entity$
+createBurnTrap() : Entity$
-createTrap(effect : TrapEffectComponent, visual : RenderComponent) : Entity$
}
class Entity
class Component
class HitboxComponent
class TrapEffectComponent {
<<abstract>>
-hitbox : HitboxComponent
-triggered : boolean
+create() : void
-onCollisionStart(mine : Fixture, other : Fixture) : void
#applyEffect(effects : StatusEffectsControllerComponent, combatStats : CombatStatsComponent) : void*
}
class FreezeTrapEffectComponent {
+DURATION_MILLIS : long = 5000$
#applyEffect(effects : StatusEffectsControllerComponent, combatStats : CombatStatsComponent) : void
}
class BurnTrapEffectComponent {
#applyEffect(effects : StatusEffectsControllerComponent, combatStats : CombatStatsComponent) : void
}
class RenderComponent {
<<abstract>>
}
class IceTrapRenderComponent {
+TEXTURE : String$
-frames : TextureRegion[]
-triggered : boolean
-elapsed : float
+IceTrapRenderComponent()
+create() : void
+update() : void
~currentFrame() : TextureRegion
#draw(batch : SpriteBatch) : void
+getLayer() : int
+getZIndex() : float
}
class FireTrapRenderComponent {
+START_TEXTURE : String$
+LOOP_TEXTURE : String$
+END_TEXTURE : String$
-ignition : TextureRegion[]
-burning : TextureRegion[]
-extinguishing : TextureRegion[]
-elapsed : float
-triggeredElapsed : float
-triggered : boolean
-frames(path : String) : TextureRegion[]$
-onTriggered() : void
+create() : void
+update() : void
~currentFrame() : TextureRegion
#draw(batch : SpriteBatch) : void
+getLayer() : int
+getZIndex() : float
}
class StatusEffectsFactory {
+createFrozen(time : GameTime, duration : long) : TimedStatusEffect$
+createBurn(combatStats : CombatStatsComponent) : StatusEffect$
}
class StatusEffectsControllerComponent {
+addStatusEffect(effect : StatusEffect) : void
}
TrapFactory ..> Entity : creates trap
Entity "1" *-- "0..*" Component : owns
Component <|-- TrapEffectComponent
Component <|-- RenderComponent
TrapEffectComponent "0..1" --> "0..1" HitboxComponent : hitbox
TrapEffectComponent <|-- FreezeTrapEffectComponent
TrapEffectComponent <|-- BurnTrapEffectComponent
RenderComponent <|-- IceTrapRenderComponent
RenderComponent <|-- FireTrapRenderComponent
TrapFactory ..> FreezeTrapEffectComponent : creates
TrapFactory ..> IceTrapRenderComponent : creates
TrapFactory ..> BurnTrapEffectComponent : creates
TrapFactory ..> FireTrapRenderComponent : creates
FreezeTrapEffectComponent ..> StatusEffectsFactory : createFrozen
BurnTrapEffectComponent ..> StatusEffectsFactory : createBurn
TrapEffectComponent ..> StatusEffectsControllerComponent : applies to target
-
+means public,-private,#protected and~package visibility. - Attributes use
name : Type; operations usename(parameter : Type) : ReturnType. - Static members are underlined by Mermaid's trailing
$marker. The abstract operation is italicised using its trailing*marker; abstract classes are labelled«abstract». - A solid line with a hollow triangle points from subclass to superclass. Freeze and burn specialise
TrapEffectComponent; ice and fire rendering specialiseRenderComponent. - The filled diamond at
Entitydenotes composition: it owns its attached components and disposes them with itself. One entity can have zero or more components; each attached component belongs to one entity. - The directed solid association to
HitboxComponentrepresents the storedhitboxreference. It is optional beforecreate()and required after successful creation. The reverse multiplicity describes the normal trap setup, where at most one trap-effect component refers to that hitbox. - Dashed arrows denote dependencies: factories create objects and effect code uses the status-effect API without owning those services. No hollow-diamond aggregation is needed for these relationships.
There is intentionally no direct association between an effect component and its renderer: both communicate through the entity's event handler rather than holding references to each other. The status-effects controller belongs to the target player, not the trap. Supporting physics and asset-service relationships are omitted to keep the diagram focused.
TrapFactory creates an ordinary Entity assembled from four components:
- A fire or ice render component.
- A static
PhysicsComponent. - A
HitboxComponentonPhysicsLayer.TRAP. - A concrete freeze or burn effect component.
There is no separate FreezeTrap or BurnTrap entity subclass. The concrete component types define behaviour. This fits the existing entity-component architecture and keeps physics, presentation and gameplay responsibilities separate.
TrapFactory.createFreezeTrap() and createBurnTrap() encapsulate the matching effect/renderer combinations and common physics setup. Their private helper accepts a TrapEffectComponent and a RenderComponent; it does not accept an enum and repeatedly branch on trap type.
StatusEffectsFactory separately constructs the reusable status effects. Trap code asks for an effect rather than reimplementing burn damage or freeze duration logic.
These are static factory methods, not the GoF Factory Method pattern based on overridable creator subclasses, and not an Abstract Factory hierarchy.
TrapEffectComponent defines the shared activation sequence: validate contact, find target components, call applyEffect(), mark the trap consumed and emit an event.
Subclasses override only applyEffect():
// FreezeTrapEffectComponent
effects.addStatusEffect(
StatusEffectsFactory.createFrozen(ServiceLocator.getTimeSource(), DURATION_MILLIS));
// BurnTrapEffectComponent
effects.addStatusEffect(StatusEffectsFactory.createBurn(combatStats));This is a Template Method-style design: the collision algorithm stays in the base class, while polymorphism supplies the varying step. It is not a separate interchangeable Strategy-object hierarchy.
After creation, behaviour is determined by the attached Java component type. The base collision handler invokes applyEffect() without checking whether the trap is a freeze or burn trap.
The physics listener emits collisionStart. After applying its effect, the trap emits trapTriggered. Its renderer subscribes to that event and changes animation state.
The effect component does not call animation methods, and the renderer does not apply damage. This lets artwork and animation change without changing the activation rules. It also means visual state is not the authority for whether a trap can hurt the player.
The implementation also follows the project's existing ServiceLocator approach for time and assets. This avoids introducing a new service mechanism, but means those services and loaded textures must be available before constructing trap renderers.
A sensor reports overlap without physically blocking movement. HitboxComponent.create() calls setSensor(true) before creating its fixture. The trap therefore needs no solid ColliderComponent alongside its hitbox.
- The static body anchors the trap in place.
- The sensor detects a player stepping onto it without pushing the player away.
-
PhysicsLayer.TRAPlabels the fixture as a trap. The new layer alone does not implement activation or make a fixture non-solid. -
TrapEffectComponentexplicitly checks the other fixture's category againstPhysicsLayer.PLAYER, so enemies do not consume these traps.
The factory sets trap scale to 0.5f × 0.5f, matching the current one-tile footprint. Renderers deliberately draw artwork beyond that footprint. Tall flames and growing ice do not enlarge the trigger area.
On collisionStart, the base effect component:
- Ignores the event if
triggeredis already true or the contacting fixture is not this trap's hitbox. - Requires the other fixture to be on the player layer.
- Resolves its entity through
BodyUserData. - Requires both
CombatStatsComponentandStatusEffectsControllerComponenton the target. - Calls the concrete
applyEffect()implementation. - Sets
triggered = trueand emitstrapTriggered.
Invalid contacts leave the trap armed. Under normal collision-event processing, subsequent valid contacts are ignored after successful activation, including contact with another fixture of the same player. There is no rearm timer, repeat-damage loop or collisionEnd reset in the trap component.
The renderers have their own trigger flags solely for animation. The gameplay flag in TrapEffectComponent prevents another effect application.
Once added, the target's StatusEffectsControllerComponent owns the effect: it updates it, removes it when finished and performs removal callbacks. The trap does not need to remain visible for burning or freezing to continue.
-
Freeze:
createFrozen(time, duration)returns a freshFrozenEffect. This is the full immobilising effect, not the older similarly namedcreateFreeze(combatStats)slow-effect helper. The factory method is also available to the freeze spell so both callers can use the same effect type. -
Burn:
createBurn(combatStats)returns a freshBurninginstance tied to the target's combat stats. Its ticks calltakeDamage(1)rather than bypassing the damage system. - Multiple effects: The controller stores separate instances. A burn and freeze can coexist; adding one does not automatically replace the other. Separate burn applications have independent timers. Separate freezes are not merged into a single refreshed duration; immobilisation remains while an applicable freeze remains active.
-
Enemy attacks: The trap does not control enemy attacks. A frozen victim can still take incoming damage. Repeating enemy contact damage belongs to the separately implemented
TouchAttackComponentfix, not the trap lifecycle.
The trap does not itself check concealment, shields or invulnerability before adding an effect. Any damage mitigation is handled by the relevant effect/combat code, not by a general “trap immunity” rule.
Both renderers use nearest-neighbour texture filtering to preserve pixel art. They draw on layer 0 at z-index 1: above the terrain's z-index 0, but below characters on layer 1. This prevents the tall artwork from covering the player as though the player were underneath a floor tile.
FireTrapRenderComponent uses three sheets, each containing four 18 × 59 frames at 0.1 seconds per frame:
-
fire-start.png: ignition during the first 0.4 seconds of entity animation. -
fire-loop.png: repeating flame while armed and before the triggered ending phase. -
fire-end.png: begins 0.8 seconds after activation and runs for 0.4 seconds.
After approximately 1.2 seconds from activation, currentFrame() returns null and drawing stops. The renderer anchors the canvas at the floor and offsets it one native pixel left. Pixel size is derived from the trap width divided by 16, rather than stretching the tall sheet into the sensor rectangle.
IceTrapRenderComponent reads twenty 32 × 32 frames from a five-column sheet:
- Armed: frame 0 remains visible as a warning spike.
- Triggered growth: frames 0–12 over 1.3 seconds.
- Hold: frame 12 for 0.5 seconds.
- Melt: frames 13–19 over 0.7 seconds.
- Finished: no frame is drawn after approximately 2.5 seconds.
The artwork is drawn two tiles wide/high and centred horizontally over the one-tile sensor. The five-second freeze outlasts the growth/melt sequence; animation completion does not thaw the player.
The implementation currently uses different timing mechanisms:
| System | Timing source | Consequence |
|---|---|---|
| Fire and ice animation | Accumulated ServiceLocator.getTimeSource().getDeltaTime() in seconds. |
Animation follows scaled frame time and only advances when updated. |
| Frozen effect |
TimedStatusEffect deadline using the supplied clock's getTime() in milliseconds. |
Expiration follows elapsed clock time, not accumulated animation time. |
| Burning effect | Its own GameTime instance and elapsed millisecond checks. |
Tick scheduling and lifetime are separate from renderer timing and the shared clock's time scale. |
GameTime.setTimeScale() affects delta time, not getTime(). Therefore pause/time-scaling consistency is not guaranteed merely because both systems use GameTime. If updates pause, clock deadlines can still pass and be observed on resumption. No broad timing refactor is part of the trap implementation.
The burn factory's 1,000 ms cooldown is configuration, not an exact ten-tick guarantee: Burning checks elapsed time during updates, uses a strict > comparison and initialises its last-tick field to zero. Its first tick depends on the first eligible update. Do not infer exact total damage from the visual flame duration.
The effect and render components do not remove the entity when its animation finishes. The sensor remains present, but the gameplay flag ignores further activation and the renderer stops drawing. Entity disposal remains the responsibility of the owning system.
The trigger flag belongs to each component instance. A newly constructed trap starts armed; these classes do not persist consumed state or automatically rearm an existing trap.
All four trap textures must be available through the resource service before TrapFactory constructs render components, since frame extraction happens during construction. Renderers use shared textures rather than owning separate copies.
| Asset | Source and repository attribution |
|---|---|
| Fire start/loop/end sheets | Ranitaya Studios, Massive Dungeon Pack. See RANITAYA-ATTRIBUTION.md. |
| Ice-spike sheet | Coloritmic, extended Ice Cool Pack. See COLORITMIC-ATTRIBUTION.md. |
The imported sheets retain their original image data under project filenames. The purchased packs' terms apply; the attribution files are not a replacement licence granting unrestricted redistribution.
For a new status-effect trap:
- Add a concrete subclass of
TrapEffectComponentand implementapplyEffect()using the relevant status-effect factory. - Reuse a suitable renderer or add one that listens for
trapTriggered. - Add a typed method such as
TrapFactory.createPoisonTrap()to assemble the matching effect and visual components. - Ensure any new textures are loaded before construction and document their source/licence.
An instant-damage trap could use the provided combat-stats argument to call the normal damage API rather than inventing a lasting status effect solely to represent a hit. This is a possible extension, not implemented behaviour. The current base class still requires a status-effects controller on every valid target, so supporting targets without one would require a deliberate change to that contract.
Repeated or rearming traps would require an explicit change to the one-shot lifecycle. Restarting an animation alone would not rearm gameplay.