Skip to content

Upgraded Weapons

Faraj edited this page Sep 17, 2026 · 3 revisions

Overview

This page covers the Weapon Upgrades feature: how a weapon becomes upgraded, what each upgrade grants, and how the work is tested.

  • The shared upgrade system — per-weapon upgrade stats and the heavy attack pipeline (Weapons, Weapon Upgrades).
  • The upgraded Sword: a 360° heavy spin on K, stronger light sweep, and a fancier blade sprite.
  • The upgraded Knife: a three-hit heavy flurry on K, and a fancier knife sprite.
  • The upgraded Bow: a three-arrow heavy volley on K, and a fancier arrow sprite.
  • The QA terminal command upgrade sword|knife|bow [off].

An upgrade never edits the player's shared weapon stats, so upgrading one weapon can never buff another. Every weapon also swaps to a fancier sprite while upgraded, so an upgrade is recognisable at a glance.

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.

Design Summary

An upgrade gives a weapon four things: more light-attack damage, a heavy attack on K, a longer shared cooldown when that heavy attack is used, and an upgraded sprite. Values live in WeaponUpgradeComponent on the player, keyed by weapon.

Weapon Light damage Heavy damage Heavy cooldown
Sword ×1.2 ×1.35 ×2 (1.0 s)
Knife ×1.2 ×0.6 per slash, ×1.2 for the finisher ×3 (1.5 s)
Bow ×1.2 — (3 arrows at ×1 each) ×2 (heavy attack duration is instant)

Every upgraded sprite is an unedited crop of the same CC0 steel-weapons.png sheet, drawn at the same angle as the base sprite, so swapping one needs no rendering changes. Grid cells are recorded in core/assets/licenses/OPENGAMEART-ATTRIBUTION.md.

Weapon Base sprite Upgraded sprite
Sword sword.png sword_upgraded.png — winged golden guard
Knife knife.png knife_upgraded.png
Bow throwing_knife.png throwing_knife_upgraded.png — winged blade with a blue gem

Upgraded Sword

Property Light (J) Heavy (K)
Arc 90°, centred on facing 360° around the player
Duration 0.25 s 0.8 s
Blade hitbox 1.0 × 0.4 1.0 × 0.4 (unchanged)
Damage at base attack 10 12 14
Sprite Upgraded winged sword while upgraded Same

Upgraded Knife

The heavy attack is a three-hit flurry, aimed where the player faced when K was pressed.

Strike Spawns at Duration Damage at base attack 10 Knockback
Slash 1 (right → left, 70°) 0.00 s 0.12 s 6 none
Slash 2 (left → right, 70°) 0.15 s 0.12 s 6 none
Finishing stab 0.30 s 0.15 s 12 1.5× the weapon's knockback

Only the finisher knocks back, so enemies stay in reach of all three hits. Switching weapons mid-flurry cancels the strikes still queued. While upgraded, the stab and every flurry strike are drawn with the upgraded knife sprite.

Upgraded Bow

The heavy attack fires three arrows at once instead of a stronger single one — the upgrade's power comes from arrow count, not per-arrow damage.

Property Light (J) Heavy (K)
Arrow count 1 3 (centre + ±15°)
Damage at base attack 10 12 10 per arrow
Each arrow's path Checked independently; a blocked shot despawns silently Same — a wall blocking the centre arrow does not stop the two side arrows
Sprite Upgraded arrow while upgraded Same, on all three arrows

Unlike the sword and knife, the bow's heavy attack has no separate hitbox shape or lifetime from the light shot — it is the same arrow, fired three times in a spread, each with its own wall check.

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|sword|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

Run all automated tests with:

cd source
./gradlew :core:test

Last verified run: 17 Sept 2026 on task/126-bow-sprite-change — 1178 tests, 0 failures.

Automated Unit Test Plan

