Skip to content

Player Attack System

dhirenmalik edited this page Oct 7, 2026 · 4 revisions

Player Attack System

Introduction

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.

Controls

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.

Architecture

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
Loading

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.

KeyboardPlayerInputComponent

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.

ItemUseComponent

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.

WeaponComponent

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);

AttackBehaviour

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.

BowComponent

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.

ProjectileFactory

Constructs standard, fire, cold and grapple projectile entities with their physics, hitbox, rendering and projectile components. Their configured values are listed under Tuning.

Projectile Lifecycle

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.

Melee Attack

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
Loading

The sequence separates attack acceptance, damage timing and presentation. Changing the visible arc alone does not change the damage area.

Charged Bow and Ammunition Handling

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.

Tuning

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.

Testing

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.

Mockito Combat Regression Coverage — PR #145

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 javadoc

Run 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.

Known Limitations

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.

Relevant Development Links

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

Contribution Attribution

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.

Clone this wiki locally