Weapon Upgrades — the shared upgrade system, each weapon's heavy attack on K, and its upgraded sprite.
The fix for the game crashing when a weapon hitbox killed an enemy (#56).
Testing is divided into automated unit tests (what the code must do) and manual gameplay tests (what the player must see and feel). Every behaviour listed in the automated plan is enforced by a test in the repository — the test class is named above each table.
Test Environment
Unit testing: JUnit 5 and Mockito, run headlessly with the engine's GameExtension
Game testing: Desktop application
Light attack:J — Heavy attack:K — Movement:WASD
Debug terminal (F1) commands:weapon knife, weapon sword, weapon bow to equip; upgrade sword|knife|bow to apply an upgrade and upgrade sword|knife|bow off to revert; debug on to show hitbox outlines
In normal play weapons are equipped from the weapons hotbar; the weapon command is the QA shortcut for the same state
Run all automated tests with:
cdsource
./gradlew :core:test
Coverage is written to source/core/build/reports/jacoco/test/html/index.html by:
cdsource
./gradlew :core:jacocoTestReport
Coverage
Last verified run: 17 Sept 2026 on task/126-bow-sprite-change — 1178 tests, 0 failures project-wide, of which 184 tests across 17 classes cover weapons.
The badges above are project-wide. The figures below are the weapons code only.
Per-file coverage
Figures are from a local JaCoCo run of this branch, which is the same measurement SonarCloud consumes. SonarCloud's own published figures lag by one analysis until this branch is merged. Each file links to its SonarCloud measures page.
Aggregate across all 18 classes (JaCoCo, source/core/build/reports/jacoco/test/html/index.html):
Counter
Covered
Missed
Coverage
Instructions
2389
46
98.1%
Branches
211
21
90.9%
Lines
543
15
97.3%
Methods
149
2
98.7%
Classes
18
0
100.0%
Every weapon class, the upgrade system, the hitbox factory and both terminal commands are at 100% line coverage. The gaps are listed under Known coverage gaps.
Tests per class
Test class
Tests
WeaponStatsComponentTest
21
WeaponComponentTest
20
KnifeWeaponComponentTest
20
SwordWeaponComponentTest
19
HitboxFactoryTest
17
WeaponUpgradeComponentTest
12
SweepComponentTest
11
BowWeaponComponentTest
10
ProjectileComponentTest
9
RotatingTextureRenderComponentTest
8
FollowComponentTest
8
UpgradeCommandTest
7
LifetimeComponentTest
7
HitboxSpecTest
5
WeaponSelectionComponentTest
4
CollisionDisposalTest
3
CollisionSpawnTest
3
Total
184
Automated Unit Test Plan
Shared Pipeline
Every weapon shares the same stats, cooldown gate, and hitbox factory, so these tests protect all three weapons at once. Damage resolves from the wielder's effective base attack, so both item buffs (Strength Charm) and ability buffs (Last Stand) scale weapon hits.
Attack while a Last Stand buff is active, then after it expires.
Damage and cooldown follow the live ability buff without permanently editing weapon or player stats.
Weapon Upgrades
An upgrade grants a weapon more light-attack damage, a heavy attack on K, a longer shared cooldown when that heavy attack is used, and a fancier sprite. Upgrade state lives on the player in WeaponUpgradeComponent, never in the weapon, so upgrading one weapon cannot buff another.
Each upgraded sprite is an unedited crop of the same CC0 steel-weapons.png sheet, drawn at the same angle as the base sprite, with grid cells recorded in core/assets/licenses/OPENGAMEART-ATTRIBUTION.md.
WeaponUpgradeComponentTest
Test
Purpose
Expected Result
shouldNotBeUpgradedInitially
Inspect a fresh component.
Nothing is upgraded and every multiplier reads 1, so unupgraded weapons behave exactly as before.
The heavy attack uses the player's current facing.
Knife
The knife's heavy attack is a three-hit flurry: two 70° slashes in opposite directions, then a finishing stab. Only the finisher knocks back, so enemies stay in reach of every hit.
KnifeWeaponComponentTest
Test
Purpose
Expected Result
shouldSpawnFollowingHitboxWithoutASweep
Attack with the knife.
One short-lived hitbox spawns, follows the wielder, and has no sweep behaviour.
The sprite shifts (e.g. handle pulled to the wielder) without moving the hitbox.
shouldCopyVisualScaleOnSetAndGet
Mutate vectors after passing them in.
State is defensively copied.
Regression: Crash When a Weapon Kills an Enemy
Box2D forbids creating or destroying physics bodies during a collision callback. These tests step a real Box2D world and confirm the fix from #56 holds: they reproduce the original crash when the fix is reverted.
CollisionDisposalTest
Test
Purpose
Expected Result
shouldNotDisposeEnemyDuringPhysicsStepThatKillsIt
A weapon sensor deals lethal damage mid-step.
The enemy is not disposed during the physics step (which would crash in DestroyFixture).
Setup for every test below: run the game, open the debug terminal (F1), enter weapon knife / weapon sword / weapon bow to equip, enter debug on to see hitbox outlines, then close the terminal (F1). Upgrade tests additionally use upgrade <weapon> and upgrade <weapon> off.
Knife
Test
Steps
Expected Result
Directional stab
Face each direction with WASD and press J.
A small hitbox and knife sprite appear just in front of the player, pointing the way they face.
Follows the wielder
Attack while walking.
The hitbox and sprite move with the player for the stab's duration.
Damage
Stab an enemy.
The enemy takes damage and dies at zero health.
No stacked hits
Hold or spam J next to an enemy.
Only one stab exists at a time; the enemy is never double-hit by overlapping hitboxes.
Heavy locked
Press K before upgrading.
The attack sound plays, but no hitbox appears.
Flurry
Run upgrade knife, then press K next to an enemy.
Two slashes sweep across the front of the player in opposite directions, then a forward stab.
Enemies stay in reach
Flurry a single enemy.
The first two slashes do not push it away; only the final stab knocks it back.
Aim is locked
Press K, then turn immediately.
All three strikes keep the direction faced when K was pressed.
No sprite flash
Watch the player through the flurry.
No knife sprite appears on top of the player between strikes.
Switching mid-combo
Press K, then change weapons on the hotbar straight away.
The remaining strikes never appear, including after switching back.
Upgraded sprite on every strike
Watch a flurry while upgraded.
Both slashes and the finisher use the upgraded knife sprite, not just the stab.
Upgraded sprite and revert
Compare the blade before and after upgrade knife, then run upgrade knife off.
The upgraded knife sprite is drawn while upgraded, and the base knife and inert K return afterwards.
Sword
Test
Steps
Expected Result
Sweeping arc
Attack and watch the debug outline.
The hitbox sweeps a 90° arc around the player, centred on the facing direction.
Blade turns with the swing
Attack and watch the sprite.
The sword sprite rotates through the arc, handle staying near the player.
Follows the wielder
Attack while walking.
The whole arc moves with the player.
Damage
Sweep through an enemy.
The enemy takes damage anywhere along the arc.
Heavy locked
Press K before upgrading.
The attack sound plays, but no hitbox appears.
Heavy spin
Run upgrade sword, then press K while surrounded.
The blade sweeps a full circle around the player and hits enemies on every side.
Readable spin
Watch the spin at normal speed.
The full turn is easy to follow, and the blade stays pointed along its arc.
Shared cooldown
Press K, then immediately J.
The light attack is blocked until the longer cooldown ends.
Upgraded sprite and revert
Compare the blade before and after upgrading, then run upgrade sword off.
The fancier winged sword is drawn while upgraded; the Sprint 1 sword and inert K return afterwards.
Bow
Test
Steps
Expected Result
Arrow flight
Face each direction and press J.
An arrow spawns at the player's edge and flies straight, sprite pointing along its flight.
Non-piercing
Fire into a group of enemies.
The arrow damages the first enemy it touches, then disappears.
Blocked by obstacles
Fire at walls, rocks, and barrels.
The arrow stops instead of passing through.
No firing through walls
Stand against a wall and fire into it.
No arrow spawns beyond the wall.
Range cap
Fire into open space.
The arrow expires after roughly 1.5 seconds of flight.
Heavy locked
Press K before upgrading.
The attack sound plays, but no arrow appears.
Three-arrow volley
Run upgrade bow, then press K.
Three arrows fly out: one straight ahead and one angled to each side.
Partly blocked volley
Heavy attack with a wall directly ahead.
The blocked arrow is skipped while the others still fly.
Upgraded sprite on every arrow
Fire J and K while upgraded.
The single shot and all three volley arrows use the upgraded arrow sprite, including the angled ones.
Revert
Run upgrade bow off.
Single arrows with the base sprite return, and K goes back to sound only.
Rendering
Test
Steps
Expected Result
Sprite vs hitbox
Attack with debug on.
The sprite and the hitbox outline are independent: the outline is sized for collision, the sprite keeps its own square aspect ratio.
Crisp pixel art
Attack in every direction and mid-sweep.
Sprites stay sharp at every angle (rotation happens at draw time, not by resampling the image).
One sprite at a time
Spam attacks.
Old sprites disappear with their hitboxes; sprites never pile up.
Upgraded sprites line up
Upgrade each weapon, then attack in every direction.
Upgraded sprites sit and rotate exactly like the base ones, since each is cut from the same sheet at the same angle.
Abilities
Activate Invisibility, then attack with each weapon.
Weapon sprites, including every flurry strike and volley arrow, fade with the player.
Weapon Selection
Test
Steps
Expected Result
Hotbar and terminal agree
Switch with the hotbar, then with weapon <name>.
Both change the same equipped weapon; only that weapon attacks.
Upgrades are per weapon
Upgrade one weapon, then switch to another.
Each weapon keeps its own upgrade state and sprite; switching never grants or removes an upgrade.
Cooldown is shared
Attack, then switch weapons immediately and attack again.
The shared cooldown still applies, so switching cannot be used to attack faster.
Stability
Test
Steps
Expected Result
Kill on contact
Kill many enemies with each weapon, light and heavy, with and without debug on.
No Box2D crash when the killing blow lands.
Split enemies
Hit a splitting enemy with a weapon.
It splits into two children without crashing, and the children die normally.
Known Coverage Gaps
Fifteen lines are uncovered, all of them defensive fallbacks or one untested attack axis:
Location
Uncovered
Why
LifetimeComponent.removeNow()
The whole method, plus the expired early return
The fallback used when no EntityService is registered. Every test and the game itself register one, so only the normal runAfterUpdate path is exercised. This is the largest gap in the feature.
ProjectileComponent.update() / onCollisionStart()
Null-clock and paused-frame guards, and the "not my fixture" / "not a stopping layer" early returns
Guards against a missing clock, a paused game and collisions reported for other fixtures. The flight and wall-stop behaviour they protect is covered directly.
SwordWeaponComponent.spawnSweep()
The vertical branch of the axis flip
shouldSizeAndTransposeHitboxWithTheAttackAxis only asserts a horizontal swing, so the transposed blade and reach for an up or down swing are never measured. Worth closing — the knife covers the same branch in both axes.
WeaponComponent.onWeaponAttack()
The !enabled early return for the light attack
The heavy attack's equivalent guard is covered; the light one is only exercised through weapon selection rather than by a direct test.
HitboxSpec.visualScale() / visualOffset()
The null branches of both setters
Clearing a visual override back to null never happens in production; the non-null paths are covered.
WeaponAssetsComponent.dispose()
The null ResourceService branch
Guards disposal after the service is torn down.
WeaponCommand.action()
Two branches of the equip guard
The missing-player and missing-selection paths; WeaponSelectionComponentTest covers equipping itself.
Areas Not Covered by Automated Tests
Rendering tests verify the arguments passed to the sprite batch, not actual pixels. Visual correctness (anchoring, crispness, whether the sword swing "reads" as a swing, whether an upgraded weapon looks like an upgrade) is tested manually.
Game feel and tuning values (lifetimes, arc widths, spin and flurry timings, arrow speed and spread, damage multipliers, cooldown lengths) are judged manually, not asserted.
Hitboxes stay axis-aligned while their sprites rotate, a pre-existing engine limitation. During the sword's spin, reach at the player's sides is slightly shorter than straight ahead. This is accepted, not tested.
Heavy attacks have no dedicated player animation yet; that belongs to the upgrade animations ticket.
How upgrades are granted in normal play (pickup or shop) is not built yet. Anything granting one calls WeaponUpgradeComponent.setUpgraded(...), the same entry point the upgrade terminal command uses.
The debug terminal weapon and upgrade commands are exercised manually every session; UpgradeCommandTest covers the command logic, but the terminal parsing around it is manual.
AI Declaration
This page was written with LLM assistance (Cursor on Fable 5, later updated with Claude Code on Opus 5), generated from the weapons test suite and the team's pull requests, and reviewed before publishing.