Final Boss Stage 2 Remake and Stage 3 Improvements - UQcsse3200/2026-studio-4 GitHub Wiki

Overview

This page documents the Final Boss Stage 2 remake and Stage 3 tornado improvements in PR #218, linked to feature ticket #217. The implementation and coverage results refer to merge revision c18edb1fedf786260d2551373f1ea19d54733f52.

The original Final Boss page remains a record of the earlier design. Stage 2 now centres on fireball avoidance, temporary ice cover and collectible ice magic. Stage 3 keeps its ice, statue and ending structure, while its tornadoes gain coordinated pursuit and repeated contact damage.

The encounter still uses one Boss entity with 100 maximum health by default. Stage 1 hands over at 80 health; Stage 2 ends at 60 health. Stage 3's statue wave begins at 20 health. The shared three-second protected transition separates stages.

Stage 2: Fire arena and ice magic

Arena and attack cycle

At entry, the camera locks its current view to form the arena. Player movement, dashing and knockback are contained within it. The Boss roams slowly at 1.2 world units per second; this does not reduce the player's movement speed. Viewport and zoom changes are accounted for, without allowing an enlarged window to expand the original arena.

An orange transformation changes the Boss into the fire wizard. A looping fire circle remains visible throughout Stage 2, including firing pauses. Stage 2 disables the old charge behaviour, and the factory no longer attaches FinalBossStageTwoContactDamageComponent.

The Boss alternates 20 seconds of firing and a three-second pause. Each volley randomly selects a six-shot ring, four-shot fan or five-shot spread. Shots travel straight with pattern variation; they do not home towards the player.

Setting Default
Initial firing delay 0.6 seconds
Volley interval 0.3 seconds
Fireball speed / lifetime 2.4 world units per second / 6 seconds
Fireball visual size / collision radius 1.15 × 1.15 / 0.16 world units
Fireball damage 0.5 per accepted hit
Maximum active fireballs 128

Fireballs originate around the fire circle instead of the centre of the Boss sprite. The final compact emission radius is 1.55 world units for the default 2 × 2 Boss. Slight visual overlap is permitted to keep the ring close to the character. A blocked or out-of-arena origin skips that shot.

Existing fireballs continue travelling during firing pauses. Swept collision checks resolve the first wall, ice cover, player or arena-boundary collision, reducing tunnelling during fast movement or a long frame.

Temporary ice cover

Ice obstacles provide short-lived shelter from fireballs. Placement prefers positions 2.5–4.5 world units from the player, towards the Boss, then falls back to other safe arena positions. This is a placement preference, not a guarantee that every crystal appears beside the player.

Setting Default
Initial placement attempts / active limit Up to 4 / 4 obstacles
Replenishment At most 1 every 3.5 seconds
Obstacle size 0.9 × 1.8 world units
Lifetime At most 7 seconds, including appearance
Durability 4 fireball hits
Appearance / shatter effect 0.6 / 0.55 seconds

A blue appearance effect plays while the obstacle is already solid. The first three fireball hits show damage feedback; the fourth removes the collider immediately and plays ice shards. Natural expiry removes the obstacle without the shatter effect.

Normal player attacks do not damage these obstacles. Player ice projectiles are stopped by cover but do not consume its fireball durability. Crystals are encounter-owned physics objects and do not count as enemies for room completion. Placement and disposal also handle locked physics-world updates.

Blue pickups and energy reserves

Blue gems spawn independently of the ice obstacles; breaking cover does not drop a gem. One gem is attempted when active combat starts, followed by at most one every four seconds. There can be two ground gems, each lasting seven seconds. Full player reserves pause further spawn attempts.

Walking or dashing through a gem collects it. Each pickup fills one empty reserve, up to two reserves. It does not refill a partially used reserve or add a third slot.

  • One reserve displays one blue bar; two reserves display two.
  • Empty slots have no outline, and acquired reserves do not expire over time.
  • Each reserve supplies eight shots. The oldest reserve is consumed first.
  • If both slots are occupied, an existing ground gem remains until space becomes available or it expires.

