Snake Miniboss Remake - UQcsse3200/2026-studio-4 GitHub Wiki

Overview

This page documents the Dungeon 2 Snake miniboss rework in PR #223, based on revision 16a98c0.

The original Snake miniboss page remains a record of the earlier implementation. This rework replaces its coil and poison-pool attacks with a two-phase encounter built around burrowing, aimed poison volleys and collectible shields.

The Snake retains 150 maximum health and uses the existing character artwork. Its size is increased to 2 × 2 world units. The encounter gives players short attack opportunities between burrow strikes, then adds projectile avoidance and gem collection at half health.

Phase 1: Burrow and emerge

The Snake enters with a green dust effect and repeats the following cycle:

State Duration Behaviour
Burrowing 0.45 seconds The Snake and its health bar disappear; incoming damage is blocked.
Underground movement 2.4 seconds The Snake moves around the player, leaving green trails.
Ground warning 0.9 seconds An orange-red circle marks a fixed emergence position near the player.
Emergence and exposure 1.5 seconds The Snake emerges, strikes once and remains vulnerable before burrowing again.

The warning has a radius of 0.9 world units and stops following the player once placed. Emergence deals 5 direct damage if the player's centre is inside the circle at that instant, unless a shield blocks it. Moving out avoids the attack; entering afterwards does not cause another hit.

The Snake cannot be damaged underground. Players can attack during its 1.5-second exposure window. Safe emergence positions are checked against room bounds and static obstacles.

Phase 2: Poison volleys and burrowing

Phase 2 begins at 50% health or below. The Snake finishes its current burrow cycle and exposure window before its first poison attack. It then alternates poison attacks with the existing burrow cycle.

Each poison attack has a 0.6-second windup, five volleys of four projectiles and a 0.35-second recovery. Volleys are 0.4 seconds apart, giving 20 projectiles per attack.

The Snake uses its existing horizontal, tongue-out chase pose while spitting. It turns towards the player before each volley, and shots originate from its mouth. Each volley forms a 54-degree fan with a slightly different angular offset. Once emitted, projectiles travel straight and do not home towards the player.

Poison projectiles deal 1 direct damage each, with no damage over time. Their visual size is enlarged to 1.2 world units while the collision radius remains 0.12. Solid obstacles and room boundaries stop them. Already-fired projectiles remain active when the Snake burrows.

The Snake is vulnerable during the windup, volleys, recovery and emergence window. The old coil, poison-pool and automatic contact attacks are no longer connected to this miniboss.

Green gems and shield

Phase 2 introduces rotating green gem pickups. Each appears with a green burst and becomes collectible after its 0.35-second appearance effect.

Setting Default
Initial gems 2, subject to safe placement
Further spawn attempts 1 every 5 seconds
Maximum active gems 3
Gem lifetime 12 seconds
Shield capacity 6 blocked Snake attacks

New gems spawn within the current camera view and room bounds. Placement alternates a preference for nearby and farther visible positions, checks obstacles and requires a clear approach from the player. If no safe visible position exists, that drop is skipped. Existing gems stay at their world positions when the camera moves.

Collecting a gem fills one green durability bar and displays a translucent green shield around the player. Further pickups refill the same bar; they do not add another shield or stack capacity.

The shield blocks both poison projectiles and emergence strikes. Each intercepted attack consumes one durability point and deals no damage. Durability does not drain over time, and dodging uses none. The last point fully blocks its attack; the shield and bar then disappear. Other sources of damage retain their normal behaviour.

Low-health healing

Every pickup checks the player's current health:

  • Below 40% maximum health: restore 15% of maximum health, with a 90% maximum-health cap.
  • At or above 40%: refill the shield without healing.

Healing is rounded to whole health points and cannot revive a dead player. The condition is checked again for every gem. For example, with 100 maximum health, a pickup at 30 health restores it to 45; another pickup then refills only the shield. The 90% cap does not mean pickups continue healing towards 90%.

Visuals and feedback

Actual damage triggers a green impact effect alongside the player's existing red damage flash. Dodged or shielded attacks do not trigger hurt effects; a successful block briefly brightens the shield.

