Repository navigation
Player Grappling
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.
The Grapple System lets the player fire a hook at terrain and swing from it as a pendulum. It reuses the projectile pipeline built for the bow, so the grapple is a physical arrow that travels and lands rather than an instant raycast.
Swinging is driven by Box2D physics rather than scripted motion, so momentum carries naturally into jumps and releases.
| Input | Action |
|---|---|
| Inventory selection controls | Select a rope arrow |
| Hold right click | Charge the grapple and display its trajectory preview |
| Release right click | Fire the grapple |
| Press and release right click while attached | Let go of the rope |
| Space while attached | Release with an upward boost |
| A/D while attached | Pump the swing |
| W/S while attached | Climb or descend |
Rope arrows are not consumed. Once attached, the player does not need to hold right click to remain connected. A charge cannot start while attached, so letting go of right click on the next click drops the rope instead of firing. The same release also cancels a rope that is still in flight.
ItemUseComponent routes the selected rope arrow into the grapple charge and release path. The bow and grapple share BowCharge, keeping their charge response consistent.
GrappleArrowComponent handles projectile contact and attachment. GrappleComponent owns the rope path, attachment state, swing and climbing behaviour. Rendering components display the aiming arc, travelling rope and attached path.
The projectile and rope have separate responsibilities: terrain contact identifies an attachment, while the grapple component manages the player constraint and wrapped path.
flowchart LR
Input["Input / ItemUseComponent"] -->|charge and release| Grapple["GrappleComponent"]
Grapple --> Charge["BowCharge"]
Grapple --> Factory["ProjectileFactory"]
Factory --> Arrow["GrappleArrowComponent"]
Arrow -->|contact and attachment| Grapple
Grapple --> Rope["Rope constraint and wrapped path"]
Grapple --> Renderer["GrappleRenderComponent"]
Pressing right click with a rope arrow selected sends grappleDrawStart, and letting go sends grappleDrawRelease. These are separate from the bow's chargeStart and chargeRelease so a rope draw can never fire the bow, or the other way round. GrappleComponent answers an accepted draw with grappleChargeStart and grappleChargeFire, which PlayerAnimationController plays as the draw and release animations. A press that is ignored (still on cooldown, or already attached) sends neither, so no animation plays for it.
Launch speed comes from the same BowCharge curve as the bow. The multiplier rises in a straight line from 0.3 for a tap to 1.5 after 1.5 seconds held, so with the base speed of 22 the arrow leaves at about 6.6 to 33 units per second. The range stays at 50.
Like every other arrow, the grapple arrow arcs under gravity (ARC_GRAVITY_SCALE, 0.4 of world gravity) instead of flying straight.
While a rope arrow is charging, GrappleIndicatorRenderComponent draws a dotted white arc with a larger dot where it lands. It simulates the launch point, gravity and current charge speed that the real arrow would get if released at that moment, so the arc stretches as the charge builds. It stops at the first solid surface or at the maximum range. It disappears as soon as the arrow is fired or the charge is cancelled. The shared arc simulation lives in ArcIndicatorRenderComponent.
The arrow turns about its centre as it flies, so its hitbox stays under the drawn sprite. When it hits terrain, GrappleArrowComponent anchors the rope to the nearest point on the surface it hit, not to wherever the arrow's centre happened to be, so the rope ends on the wall rather than in the air beside it. A hit within 1.2 units of the player is ignored.
- Holding right click starts an accepted charge and its draw animation.
- The trajectory preview displays the expected flight arc.
- Releasing fires the arrow with charge-dependent speed.
- The rope follows the arrow during flight and records bends around terrain.
- A valid terrain contact attaches the rope and preserves the wrapped path.
- Attachment clears the firing cooldown.
- Jumping releases the rope with an upward boost.
- Respawning clears charging, the flying arrow and the attached rope.
A request rejected by cooldown does not start the draw animation. Attachment persists independently of whether right click remains held.
The rope is a Box2D RopeJoint, which only caps how far the player can get from the anchor. It goes slack when the player is closer, so an anchor below the player does not force movement around a fixed-radius arc. An earlier version used a DistanceJoint, which behaves like a rigid rod and swung the player round the anchor even when it was underneath them. collideConnected is true so the player still collides with the surface they grappled.
Swing force is applied along the arc, perpendicular to the line from the anchor to the player. The direction is flipped when needed so the left key always pushes left and the right key always pushes right, including when the player is above the anchor, where a fixed quarter turn would push the wrong way:
Vector2 tangent = new Vector2(-r.y, r.x).nor();
boolean pointsTheWrongWay =
Math.abs(tangent.x) > MIN_TANGENT_X ? tangent.x * direction < 0 : direction < 0;
if (pointsTheWrongWay) {
tangent.scl(-1f);
}When the player is almost level with the anchor the arc is nearly vertical and tangent.x is too small to pick a side, so it falls back to a fixed convention (MIN_TANGENT_X). Movement remains physics-driven so momentum carries into release.
While attached, PlayerActions redirects walking input into the swing rather than calling updateSpeed(). Without this, the impulse that snaps horizontal velocity to walking speed would cancel the pendulum every frame. The same guard applies in stopWalking(), sprint() and stopSprinting().
Air control is reduced to 10% of ground control so a fast landing from a swing isn't immediately wiped when a movement key is pressed.
Jumping while attached releases the rope and adds an upward impulse scaled by ROPE_JUMP_MULTIPLIER on top of existing velocity, so swinging fast and jumping at the bottom of the arc launches the player properly.
The rope tracks bends during projectile flight and preserves them when attachment completes. Its path remains consistent across the transition from a flying arrow to an attached rope.
Corner-climbing targets are tracked separately from rope bends. This allows the player to negotiate terrain corners without replacing the contact geometry that describes the wrapped rope.
When extending climbing behaviour, preserve the distinction between the player’s movement target and the rope’s persistent contact path.
| Constant | Class | Value | Purpose |
|---|---|---|---|
SWING_FORCE |
GrappleComponent |
7 | Tangent force multiplier, scaled by player mass |
MAX_SWING_SPEED |
GrappleComponent |
7 | Cap on pumped speed |
SWING_DAMPING |
GrappleComponent |
0.5 | How fast the swing decays |
| Release damping | GrappleComponent |
Saved pre-grapple value | Restore drag after letting go |
GRAPPLE_COOLDOWN |
GrappleComponent |
2s | Firing cooldown; cleared on attachment |
ROPE_JUMP_MULTIPLIER |
PlayerActions |
0.7 | Upward kick when jumping off |
AIR_CONTROL |
PlayerActions |
0.1 | Steering while airborne |
GRAPPLE_ARROW_SPEED |
ProjectileFactory |
22 | Base speed before charge scaling |
GRAPPLE_ARROW_RANGE |
ProjectileFactory |
50 | Distance from live shooter position |
MIN_SPEED_FACTOR |
BowCharge |
0.3 | Speed multiplier for a tap |
MAX_SPEED_FACTOR |
BowCharge |
1.5 | Speed multiplier at full charge |
MAX_CHARGE_SECONDS |
BowCharge |
1.5 | Hold time for a full charge |
ARC_GRAVITY_SCALE |
ArrowProjectileComponent |
0.4 | Share of world gravity acting on every arrow |
SPAWN_OFFSET |
GrappleComponent |
0.6 | How far in front of the player, in player widths, the arrow appears |
MIN_TANGENT_X |
GrappleComponent |
0.05 | Below this the swing side comes from a fixed convention |
MIN_TRAVEL |
GrappleArrowComponent |
1.2 | Hits closer to the player than this do not attach |
Automated coverage includes:
- Charge acceptance, firing and cooldown behaviour.
- Shared bow and grapple charge-curve behaviour.
- Slack rope and swing direction.
- Wall-face attachment from different approach angles.
- Flight-time wrapping and bend preservation.
- Climbing around terrain corners.
- Respawn cleanup.
- Rope, preview and arrow rendering.
- The preview only drawing while a rope arrow is charging, and its launch speed following the charge.
- The 16-contact path limit.
GrappleArrowAnchorIntegrationTest uses real arrows, real Box2D bodies and a real wall, and checks the rope ends on the wall face for arrows fired from several angles.
Relevant classes include BowChargeTest, GrappleComponentFiringTest, GrappleComponentRopeTest, GrappleArrowAnchorIntegrationTest, GrappleIndicatorRenderComponentTest, GrappleFlightRopeTest, GrappleCornerClimbTest and GrappleContactLimitTest.
| Manual check | Expected result |
|---|---|
| Short and long charges | Different projectile speeds consistent with the preview |
| Fire at a wall from a shallow and a steep angle | The rope ends on the wall surface, not in the air beside it |
| Press and release right click while attached | The rope drops and no new arrow is fired |
| Press right click again while the grapple is on cooldown | No draw animation plays |
| Fall towards an anchor below the player | Rope allows slack until its length constrains movement |
| Fire past terrain corners | Rope wraps during flight and retains bends on attachment |
| Climb around a corner | Player progresses around terrain without losing the wrapped path |
| Respawn while charging, during flight or while attached | No charge, projectile or rope remains active |
Wrapping is constrained by supported terrain geometry and the contact limit. Physics and rendering tests cover specific layouts; unusual terrain arrangements and combinations of movement, collision and climbing still require level playtesting.
An arrow that hits terrain closer than 1.2 units to the player does not attach, so a point-blank shot at a wall right next to the player can miss.
The aim preview is an approximation. It steps its own simulation, and Box2D integrates the real arrow a little differently, so on long shots the arrow can land slightly below the landing dot.
| 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 grapple system, swing physics, rope rendering, camera aiming, arrow switching and associated tests were implemented by Darcy Franklin (DarcyMF). The projectile pipeline the grapple arrow reuses was built by Dhiren Malik (deerainn) as part of the ranged attack system.
That original credit includes historical arrow cycling; current selection integrates the inventory system. Later work includes Rohan Shirkhedkar's hold/SOLID-layer integration and new terrain wrapping, bend persistence and W/S climbing; Tristan Sheedy's damping restoration; and Jared Smith's moving-platform integration. New grapple geometry and climb tests accompany Rohan's merged commits.
Dhiren Malik added the player-action/grapple interaction regressions and merged-input/ledge coverage; these complement the grapple component and geometry tests without reassigning their authorship.
Sprint 3 work includes Darcy Franklin’s charged firing, trajectory preview, slack-rope behaviour and wall attachment; Rohan Shirkhedkar’s flight-rope tracking, preserved bends and corner climbing; and Dhiren Malik’s lifecycle, contact-limit and rendering regressions.
Entities and Components
Input Handling
Arrow Selection Wheel Arrow Selection Wheel — Sprint 3