The bars appear beside the player and stay within the arena view. A blue aura indicates available energy. Collection produces sparkles; an uncollected gem expires with a blue ring. Consuming the final reserve also plays an ending effect above the player, cancelled if another gem is collected.

Attacking the shielded Boss

Hold J while a reserve is available to fire homing ice projectiles. The input temporarily replaces the normal J attack while preserving movement, dash and other controls. Holding J before collecting a gem also works.

Setting Default
Shots per reserve / maximum stored shots 8 / 16
Shot interval 0.18 seconds
Speed / maximum turn rate 6 world units per second / 240 degrees per second
Lifetime / collision radius 4 seconds / 0.12 world units
Damage to Boss 0.25 per landed shot

Only an emitted shot consumes energy. Releasing J stops new shots; existing shots continue moving and homing. Walls, ice cover, arena exits and expiry remove shots. New shots require an active, living player who is not action-locked or immobilised.

The Stage 2 shield blocks ordinary damage. Landed ice shots use a dedicated authorised damage path, while the three-second handover remains protected. A health floor prevents an oversized hit from bypassing the 60-health transition to Stage 3.

Both the Boss and player retain the existing integer-health system. Fractional damage is accumulated inside the relevant controllers: four accepted ice hits produce one point of Boss damage, while two accepted 0.5 fireball hits produce one point of player damage before ordinary damage modifiers. A single half-point hit therefore need not visibly reduce health. Shielded or invulnerable hits do not turn into delayed health damage when protection ends.

At the default 100 maximum Boss health, moving from 80 to 60 requires 80 landed ice shots, equivalent to ten fully used pickups if every shot lands. Misses and interruptions increase this requirement. Ice shots do not cancel incoming fireballs in this revision.

Stage 3: Tornado improvements

Retained encounter structure

Stage 3 retains its initial ice attacks, five-statue wave and peaceful Grandpa ending. The first four defeated statues can each leave a tornado; the final statue does not create a fifth. Existing and pending tornadoes share a limit of four.

These mechanics provide the context for this work. The main Stage 3 changes in PR #218 are tornado movement, spacing, contact damage, feedback and their integration with the existing finale.

Slow pursuit and separate approach directions

A tornado has a harmless 0.6-second appearance period. Once active, it follows the visible player within the current arena at 0.65 world units per second. It uses the existing 0.55-speed wandering behaviour when the player is concealed or outside that arena.

Tornadoes receive stable diagonal approach directions at 45°, 135°, 225° and 315°, around a 1.2-unit target radius. Movement checks enforce 1.6-unit peer clearance and account for walls, statues and reserved statue routes. Near an arena edge, the group of target positions shifts inward together instead of independently collapsing into a corner.

Approach order and temporary wider waiting positions reduce bunching. Blocked tornadoes turn, wait or retreat rather than pass through obstacles. Separation in an extremely small or obstructed viewport is best-effort; the implementation does not promise an impossible arrangement.

Repeated low contact damage

Setting Default
Contact range 1.35 world units from the player's ground point
Damage per accepted tick 0.5
Continuous-contact interval 0.25 seconds
Damage timing One shared clock across all tornadoes
White hit-effect duration 0.24 seconds

Only fully appeared tornadoes can damage the player, and a static wall blocks contact through it. Overlapping several tornadoes does not multiply the tick frequency. Leaving all contact ranges resets the contact timer. A long frame produces at most one tick, avoiding a burst of catch-up damage.

Jumping does not evade this tall hazard. The tornado introduces no additional knockback, stun or freeze.

Two accepted unshielded half-point ticks accumulate into one integer point of damage. The accepted remainder persists across contact gaps and is cleared when the encounter is cleaned up. Protection and rechargeable-shield accounting are handled separately so blocked fractions do not leak into later health loss.

Each accepted unshielded tick plays a white whirlwind on the player, including the first half-point tick before a visible health change. Blocked hits do not falsely show this feedback. The effect uses the fourth row of the shared transformation sheet.

Finale and cleanup

