Repository navigation
Player Attack System
The Player Attack System provides mouse-aimed bow attacks and facing-based melee combat while separating input, inventory use, weapon coordination and weapon-specific behaviour.
The original Sprint 1 bow used unlimited ammunition. Main now integrates inventory-selected standard, fire and cold arrows, while rope arrows route to the grapple system without being consumed.
This page documents the Sprint 3 implementation in PR #196, PR #211 and PR #229. Earlier revision-specific test results are retained as historical evidence.
| Input | Action |
|---|---|
E |
Use the selected item; fire a combat arrow immediately |
Release E
|
Allows another arrow to be fired |
| Left click | Melee attack in the player’s facing direction |
| Right click | Charge selected combat arrow on press; fire on release |
| 1–9 or comma/period | Select an inventory slot / previous or next item |
Only one arrow is fired per key press, preventing repeated attacks while the key is held.
Slots are not fixed arrow-type bindings. Rope arrows charge on right-click and fire on release; attachment persists after release. Potions use E and are ignored by right-click shooting. See Player Grappling and Player inventory.
Inventory selection is documented in Player inventory and Arrow Selection Wheel. The facing-based melee path is independent of selected Sword/Spear item attacks from earlier inventory revisions.
The primary-weapon interface separates weapon coordination from weapon-specific behaviour.
classDiagram
class AttackBehaviour {
<<interface>>
attack(Vector2 direction)
}
class PrimaryWeapon {
<<interface>>
isReady() boolean
getCooldownRemaining() float
}
class WeaponComponent {
setPrimaryWeapon(PrimaryWeapon weapon)
attackPrimary(Vector2 direction)
}
class BowComponent
class ProjectileFactory
class ArrowProjectileComponent
AttackBehaviour <|-- PrimaryWeapon
PrimaryWeapon <|.. BowComponent
WeaponComponent --> PrimaryWeapon : delegates when ready
BowComponent ..> ProjectileFactory : creates projectiles
ProjectileFactory ..> ArrowProjectileComponent : attaches behaviour
A new primary weapon implements PrimaryWeapon and is selected using WeaponComponent.setPrimaryWeapon(...). Its readiness check must account for states that reject attacks, including charging and cooldown.
Inventory-backed attacks should retain the item-use validation path so rejected requests do not spend ammunition. Melee uses a separate event-driven component rather than the bow’s primary-weapon coordinator.
Converts mouse coordinates into a world-space direction using the active camera. E calls ItemUseComponent.useSelectedItem() once per press; right-click routes aimed shooting, while left-click emits meleeStart without an aiming payload. Missing camera prevents aimed shooting; generic item use has a rightward fallback aim.
The input component does not know which weapon is equipped.
Routes the selected inventory item to bow, grapple or potion behaviour. A normal E arrow request selects the arrow type, emits primaryAttack, consumes one arrow and emits itemUsed. The readiness check rejects unavailable/busy primary weapons before spending ammunition.
Replaces the earlier PlayerAttackComponent. It listens for primaryAttack and delegates to its equipped PrimaryWeapon when ready. PrimaryWeapon extends AttackBehaviour with isReady() and getCooldownRemaining().
The active behaviour can be changed using:
weaponComponent.setPrimaryWeapon(primaryWeapon);Defines the common interface used by player attack types:
public interface AttackBehaviour {
void attack(Vector2 direction);
}Bow implements PrimaryWeapon. Melee is a separate component listening for meleeStart; the primary-weapon interface does not imply an implemented weapon-switching UI.
Implements PrimaryWeapon and owns bow-specific behaviour:
- Normalises the aiming direction.
- Calculates the arrow’s spawn position.
- Creates the arrow through
ProjectileFactory. - Registers the projectile with
EntityService. - Plays the attack sound.
- Triggers
attackAnimation; the charged sprite sequence separately follows charge events. - Enforces a 0.4-second cooldown shared by its E and right-click paths.
Constructs standard, fire, cold and grapple projectile entities with their physics, hitbox, rendering and projectile components. Their configured values are listed under Tuning.
ProjectileFactory constructs the projectile entity and its components. ArrowProjectileComponent manages movement, collision handling and expiry; grapple-specific contact is handled by GrappleArrowComponent.
Collision exclusions apply to both existing fixtures and the collider’s fixture definition. Player-collision filtering therefore remains correct regardless of whether the projectile or hitbox component initialises first.
Grapple arrows follow an arc and use the shared bow charge curve. Arrow sprites align with their flight direction. Rope attachment and path management remain the responsibility of the grapple components.
Enemy death and removal remain outside the projectile component. Projectile cleanup uses the entity service’s deferred removal path.
Left click emits meleeStart without an aiming payload. MeleeComponent captures the player’s facing direction when it accepts the request. Requests are rejected when the player is dead, already swinging or cooling down.
An accepted swing emits meleeSwing(int facing), where facing is 1 or -1. At the hit point in the swing, nine rays sample a 110° arc extending 1.5 world units from the player’s centre. Each ray is shortened at the nearest solid obstruction before checking enemy fixtures.
Targets are deduplicated across rays. Each eligible enemy therefore receives at most one hit per swing. Damage uses the attacker’s CombatStatsComponent, and meleeHit(Entity target) identifies each damaged target.
sequenceDiagram
participant Input as Keyboard input
participant Melee as MeleeComponent
participant Actions as PlayerActions
participant View as Animation / renderer
participant Physics as PhysicsEngine
participant Enemy as Enemy combat stats
Input->>Melee: meleeStart()
alt Dead, swinging or cooling down
Note over Melee: Reject request
else Accepted
Melee->>Actions: getFacingDirection()
Melee-->>View: meleeSwing(facing)
Note over Melee: Wait until hit delay
loop Each ray
Melee->>Physics: Find solid obstruction
Melee->>Physics: Find NPCs before obstruction
end
Note over Melee: Deduplicate targets
loop Each eligible target
Melee->>Enemy: hit(attacker)
Melee-->>View: meleeHit(target)
end
Note over Melee: Finish swing, then cooldown
end
The sequence separates attack acceptance, damage timing and presentation. Changing the visible arc alone does not change the damage area.
Right-click reserves/consumes one selected combat arrow at charge start and emits itemUsed and chargeStart. No projectile is created until release. The release direction uses the current cursor aim. Speed scales from 0.3 times base speed for a tap to 1.5 times base speed after 1.5 seconds; damage and maximum range stay unchanged. E remains immediate at base speed.
The bow is not ready while charging or during its 0.4-second post-shot cooldown. Rejected E attempts preserve ammunition, return false and emit itemUseFailed without primaryAttack or itemUsed. Missing coordinator/weapon, busy weapon and invalid right-click aim reject charge reservation before spending ammunition. Empty inventory/selection may return early without a failure event.
Repeated charge starts and E during charging cannot spend more ammo or change the reserved arrow type. Consuming the last arrow can advance inventory selection without losing the pending release. Death cancels a charge without firing; the already-reserved arrow is not refunded. Rope and potion routes retain their separate behaviour.
See Animations for the draw/hold/shoot event flow.
Creates player arrows with the following base values:
| Arrow | Direct damage | Speed (world units/second) | Maximum range |
|---|---|---|---|
| Standard | 10 | 18 | 50 |
| Fire | 12 | 18 | 50 |
| Cold | 8 | 16 | 50 |
| Grapple | None | 22 | 50 |
All use PLAYER_PROJECTILE. Range is measured from the live shooter position, with spawn fallback when no shooter is supplied. Standard/fire/cold ammunition comes from inventory; grapple arrows are not consumed.
| Scenario | Expected result |
|---|---|
| Several melee rays intersect one enemy | One hit per swing |
| Enemy stands behind a solid wall | No melee damage through the wall |
| Attack requested during a swing or cooldown | No additional swing starts |
| Arrow and hitbox initialise in either order | Collision exclusions remain applied |
| Item use rejected because the weapon is unavailable | Ammunition is preserved |
| Bow charge begins during another animation | The charge is preserved through the transition |
Relevant tests include MeleeComponentTest, MeleeRenderComponentTest, BowLifecycleTest, ArrowProjectileLifecycleTest, ItemUseFailureTest and PlayerAnimationControllerTest.
PlayerCombatIntegrationTest uses real inventory, item-use, keyboard input, weapon coordinator and bow components. Mockito supplies time, resources, sound, camera/input, event observers and entity registration. Scoped static factory mocks return mock projectiles and are closed after each test; shared services/input are reset. This tests combat routing and ammunition, not actual projectile physics or visual rendering.
Coverage includes successful shots, empty/last-arrow inventory, standard/fire/cold factory selection, cooldown expiry, arrow switching while busy, missing weapon/coordinator, one E attempt per press, shared E/right-click cooldown, charge reservation/release, death cancellation, rope non-consumption and potion/full-health behaviour.
Before the original ammo fix (fd64fdd), firing once and retrying during cooldown from a stack of three left 1 arrow instead of the expected 2. The regression passes after 26f0cec. Later charged-bow integration added protection against repeated charge starts and E during charging.
Recorded Java 21 verification at c900c8d: 503 tests, zero failures/errors/skips, with successful formatCheck and javadoc checks. This includes the grapple wrapping/climbing tests and 19 new movement/ledge tests. The count belongs to that tested submission revision, not to every later main revision; merged main 4941dba also contains cutscene changes absent from that run. Javadoc has existing documentation warnings.
./gradlew :core:test --tests '*PlayerCombatIntegrationTest' --tests '*ItemUseComponentTest' --tests '*BowComponentTest'
./gradlew test
./gradlew formatCheck
./gradlew javadocRun from source with Java 21. Dhiren reported ammunition/counts, inventory, movement, airborne grapple, collisions/damage, skeleton death, potions and pause/death working on the earlier 70545fc branch. That is user-reported evidence, not a visual test of the integrated charged bow or new climbing. For the merged player mechanics, manually check quick/long charge, slot changes and last-arrow release, E/right-click cooldown, rope wrapping/climbing and death cancellation. Exact aiming matrices, i-frame timing and animation overlaps remain unverified.
The PR #145 test-only additions were movement regressions (fee4e84) and ledge-drop integration (c900c8d). Their 19 tests complement combat coverage; see Player Movement for scope.
Enemy death and removal are owned by the enemy system rather than the attack component. Rendering and component tests do not establish every animation overlap or visual alignment; see Animations.
- Player Attack ticket #31
- Combined Player PR #57
- Initial ranged attack implementation
- Attack abstraction refactor
- Projectile/weapon update PR #126
- Merged bow/mechanics/fixes PR #145
- Sprint 2 weapon implementation
| PR | Work |
|---|---|
| #196 | Regression coverage, collision fixes, maintainability and Sonar coverage reporting |
| #211 | Charged grapple, rope physics, flight wrapping and corner climbing |
| #229 | Movement, melee and animation overhaul |
The ranged-attack implementation, automated tests, attack abstraction, and initial player-system integration were completed by Dhiren Malik (deerainn). Movement and grapple functionality were implemented by Darcy Franklin, while later integration changes to camera handling, physics, and damage events were completed by Rohan Shirkhedkar
Later contributions include Darcy's weapon/projectile refactoring and melee implementation, Tristan Sheedy's charged-bow integration, Josiah Enns's attack-animation work, and Dhiren's ammunition fix, Mockito combat regressions, conflict integration and later movement/ledge regression coverage. Inventory/item-use and elemental-item integration also include other teams' work; the player system builds on those contributions rather than claiming sole ownership.
Entities and Components
Input Handling
Arrow Selection Wheel Arrow Selection Wheel — Sprint 3