The existing Snake sprite sheet is credited to Shravika in NPCFactory. The horizontal spit pose is an existing single-frame region, rotated to aim at the player. Burrow dust, trails and the ground warning are drawn in code.

Added assets provide poison formation, flight and fading, green hit feedback, rotating gems, gem appearance and the translucent shield. Sources and attribution notes are recorded in ASSETS.md.

Implementation and integration

Component Responsibility
SnakeBurrowComponent / SnakeBurrowArena Phase progression, burrow timing, emergence attacks and safe positions
SnakeBurrowVisualComponent / SnakePlayerHitVisualComponent Dust, trails, warnings and damage feedback
SnakePoisonVolleyComponent / SnakePoisonCollision Mouth aiming, volleys and projectile collisions
SnakeShieldPickupComponent / SnakeGemSpawnArea Visible gem placement, collection and healing
SnakeShieldComponent Shared shield durability and rendering

NPCFactory.createSnakeMiniBoss connects these components. EnemyManagerComponent supplies room bounds and the gameplay camera, while RoomAssets manages textures. Default tuning is held in SnakeMiniBossConfig.

Existing enemy death, item drops and room-clear handling are retained. Death or room disposal clears encounter effects, projectiles, gems and the shield. Player health locking and the local shortcut to the miniboss room are excluded from this rework.

Design choices and patterns

The rework builds on the game's existing component framework. NPCFactory.createSnakeMiniBoss(target) assembles a normal Entity; there is no Snake entity subclass. All six Snake components belong to that entity, including the shield, gems and hit effect that act around the player. Projectiles and gems are private Shot and Gem objects held by their components, rather than independently registered entities.

Design choice Application and reason Tradeoff
Component composition The controller handles encounter timing, while dedicated components handle burrow visuals, projectiles, pickups, shield durability and player-hit artwork. Existing combat, physics and death components remain reusable. Components discover collaborators through getComponent() during create(), so the factory and lifecycle must supply the expected combination. This is a component-based architecture, not a claim that the game uses a data-only ECS.
Static factory NPCFactory.createSnakeMiniBoss() assembles and configures the encounter in one place, using createBaseNPC() for common enemy components. Assembly changes require editing this factory. It is a static creation method, not the GoF Factory Method pattern based on subclass overrides.
Finite-state machine SnakeBurrowComponent.State and the update() switch make the six encounter states explicit. enter() resets state timing; each update advances at most one state so a slow frame cannot skip the warning. State behavior remains in one controller. This implementation does not use separate GoF State objects or interchangeable Strategy objects.
Observer-style events Existing entity events notify death cleanup and health-bar visibility. Player health updates also drive the existing red damage flash. The controller can publish enemyHealthBarVisible without manipulating the UI table directly. Event names and argument types must agree at runtime. The green Snake hit effect uses a direct play() call after confirmed, nonfatal health loss.
Existing Service Locator Components obtain the shared clock and resources through ServiceLocator; pickup placement can obtain the world camera there when no camera is supplied. These dependencies are less visible than constructor parameters and require service setup in tests. The player target and encounter configuration are passed explicitly to constructors.

Class structure

These diagrams show selected structural relationships. Solid diamonds denote ownership, hollow triangles denote inheritance, solid arrows denote stored references, and dashed arrows denote creation or method-call dependencies. Unattached components have no owner; an attached component has one. An entity permits at most one component of each concrete component type.

classDiagram
    direction TB
    class NPCFactory {
        +createSnakeMiniBoss(Entity target) Entity$
        +createBaseNPC() Entity$
    }
    class Entity {
        +addComponent(Component component) Entity
        +getComponent(Class type) Component
        +getEvents() EventHandler
        +create()
        +update()
        +dispose()
    }
    class Component {
        #Entity entity
        +create()
        +update()
        +dispose()
    }
    class RenderComponent {
        <<abstract>>
        +render(SpriteBatch batch)
        #draw(SpriteBatch batch)*
    }
    class EventHandler {
        +addListener(String eventName, EventListener0 listener)
        +trigger(String eventName)
    }
    NPCFactory ..> Entity : creates and assembles
    Entity "0..1" *-- "0..*" Component : owns attached components
    Entity "1" *-- "1" EventHandler : owns
    Component <|-- RenderComponent
