Skip to content

Trap system

Abhinav Pradeep edited this page Oct 8, 2026 · 1 revision

Traps

Overview

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 Responsibilities

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.

Architecture and UML

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
Loading

Reading the diagram

  • + means public, - private, # protected and ~ package visibility.
  • Attributes use name : Type; operations use name(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 specialise RenderComponent.
  • The filled diamond at Entity denotes 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 HitboxComponent represents the stored hitbox reference. It is optional before create() 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.

Design Patterns Used

Entity-component composition

TrapFactory creates an ordinary Entity assembled from four components:

  1. A fire or ice render component.
  2. A static PhysicsComponent.
  3. A HitboxComponent on PhysicsLayer.TRAP.
  4. 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.

Typed static factories

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.

Template Method and polymorphism

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.

Observer/event-driven animation

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.

Shared services

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.

Physics: Why a Sensor?

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.TRAP labels the fixture as a trap. The new layer alone does not implement activation or make a fixture non-solid.
  • TrapEffectComponent explicitly checks the other fixture's category against PhysicsLayer.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.

Activation and One-Shot State

On collisionStart, the base effect component:

  1. Ignores the event if triggered is already true or the contacting fixture is not this trap's hitbox.
  2. Requires the other fixture to be on the player layer.
  3. Resolves its entity through BodyUserData.
  4. Requires both CombatStatsComponent and StatusEffectsControllerComponent on the target.
  5. Calls the concrete applyEffect() implementation.
  6. Sets triggered = true and emits trapTriggered.

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.

Effect Ownership and Interactions

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 fresh FrozenEffect. This is the full immobilising effect, not the older similarly named createFreeze(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 fresh Burning instance tied to the target's combat stats. Its ticks call takeDamage(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 TouchAttackComponent fix, 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.

Animation and Layering

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.

Fire

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.

Ice

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.

Timing Limitations

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.

Consumed Trap State

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.

Assets and Attribution

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.

Adding Another Trap

For a new status-effect trap:

  1. Add a concrete subclass of TrapEffectComponent and implement applyEffect() using the relevant status-effect factory.
  2. Reuse a suitable renderer or add one that listens for trapTriggered.
  3. Add a typed method such as TrapFactory.createPoisonTrap() to assemble the matching effect and visual components.
  4. 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.

Table of Contents

Home

Test Plan

Team 2

Features

Components
Player
Items and Inventory
Enemies
Interactable NPCs
Cutscenes and Dialogue
Music
Bosses
Rooms
Weapons
Abilities & Spells
Game Screens and Areas
Dungeons

Game Engine

Getting Started

Entities and Components

Service Locator

Loading Resources

Logging

Unit Testing

Debug Terminal

Input Handling

UI

Animations

Audio

AI

Physics

Terrain

Concurrency & Threading

Settings

Troubleshooting

MacOS Setup Guide

Clone this wiki locally