After the final statue is defeated, tornado attacks stop, pending spawns are cancelled and remaining tornadoes dissolve using the existing finale effects. Grandpa returns near the player within the current camera view, stays peaceful and invulnerable, and continues into the existing ending dialogue.

Stage 2 clears projectiles, cover, gems, energy and effects on phase exit, death or disposal, and releases its camera/input control. In Stage 3, player defeat clears bolts and tornadoes and stops statue combat; surviving statue entities are removed during disposal.

Iteration and design rationale

The final tuning follows the implementation and playtesting discussions rather than the earliest proposal:

Observation Resulting change
Fast, crowded fire patterns were difficult to read Slower fireballs, enlarged artwork and fewer shots per pattern, with frequent volleys
Distant or infrequent cover did not provide useful shelter Player-relative placement, followed by a lower replenishment rate and a seven-second lifetime
Ice magic ended Stage 2 too quickly Eight shots per pickup and 0.25 damage per hit, encouraging continued dodging and collection
Centre-origin fireballs obscured the Boss Emission moved onto a ring, then brought closer through subsequent tuning
Ground pickups vanished abruptly Blue expiry and final-reserve ending effects
Tornadoes lacked an active threat and could bunch together Slow directional pursuit, spacing checks, shared 0.5-damage ticks and white feedback

Implementation and design patterns

FinalBossFactory.createFinalBoss() assembles the Boss using the existing entity-component framework. Stage controllers coordinate encounter logic; render components draw its state. Defaults are supplied by FinalBossStageTwoConfig and FinalBossStageThreeConfig. Room integration supplies resources and the camera.

Design choice Application Tradeoff
Component composition Phase, damage, movement, arena, stage logic and rendering are separate components on one Boss entity. Their lifecycle and required collaborators must be assembled correctly. This is not a claim that the game uses a data-only ECS.
Static factory FinalBossFactory creates and connects the components in one place. Assembly changes edit the factory. This is a static creation method, not the GoF Factory Method based on subclass overrides.
Enum-based finite-state coordination Phase progression and Stage 3 wave states make transitions, protection and completion explicit. State logic remains in controllers; the code does not use separate GoF State objects.
Observer-style events updateHealth, damageAttempted, STAGE_COMPLETED and PHASE_CHANGED connect combat and stage transitions. Event names and argument types must agree at runtime; synchronous events may change a stage during a damage call.
Owned helpers and callback injection Fire, cover, pickups, ice shots and tornadoes use dedicated helpers. Collision queries and hit callbacks keep projectile calculations separate from entity wiring. The owning component must update and clear helpers consistently. Callbacks are not, by themselves, a claim of a formal Strategy pattern.
Existing Service Locator Shared clock, physics, input and resource services are accessed through the existing game infrastructure. Dependencies need explicit test setup and are less visible than constructor parameters.

The fire, cover, pickup, player-ice and tornado controllers are ordinary helper classes, not attached components. FinalBossStageTwoIceInput is a special case: it extends InputComponent, but Stage 2 manually creates and disposes it. Projectile and pickup data objects are not independently registered enemy entities.

Class diagrams

These focused views omit unrelated collaborators and method parameters. Solid diamonds denote owned helper objects.

classDiagram
    direction TB
    class FinalBossStageTwoComponent {
        <<Component>>
        +startEncounter()
        +update()
        -updatePlayerIce()
        -hitBossWithIce()
    }
    class FinalBossStageTwoPickupController {
        <<helper>>
        update()
        consumeEnergy()
        clear()
    }
    class FinalBossStageTwoPlayerIceController {
        <<helper>>
        update()
        advance()
        clear()
    }
    class FinalBossDamageControllerComponent {
        <<Component>>
        +takeStageTwoIceDamage()
        -onDamageAttempted()
    }
    class FinalBossStageTwoVisualComponent {
        <<RenderComponent>>
        +update()
    }
    FinalBossStageTwoComponent *-- FinalBossStageTwoPickupController : owns
    FinalBossStageTwoComponent *-- FinalBossStageTwoPlayerIceController : owns
    FinalBossStageTwoPlayerIceController --> FinalBossStageTwoPickupController : consumes energy
    FinalBossStageTwoComponent --> FinalBossDamageControllerComponent : authorised impact
    FinalBossStageTwoVisualComponent ..> FinalBossStageTwoComponent : reads state