Loading

SnakeBurrowComponent extends Component. SnakeBurrowVisualComponent, SnakePlayerHitVisualComponent, SnakePoisonVolleyComponent, SnakeShieldPickupComponent and SnakeShieldComponent each extend RenderComponent. The next diagram focuses on attack coordination; inherited owner references, rendering dependencies and the visual collaborators are omitted. The optional multiplicities reflect nullable collaborators accepted by the code; the production factory supplies all of them.

classDiagram
    direction TB
    class SnakeBurrowComponent {
        -Entity target
        -SnakeMiniBossConfig config
        -SnakeBurrowArena arena
        -State state
        +update()
        -updateStageTwo()
        -emerge()
        -updateSpitting()
    }
    class SnakePoisonVolleyComponent {
        -Entity target
        -List~Shot~ shots
        +beginSpit()
        +fireVolley(int waveIndex)
        +endSpit()
        -advance(Shot shot, float delta) boolean
    }
    class SnakeShieldPickupComponent {
        -Entity target
        -List~Gem~ gems
        +start()
        +update()
        -healIfLowHealth()
    }
    class SnakeShieldComponent {
        -Entity target
        -int capacity
        -int durability
        +refill()
        +tryBlock() boolean
    }
    class SnakeBurrowArena {
        +constrain(Vector2 centre, Vector2 halfSize) Vector2
        +findClearCentre(Vector2 desired, Vector2 halfSize) Optional~Vector2~
    }
    SnakeBurrowComponent "1" *-- "1" SnakeBurrowArena : owns
    SnakeBurrowComponent --> "0..1" SnakePoisonVolleyComponent : poison
    SnakeBurrowComponent --> "0..1" SnakeShieldPickupComponent : shieldPickups
    SnakeBurrowComponent ..> SnakeShieldComponent : tryBlock
    SnakePoisonVolleyComponent --> "0..1" SnakeShieldComponent : shield
    SnakeShieldPickupComponent ..> SnakeShieldComponent : refill
Loading

The controller owns its SnakeBurrowArena helper, but the sibling components are owned by the Snake Entity. The shield is one shared object for both attack types. refill() sets its durability back to capacity; it does not create another shield.

Sequence: emergence, block and damage feedback

This sequence starts on the final warning update, after a valid warning centre has been selected. The five lifelines represent SnakeBurrowComponent, SnakeBurrowVisualComponent, SnakeShieldComponent, the player's CombatStatsComponent and SnakePlayerHitVisualComponent. It assumes the components assembled by the factory are present. The frame dispatcher, position calculations and the internals of setUnderground() are omitted. That method restores vulnerability and physics, starts the exposed pose and publishes health-bar visibility before the attack is checked.

sequenceDiagram
    participant B as Burrow controller
    participant V as Burrow visuals
    participant S as Shared shield
    participant H as Player stats
    participant I as Green hit visual
    Note over B: update() dispatches WARNING
    B->>B: updateWarning()
    B->>B: setCentre(interpolated position)
    Note over B: stateTime reaches warningDuration
    B->>B: emerge()
    B->>B: enter(State.EXPOSED)
    B->>B: setCentre(warningCentre)
    B->>B: setUnderground(false)
    B->>V: hideWarning()
    B->>V: startEmergence(warningCentre)
    alt Player alive and centre inside attack radius
        B->>S: tryBlock()
        alt Shield has durability
            Note over S: durability-- and start block flash
            S-->>B: true
            Note over B: emerge() returns without damage
        else Shield inactive
            S-->>B: false
            B->>H: getHealth()
            H-->>B: previousHealth
            B->>H: takeDamage(config.burrowDamage, entity)
            Note over H: Existing mitigation and health events run here
            B->>H: isDead() / getHealth()
            opt Player survives and health decreased
                B->>I: play()
            end
        end
    else Player outside radius
        Note over B: No shield point or health is consumed
    end
Loading