Shared Upgrade System

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.
shouldApplySwordUpgradeStats / shouldApplyKnifeUpgradeStats / shouldApplyBowUpgradeStats Upgrade each weapon. The documented multipliers apply (sword 1.2 / 1.35 / 2, knife 1.2 / 0.6 / 3, bow 1.2 / 1 / 2).
shouldStayUpgradedWhenAppliedTwice Apply the same upgrade twice. Applying is idempotent — no stacking.
shouldRevertUpgrade / shouldRevertBowUpgrade Turn an upgrade off. The weapon returns to its unupgraded multipliers.
shouldNotAffectOtherWeapons Upgrade the sword, then read the knife and bow. Other weapons are untouched — the shared weapon stats are never modified.
shouldRejectWeaponWithoutUpgradeStats Upgrade a weapon with no upgrade defined (an explicit stats map with only the sword). Rejected rather than silently doing nothing.
shouldTriggerEventOnlyWhenUpgradeStateChanges Apply, re-apply, then revert. weaponUpgraded fires only on real changes — a hook for upgrade VFX.
shouldRejectNullStatsMap / shouldRejectNegativeUpgradeStats Construct with invalid values. IllegalArgumentException is thrown.

WeaponComponentTest

Test Purpose Expected Result
shouldHeavyAttackOnEventWhenUpgraded Fire the weaponHeavyAttack event on an upgraded weapon. The heavy attack runs, and the light attack does not.
shouldIgnoreHeavyAttackEventWhenNotUpgraded / shouldNotHeavyAttackWithoutUpgradeComponent / shouldNotHeavyAttackWithoutAHeavyAttack Fire the event in each unsupported case. Nothing spawns and no cooldown starts, so K is inert until a weapon is upgraded.
shouldIgnoreHeavyAttackEventWhenDisabled Fire the event on an unequipped weapon. Only the equipped weapon responds.
shouldTriggerScaledCooldownAndRespectItForHeavyAttacks Heavy attack twice in a row. The cooldown is multiplied by the upgrade value and blocks the second attempt.
shouldScaleLightAndHeavyDamageByUpgradeOnlyWhenUpgraded / shouldApplyExtraScaleOnTopOfHeavyUpgradeDamage Resolve damage before and after upgrading. Light and heavy damage each use their own multiplier, and combo finishers can scale further on top.
shouldRejectNullOriginOrDirectionForHeavyAttack Heavy attack with null arguments. IllegalArgumentException is thrown.

WeaponStatsComponentTest

Test Purpose Expected Result
shouldApplyExtraDamageScaleOnTopOfMultiplier Resolve damage with an upgrade scale. Damage is round(baseAttack × multiplier × scale), and the shared multiplier is left untouched.

UpgradeCommandTest

Test Purpose Expected Result
shouldUpgradeSword / shouldUpgradeAndRevertKnife / shouldUpgradeAndRevertBow Run upgrade <weapon> [off]. QA can apply and revert any weapon's upgrade without a pickup or shop.
shouldStayUpgradedWhenRunTwice Run the command twice. Idempotent.
shouldRevertSwordUpgradeWithOff Run upgrade sword off. The weapon returns to its Sprint 1 behaviour.
shouldRejectInvalidArguments / shouldFailWhenPlayerHasNoUpgradeComponent Run with bad arguments, or on a player with no upgrade component. The command reports failure instead of changing state.

KeyboardPlayerInputComponentTest and PlayerActionsTest

Test Purpose Expected Result
shouldTriggerHeavyAttackNotSpecialAttackOnK Press K. A dedicated heavyAttack event fires; specialAttack stays free for the abilities feature.
shouldTriggerHeavyWeaponAttackInFacingDirectionOnHeavyAttack Walk, stop, then heavy attack. The heavy attack uses the player's current facing.

Upgraded Sword

SwordWeaponComponentTest