Loading

Stage 3 keeps movement and contact eligibility in the tornado helper, shared damage timing in the stage component, and drawing in the renderer. The multiplicity below describes the default four-tornado cap.

classDiagram
    direction TB
    class FinalBossStageThreeComponent {
        <<Component>>
        +update()
        -updateTornadoDamage()
        -applyTornadoDamageTick()
        -clearStatueCombat()
    }
    class FinalBossTornadoController {
        <<helper>>
        statueBroken()
        update()
        canDamagePlayer()
        clear()
    }
    class Tornado {
        <<data>>
        position
        approachSlot
        destination
        dissolving
    }
    class CombatStatsComponent {
        <<Component>>
        +takeDamage()
        +setHealth()
    }
    class FinalBossStageThreeVisualComponent {
        <<RenderComponent>>
        -updateTornadoRenderers()
        -drawTornado()
        -drawPlayerEffects()
    }
    FinalBossStageThreeComponent *-- FinalBossTornadoController : owns
    FinalBossTornadoController "1" *-- "0..4" Tornado : owns items
    FinalBossStageThreeComponent --> CombatStatsComponent : player damage
    FinalBossStageThreeVisualComponent ..> FinalBossStageThreeComponent : reads state
Loading

Sequence diagram: collecting ice and damaging the shielded Boss

This sequence assumes active Stage 2, J already held and a valid shot origin. Existing projectiles advance before a new one is emitted. The hit occurs during a later update.

sequenceDiagram
    participant S as StageTwo
    participant P as Pickups
    participant I as PlayerIce
    participant D as DamageController
    participant C as Boss CombatStats
    S->>P: update(...)
    P->>P: Collect gem into a free reserve
    S->>I: update(..., held && canAct, ..., pickups)
    I->>P: consumeEnergy(1 / iceShotsPerCharge)
    I->>I: Add new shot
    Note over S,I: Later update, shot reaches Boss
    S->>I: update(...)
    I->>I: Remove shot and add impact
    I->>S: hitBossWithIce() via callback
    S->>D: takeStageTwoIceDamage(...)
    D->>D: Accumulate 0.25 damage
    opt Whole damage available
        D->>C: takeDamage(wholeDamage, iceDamageSource)
        C->>D: damageAttempted event
        D->>C: Permit matching authorised source
        C->>C: Apply damage with health floor
        Note over S,C: updateHealth may complete Stage Two
        Note over D,C: Restore protection if still active
    end
Loading

The private iceDamageSource is a reused damage source with one-use authorisation, not the visible projectile. damageAttempted is dispatched before the normal invulnerability check. The damage controller temporarily admits that source and restores protection conditionally, because a health callback can synchronously change the stage.

Sequence diagram: shared tornado contact and white feedback

The example assumes unshielded contact and a surviving player. Half-point feedback is driven by the stage's timer; it does not require a health-change event.

sequenceDiagram
    participant S as StageThree
    participant T as TornadoController
    participant C as Player CombatStats
    participant E as Player Events
    participant V as StageThreeVisual
    S->>T: update(delta, combatActive)
    S->>T: canDamagePlayer(radius)
    T-->>S: Contact after spawn and wall checks
    Note over S,T: Shared 0.25-second contact clock
    S->>S: applyTornadoDamageTick(...)
    alt First accepted half-point
        S->>S: Store 0.5 and start hit timer
        Note over C,E: No damage call or combat event
    else Second half-point
        S->>C: takeDamage(1, bossEntity)
        C->>E: damageAttempted
        C->>C: Apply mitigation and setHealth(...)
        C->>E: updateHealth and damageTaken
        C-->>S: Return after damage handling
        S->>S: Start hit timer if health fell
    end
    V->>S: Read tornadoHitRemaining
    V->>V: Draw white effect at player