The green hit effect starts only after damage is confirmed. Independently, setHealth() emits updateHealth, and PlayerDamageFlashComponent responds to a decrease while the player remains alive. A successful Snake shield block never calls takeDamage().

Sequence: gem pickup, healing and the next poison hit

This sequence spans separate updates. The first update has an active, surviving encounter, a positive time step and an unexpired gem whose appearance period has finished; the one-frame activation guard has already passed. The later poison update concerns an existing shot after its fresh and forming periods. Rendering and the internals of geometric helpers are omitted; collecting a gem does not immediately advance projectiles or synchronously start another attack.

sequenceDiagram
    participant G as Shield pickups
    participant C as SnakePoisonCollision
    participant S as Shared shield
    participant H as Player stats
    participant P as Poison volleys
    Note over G: update() finds a mature nearby gem
    G->>G: canCollect(gem)
    G->>C: wallFraction(playerCentre, gem.position, 0f, null)
    C-->>G: Greater than 1 means clear approach
    Note over G: Remove the collected Gem from gems
    G->>S: refill()
    Note over S: durability = capacity, flashRemaining = 0
    G->>G: healIfLowHealth()
    G->>H: getHealth() / getMaxHealth()
    opt Alive, below healing threshold and result exceeds health
        G->>H: setHealth(result)
        Note over H: Default example: 30 of 100 becomes 45
    end
    Note over G,P: A later update advances an incoming poison shot
    P->>P: advance(shot, delta)
    P->>C: wallFraction(shot.position, end, config.poisonRadius, bounds)
    C-->>P: wall
    P->>P: playerFraction(shot.position, end)
    P->>P: shieldFraction(shot.position, end)
    Note over P: Shield is the first unobstructed collision
    Note over P: Move shot to contact and set fading = true
    P->>S: tryBlock()
    Note over S: One point consumed, start block flash
    S-->>P: true
    Note over H,P: No damagePlayer() call
Loading

Here the later attack is poison, but emergence calls the same shield's tryBlock(). The final durability point still blocks its attack. Healing and projectile damage continue to use the player's existing CombatStatsComponent; the shield changes the decision to call damage, rather than replacing the combat system.

Testing and coverage

Coverage was measured on 4 October 2026, using a clean checkout of 16a98c009727d651c15d94195082cc8c0d2b8422 with Gradle 8.5, JDK 21 and JaCoCo 0.8.9. The full core suite passed 1,767 tests, with no failures, errors or skipped tests. The Snake package contains 124 tests across nine test classes; NPCFactoryTest adds 11 factory tests, six of which specifically exercise Snake integration.

Test strategy

Test class Tests Behaviour checked
SnakeBurrowArenaTest 8 Whole-footprint room bounds, static obstacles, unavailable physics and rejection when no safe emergence position exists.
SnakeBurrowComponentTest 35 Locked warnings, dodging, one emergence hit, underground immunity, 1.5-second exposure, half-health transition, ordered volleys, shielding and death/freeze/concealment handling.
SnakeBurrowVisualComponentTest 7 Warning position/radius, particle limits and expiry, clock handling and disposal.
SnakeGemSpawnAreaTest 7 Camera position, zoom and viewport changes, near/far sampling, room intersections and invalid geometry.
SnakePlayerHitVisualComponentTest 8 Selected frames, duration, following the player, tint isolation and cleanup.
SnakePoisonCollisionTest 9 Swept geometry, walls, room boundaries, reverse movement and disjoint collision intervals.
SnakePoisonVolleyComponentTest 22 Mouth origin, player-facing pose, shot count/order, straight flight, wall/shield hits, single damage and lifecycle cleanup.
SnakeShieldComponentTest 8 Refill, six-block capacity, shared durability, depletion, visibility and encounter cleanup.
SnakeShieldPickupComponentTest 20 Drop limits/timing, appearance, collection, refill, healing thresholds and expiry.
NPCFactoryTest 11 (6 Snake-specific) Real factory assembly, assets/render calls and connected Snake combat, pickups and shield behaviour.

These tests use a controlled game clock and mocked services/rendering where appropriate, alongside physics and factory integration checks. Test count measures executed cases; coverage measures executed production code. Neither alone proves visual quality or combat balance.