Test Purpose Expected Result
shouldKeepSprintOneLightAttackWhenNotUpgraded / shouldRevertToSprintOneSwordWhenUpgradeRemoved Attack before upgrading, and after reverting. The unupgraded sword is unchanged in size, arc and damage.
shouldNotHeavyAttackWhenNotUpgraded Press the heavy attack before upgrading. Nothing spawns and no cooldown starts.
shouldBoostLightDamageButKeepItsSweepWhenUpgraded Light attack while upgraded. Same 90° arc and blade, more damage.
shouldHeavyAttackWithTheSameBladeWhenUpgraded Heavy attack while upgraded. The spin uses the same blade hitbox, swept by a SweepComponent.
shouldSpinTheHeavyAttackAFullTurnAroundTheWielder Step the spin from start to finish. It opens behind the player, passes through the facing direction halfway, and completes a full 360°.
shouldSpinTheHeavyAttackSlowerThanTheLightSweep Compare both attacks over the same time. The light sweep finishes while the spin is still going, so the full turn is readable.
shouldDealHeavyUpgradeDamage Heavy attack while upgraded. Damage uses the heavy multiplier, not the light one.
shouldLengthenTheSharedCooldownAfterAHeavyAttack Heavy attack, then light attack. The cooldown is doubled and shared, so the spin cannot be spammed.
shouldKeepHeavyCooldownAtLeastAsLongAsTheSpinWithAttackSpeedBuffs Heavy attack with a 4× attack-speed buff. The cooldown never drops below the spin's own length, so two spins can never overlap.
shouldUseTheBaseSpriteWhenNotUpgraded / shouldUseTheUpgradedSpriteForBothAttacksOnceUpgraded Check the sprite in each state. The fancier blade is drawn for both attacks while upgraded, and the base sprite returns when reverted.

SweepComponentTest

Test Purpose Expected Result
updateMovesHitboxToTheNewOffsetInTheSameFrame Step a sweep one frame. The blade's position updates in the same frame as its rotation, so fast sweeps never look tilted.

Upgraded Knife

KnifeWeaponComponentTest

Test Purpose Expected Result
shouldNotFlurryWhenNotUpgraded Press the heavy attack before upgrading. Nothing spawns and no cooldown starts.
shouldBoostTheLightStabWhenUpgraded Stab while upgraded. Same stab hitbox, more damage, normal knockback.
shouldOpenTheFlurryWithAKnockbackFreeSlashRightToLeft Start a flurry. One slash spawns immediately, sweeping 70° across the front of the player with no knockback.
shouldFollowWithAReverseSlashThenAFinishingStab Step through the whole flurry. A reverse slash follows, then a stab with double damage and the only knockback — and nothing spawns afterwards.
shouldKeepEveryStrikeAimedWhereTheFlurryStarted Start a flurry facing up. All three strikes stay aimed where the flurry began.
shouldSpawnQueuedStrikesInFrontOfTheWielderNotOnThem Inspect queued strikes as they spawn. Each appears in front of the player, never flashing on top of them for a frame.
shouldFadeEveryFlurryStrikeWithThePlayerWhileInvisible Activate Invisibility mid-flurry. Every strike fades with the player, matching the light attack's behaviour.
shouldCancelTheRestOfTheFlurryWhenTheKnifeIsUnequipped Switch weapons mid-flurry. Queued strikes are dropped, not fired later when the knife is re-equipped.
shouldTripleTheCooldownAfterAFlurry Use the flurry. The shared cooldown is tripled.
shouldNeverCoolDownBeforeTheFlurryEndsWithAttackSpeedBuffs Use the flurry with a 4× attack-speed buff. The cooldown never ends before the combo does, so two flurries cannot overlap.
shouldUseTheBaseSpriteWhenNotUpgraded / shouldUseTheUpgradedSpriteForEveryStrikeOnceUpgraded Check the sprite in each state. The upgraded knife is drawn for the stab and every flurry strike, and the base sprite returns when reverted.

Upgraded Bow

BowWeaponComponentTest

Test Purpose Expected Result
shouldBoostLightDamageWhenUpgraded Attack (J) while upgraded. Still one arrow, now dealing 20% more damage.
shouldNotHeavyAttackWhenNotUpgraded Press the heavy attack before upgrading. Nothing spawns and no cooldown starts.
shouldFireThreeArrowsOnHeavyAttackWhenUpgraded Heavy attack (K) while upgraded. Three arrows spawn — centre plus ±15° — each dealing the same damage as an unupgraded shot.
shouldFireOnlySideArrowsWhenCentreSpawnBlockedOnHeavyAttack Heavy attack next to a wall blocking only the centre path. The two side arrows still fire independently.
shouldRevertToSprintOneBowWhenUpgradeRemoved Revert the upgrade, then attack and heavy attack. Light attack returns to base damage; heavy attack is inert again.
shouldUseTheBaseSpriteWhenNotUpgraded / shouldUseTheUpgradedSpriteForEveryArrowOnceUpgraded Check the sprite in each state. The upgraded arrow is drawn for the single shot and for all three volley arrows, and the base sprite returns when reverted.