Loading

The whole-damage branch emits damageTaken only when health actually decreases. Death or encounter completion clears feedback and hazards; a lethal hit is not a new surviving-player hit animation.

Testing strategy

A fresh complete-core test run on 4 October 2026, at the pinned merge revision, passed 1,634 tests across 212 suites, with zero failures, errors or skips. The repository's formatCheck also passed.

StageTwo-named suites contain 238 tests across 16 suites; StageThree/Tornado-named suites contain 118 across eight; the two configuration suites contain 26. These are subsets of the complete run.

Tests combine deterministic timing and controller checks, real Box2D collision integration, mocked services/input, asset-pixel checks and render-command assertions.

Area Representative test evidence
Stage transitions and health floor transitionTimeDoesNotConsumeTheFirstAttackCycle; oversizedDamageStopsAtSixtyPercentAndCompletesTheStageOnlyOnce
Arena and deferred movement finishesContainmentAfterLaterPlayerMovementAndIgnoresQueuedWorkAfterExit
Fire origins and collision ordering wallsBetweenTheBossAndItsEmissionRingCannotBeSkippedDuringSpawn; touchingAWallTakesPriorityOverAPlayerAtTheSameHitFraction
Cover and physics cleanup fourthHitDuringContactDefersBodyRemovalUntilWorldUnlocks; fourthShotShattersTheCoverAndFifthShotInTheSameFrameHitsThePlayer
Pickups during dashes and expiry dashEntryBeforeExpiryWinsEvenWhenTheFrameEndsAfterExpiry; newSpawnCannotBeCollectedByTheEarlierPortionOfTheSameDash
Current ice balance and progression oneReserveFiresEightQuarterDamageShotsAndRemovesTwoBossHealth; eightiethQuarterDamageImpactReachesSixtyPercentAndStartsTheShieldedStageThreeTransition
Tornado cap, spacing and shared damage queuedAndVisibleRemnantsTogetherShouldNeverExceedFour; aQueuedThreeHitEvadeLandsClearOfEveryMovingTornado; severalOverlappingTornadoesShareOneDamageInterval
Retained statue and control behaviour jumpingOverARingConsumesContactWithoutDamageAfterLandingInsideIt; releasingStageJumpModePreservesAnotherOwnersJumpRequest

Additional tests exercise fractional damage, shields, long frames, synchronous death/disposal, phase protection, input restoration and asset slicing. Test names, suite counts and raw results are included in the coverage evidence package.

Measured coverage and scope

Coverage comes from the full-suite JaCoCo XML. Each row below measures the whole source file, including nested classes, not just changed lines. Aggregates divide summed covered counts by summed totals. The main scope is 14 active Stage 2 implementation files and four changed Stage 3 implementation files. The two configurations and nine shared integration files are reported separately. Stage 3's file-level results include retained code as well as the tornado changes.

Active Stage 2

Source file Lines covered / total Branches covered / total
FinalBossStageTwoArenaComponent.java 104/107 (97.20%) 82/118 (69.49%)
FinalBossStageTwoAssets.java 58/59 (98.31%) 18/20 (90.00%)
FinalBossStageTwoComponent.java 309/325 (95.08%) 217/288 (75.35%)
FinalBossStageTwoFireController.java 133/135 (98.52%) 100/122 (81.97%)
FinalBossStageTwoFireGeometry.java 1/1 (100.00%) N/A
FinalBossStageTwoIceController.java 188/188 (100.00%) 131/168 (77.98%)
FinalBossStageTwoIceInput.java 60/60 (100.00%) 59/60 (98.33%)
FinalBossStageTwoIceVisuals.java 72/72 (100.00%) 22/24 (91.67%)
FinalBossStageTwoPickupController.java 152/153 (99.35%) 114/134 (85.07%)
FinalBossStageTwoPickupVisuals.java 86/86 (100.00%) 40/54 (74.07%)
FinalBossStageTwoPlayerIceController.java 126/131 (96.18%) 95/124 (76.61%)
FinalBossStageTwoPlayerIceVisuals.java 48/48 (100.00%) 24/30 (80.00%)
FinalBossStageTwoProjectileTarget.java 6/6 (100.00%) 8/16 (50.00%)
FinalBossStageTwoVisualComponent.java 98/108 (90.74%) 40/60 (66.67%)