Per-file coverage

The primary scope is all 10 source files in com.csse3200.game.components.miniboss.snake, including their nested classes. Values below come from JaCoCo XML source-file counters. They measure the whole files during the full core test run, not only changed lines or SonarCloud new-code coverage. No custom JaCoCo class/source exclusions were applied.

Source file Executable lines Line coverage Branch coverage
SnakeBurrowArena.java 40 100.0% 96.2%
SnakeBurrowComponent.java 228 97.8% 81.7%
SnakeBurrowVisualComponent.java 105 99.0% 87.5%
SnakeGemSpawnArea.java 49 100.0% 85.5%
SnakePlayerHitVisualComponent.java 48 100.0% 88.5%
SnakePoisonAssets.java 10 100.0% 100.0%
SnakePoisonCollision.java 104 99.0% 89.7%
SnakePoisonVolleyComponent.java 192 99.5% 85.6%
SnakeShieldComponent.java 89 100.0% 92.9%
SnakeShieldPickupComponent.java 148 98.0% 89.8%

Aggregate for the Snake package (counts summed before calculating percentages):

Counter Covered Missed Coverage
Instructions 4,844 45 99.1%
Branches 562 84 87.0%
Lines 1,002 11 98.9%
Methods 166 1 99.4%
Classes, including nested classes 15 0 100.0%

JaCoCo counts 15 instrumented classes because the 10 source files include five nested classes. SnakeMiniBossConfig is in a different package: it has 31/31 lines covered and no branch counter. It is included in the report but excluded from the package aggregate above.

Shared integration files are reported separately; their totals include behaviour unrelated to Snake:

Shared file Lines covered / total Line coverage Branch coverage
EnemyManagerComponent.java 48/137 35.0% 37.2%
RoomAssets.java 44/44 100.0% N/A (no branches)
NPCFactory.java 136/139 97.8% 100.0%
AnimationRenderComponent.java 65/65 100.0% 96.7%

Coverage gaps and limits

  • The package still has 11 uncovered executable lines and 84 uncovered branches. Some executed lines contain conditions for which only one outcome was exercised; high line coverage therefore does not imply complete branch coverage.
  • SnakeShieldPickupComponent.setCamera() is the only wholly uncovered method in this package. Tests exercise camera geometry and the service-locator camera path, but do not execute this explicit camera-injection setter. Room integration deserves additional focused coverage.
  • Other gaps include defensive/null/invalid-input alternatives, degenerate collision or aim cases, and cached texture-return paths. The detailed JaCoCo HTML report identifies missed lines and lines with unexercised branches.
  • Rendering is exercised through mocked SpriteBatch calls and factory tests. That verifies code paths and drawing parameters, not on-screen appearance, readability or difficulty. These still need in-game checks.

Reproduce the report

From source, with JDK 21 and normal player damage/room settings:

./gradlew :core:test :core:jacocoTestReport --rerun-tasks
./gradlew formatCheck

Open source/core/build/reports/jacoco/test/html/index.html, then select com.csse3200.game.components.miniboss.snake. Raw counters are in source/core/build/reports/jacoco/test/jacocoTestReport.xml. The command reruns the full suite so an earlier filtered test run cannot be mistaken for this measurement. formatCheck is a separate formatting check, not a coverage metric.

Manual playtest checks

Check Expected result
Dodge an emergence warning; then deliberately stay inside without a shield First attack is avoided; the second deals one 5-damage strike with green impact and red flash.
Attack underground, during exposure and during spitting Underground health is unchanged; both above-ground windows allow damage.
Move between sides during a poison attack Five four-shot volleys leave the rotating mouth; emitted shots keep their direction and stop at walls.
Collect gems and block both attack types One shared bar refills, each block spends one point, and the last point still absorbs the whole attack.
Collect at 30/100 health, then collect again without taking damage Health changes to 45 once; the next pickup only refills the shield.
Move the camera and finish or leave the encounter New drops appear in safe visible space; existing gems stay fixed; encounter effects and shield clear on exit/death.

Detailed tuning and further manual checks are in snake-miniboss.md.

⚠️ **GitHub.com Fallback** ⚠️