Shared Hitbox Pipeline

HitboxFactoryTest

Test Purpose Expected Result
shouldStartOnTheOwnerOffsetRatherThanTheSpawnOrigin Create a hitbox that follows its wielder. It starts on its offset before any update runs, so delayed strikes never render in the wrong place.

Manual Gameplay Test Plan

Setup for every test below: run the game, open the debug terminal (F1), enter weapon sword, weapon knife or weapon bow to equip, enter debug on to see hitbox outlines, then close the terminal (F1).

Unupgraded Behaviour

Test Steps Expected Result
Sprint 1 sword Start a run and attack with J without upgrading. The original 90° sweep, blade and damage.
Sprint 1 knife Equip the knife and attack with J. The original stab.
Sprint 1 bow Equip the bow and attack with J. A single arrow with the original arrow sprite.
Heavy attack locked Press K with each weapon. The attack sound plays, but nothing spawns.

Upgraded Sword

Test Steps Expected Result
Apply the upgrade Run upgrade sword. The blade changes to the fancier winged sword on the next attack.
Stronger light attack Attack an enemy with J before and after upgrading. The same 90° sweep kills in fewer hits.
Heavy spin Press K surrounded by enemies. 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 rather than looking tilted.
Shared cooldown Press K, then immediately J. The light attack is blocked until the longer cooldown ends.
Revert Run upgrade sword off and attack. The Sprint 1 sword and sprite return, and K is inert again.

Upgraded Knife

Test Steps Expected Result
Apply the upgrade Run weapon knife, then upgrade knife. J still stabs, now for more damage, drawn with the upgraded knife sprite.
Flurry 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 you faced when you pressed K.
No sprite flash Watch the player while the flurry plays. No knife sprite appears on top of the player between strikes.
Upgraded sprite on every strike Watch the flurry closely. Both slashes and the finisher use the upgraded knife sprite, not just the stab.
Switching mid-combo Press K, then change weapons on the hotbar straight away. The remaining strikes never appear, including after switching back.
Revert Run upgrade knife off. The base knife sprite returns and K goes back to sound only.

Upgraded Bow

Setup: equip with weapon bow, apply with upgrade bow.

Test Steps Expected Result
Apply the upgrade Run upgrade bow. J still fires a single arrow, now for more damage and drawn with the upgraded arrow sprite.
Heavy shot Press K facing an enemy. Three arrows fan out and can hit multiple enemies at once.
Upgraded sprite on every arrow Watch a volley. All three arrows use the upgraded sprite, including the two angled ones.
Blocked centre still fires sides Stand next to a wall directly ahead, press K. The straight shot is blocked; the two angled arrows still fire.
Revert Run upgrade bow off. J returns to a single, unboosted arrow with the base sprite; K is inert again.

Interaction With Other Features

Test Steps Expected Result
Hotbar Switch weapons with the hotbar, upgraded and not. Each weapon keeps its own upgrade state; only the equipped weapon attacks.
Player abilities Activate Invisibility, then attack with each weapon. Weapon sprites, including every flurry strike and volley arrow, fade with the player.
Stat buffs Pick up a Strength Charm, then attack. Upgraded damage scales on top of the buffed base attack.

Areas Not Covered by Automated Tests

  • Game feel and balance (spin and flurry timings, arrow spread, damage multipliers, cooldown lengths) is judged manually, not asserted. The values are constants and are expected to change with playtesting.
  • Sprite appearance is tested by the values passed to the sprite batch, not by pixels. Whether an upgraded weapon looks like an upgrade is a manual judgement.
  • 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.
  • The heavy attack has no dedicated player animation; 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 terminal command uses.

AI Declaration

This page was written with LLM assistance (Claude Code on Opus 5), generated from the weapon upgrade test suite and the team's pull requests, and reviewed before publishing.

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