Changed Stage 3 files

Source file Lines covered / total Branches covered / total
FinalBossStageThreeAssets.java 29/30 (96.67%) 9/10 (90.00%)
FinalBossStageThreeComponent.java 840/888 (94.59%) 477/597 (79.90%)
FinalBossStageThreeVisualComponent.java 79/302 (26.16%) 18/162 (11.11%)
FinalBossTornadoController.java 279/282 (98.94%) 195/208 (93.75%)

Configuration files

Source file Lines covered / total Branches covered / total
FinalBossStageThreeConfig.java 58/62 (93.55%) 36/60 (60.00%)
FinalBossStageTwoConfig.java 116/119 (97.48%) 138/148 (93.24%)

Aggregate coverage

Scope Files Lines covered / total Branches covered / total
Active Stage 2 14 1441/1479 (97.43%) 950/1218 (78.00%)
Changed Stage 3 subset 4 1227/1502 (81.69%) 699/977 (71.55%)
Combined implementation 18 2668/2981 (89.50%) 1649/2195 (75.13%)
Implementation and configurations 20 2842/3162 (89.88%) 1823/2403 (75.86%)
Changed shared dependencies 9 685/1000 (68.50%) 331/545 (60.73%)
All changed production Java files 29 3527/4162 (84.74%) 2154/2948 (73.07%)

The 18-file implementation scope also reports these JaCoCo counters:

Counter Covered / total
Instruction 15299/17415 (87.85%)
Branch 1649/2195 (75.13%)
Line 2668/2981 (89.50%)
Method 379/407 (93.12%)
Class 38/40 (95.00%)

The nine shared files are FinalBossDamageControllerComponent, FinalBossMovementComponent, FinalBossPhaseControllerComponent, FinalBossVisualComponent, StatusEffectsControllerComponent, EnemyManagerComponent, FollowingCameraComponent, RoomAssets and FinalBossFactory. Their individual counters are included in the evidence README. The class counter includes nested/anonymous classes, so its denominator differs from the source-file count.

Reproducing the report

From the repository root, with Java 21 and the project's Gradle dependencies available:

cd source
./gradlew :core:test :core:jacocoTestReport formatCheck

Open source/core/build/reports/tests/test/index.html for tests and source/core/build/reports/jacoco/test/html/index.html for coverage, using paths relative to the repository root. Reproducing the exact figures requires the documented revision and an unmodified source/test checkout.

The supplied Final-Boss-Coverage-Evidence.zip contains the HTML/XML reports, JUnit results, raw execution data, build log, CSV/JSON counters, source revision, extraction script and editable/rendered diagrams. Its README records the exact measurement environment: Gradle 8.5, JDK 21 and JaCoCo 0.8.9.

Final-Boss-Coverage-Evidence.zip

Assets and sources

Stage 2 combines the existing wizard artwork with fire transformation, fireball, shield-circle, crystal, shatter, gem and ice-magic assets. Sprite sheets are sliced at runtime and use nearest-neighbour filtering. Room assets own shared textures; disposing a visual component does not dispose those shared textures.

Relevant rows of transform/01.png are counted from the top: row 4 supplies the white tornado-hit effect, row 5 the blue expiry/ending effect, row 6 the fire transformation and row 7 the blue cover appearance. Stage 3 explicitly declares its shared effect dependency.

Asset sources and licence notes are recorded in the repository's Stage 2 asset manifest. The assets have different terms; this page does not claim original authorship or that all artwork is CC0. The manifest records unresolved public-repository redistribution confirmation for the transformation and shatter assets.

Implementation references at the documented revision:

The implementation notes preserve intermediate steps. Values on this page are taken from the final code at the pinned revision.

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