Repository navigation
Weapon Knockback
When the player hits an enemy with a weapon, the enemy should get pushed back so the player doesn't take damage while attacking. This page explains the fix for #302, merged in PR #410. (It was first merged as #375, reverted in #406 because it went in without review, then re-applied in #410.)
Test plan: Weapon Knockback Test Plan
Hitting an enemy didn't stop it from walking into you. The weird part is that knockback was already being applied, it just got undone straight away:
- The weapon hitbox touches the enemy and
TouchAttackComponent.attack()gives the enemy's body an impulse (the knockback). - Next frame, the enemy's
PhysicsMovementComponent.update()steers it back towards the player. It does this with an impulse of(desiredVelocity - currentVelocity) * mass, which basically resets the body to its chase speed. - So the knockback lasts one frame and the enemy just keeps coming.
There was also a smaller bug with the sword. The push direction was worked out from the centre of the hitbox instead of from the player. If an enemy was standing between you and the middle of the sword swing, it got pushed towards you.
| File | Change |
|---|---|
| PhysicsMovementComponent | Added stagger(float duration) and isStaggered(). While an enemy is staggered, update() doesn't steer it or hold it still, so the push can actually move it. The stagger is checked before the immobilise (freeze) check, see Medusa below. If it gets staggered again with a shorter time, it keeps the longer one. A negative duration throws IllegalArgumentException. |
| TouchAttackComponent | After the knockback impulse, it staggers the target (if it has a PhysicsMovementComponent) for KNOCKBACK_STAGGER. Also added setKnockbackSource(Entity) so the push can come from a different entity than the hitbox. The knockback code is now in its own method, applyKnockback(Entity), which also keeps Sonar happy about method complexity. |
| HitboxFactory | Sets the hitbox's owner (the player) as the knockback source. |
- Enemies that set their velocity directly in an AI task (like dash or stampede attacks) still get pushed, but they aren't staggered, so the task can override the push.
- Bosses/minibosses with their own movement components don't get the stagger.
- A frozen enemy that gets hit with knockback slides for the stagger time before the freeze holds it again. This is on purpose, so hits on frozen enemies still feel like hits.
- Hits with knockback
0(like the knife slashes) don't stagger anything. That's on purpose.
Claude (Anthropic, Claude Code) was used to help find the cause of the bug and draft the code, tests and this page. I reviewed and tested everything before publishing.
When the player hits an enemy with a weapon, the enemy should get pushed back so the player doesn't take damage while attacking. This page explains the fix for [[#302](https://github.com/UQcsse3200/2026-studio-4/issues/302)](https://github.com/UQcsse3200/2026-studio-4/issues/302), merged in [[PR #410](https://github.com/UQcsse3200/2026-studio-4/pull/410)](https://github.com/UQcsse3200/2026-studio-4/pull/410). (It was first merged as [[#375](https://github.com/UQcsse3200/2026-studio-4/pull/375)](https://github.com/UQcsse3200/2026-studio-4/pull/375), reverted in [[#406](https://github.com/UQcsse3200/2026-studio-4/pull/406)](https://github.com/UQcsse3200/2026-studio-4/pull/406) because it went in without review, then re-applied in #410.)Test plan: [Weapon Knockback Test Plan](Weapon-Knockback-Test-Plan)
Hitting an enemy didn't stop it from walking into you. The weird part is that knockback was already being applied, it just got undone straight away:
- The weapon hitbox touches the enemy and
TouchAttackComponent.attack()gives the enemy's body an impulse (the knockback). - Next frame, the enemy's
PhysicsMovementComponent.update()steers it back towards the player. It does this with an impulse of(desiredVelocity - currentVelocity) * mass, which basically resets the body to its chase speed. - So the knockback lasts one frame and the enemy just keeps coming.
There was also a smaller bug with the sword. The push direction was worked out from the centre of the hitbox instead of from the player. If an enemy was standing between you and the middle of the sword swing, it got pushed towards you.
| File | Change |
|---|---|
PhysicsMovementComponent |
Added stagger(float duration) and isStaggered(). While an enemy is staggered, update() doesn't steer it or hold it still, so the push can actually move it. The stagger is checked before the immobilise (freeze) check, see [Medusa](#medusa) below. If it gets staggered again with a shorter time, it keeps the longer one. A negative duration throws IllegalArgumentException. |
TouchAttackComponent |
After the knockback impulse, it staggers the target (if it has a PhysicsMovementComponent) for KNOCKBACK_STAGGER. Also added setKnockbackSource(Entity) so the push can come from a different entity than the hitbox. The knockback code is now in its own method, applyKnockback(Entity), which also keeps Sonar happy about method complexity. |
HitboxFactory |
Sets the hitbox's owner (the player) as the knockback source. |
hitbox touches enemy
-> TouchAttackComponent.attack(target)
-> targetStats.hit(...) // damage, same as before
-> applyKnockback(target)
-> direction = enemy centre - source centre (source = player if set)
-> applyLinearImpulse(direction * knockback)
-> movement.stagger(0.25s)
-> enemy doesn't steer for 0.25s, so it slides back
-> then it starts chasing again
| Weapon | Pushes away from |
|---|---|
| Knife | The player. Only the last stab of the upgraded flurry knocks back (1.5x), the slashes don't (see Knife). |
| Sword | The player, no matter where the enemy is on the arc. |
| Bow | The arrow, since arrows don't have an owner. In practice that's roughly the direction the arrow was flying. |
If the enemy and the source are in exactly the same spot, it uses the hitbox centre instead so the direction is never zero.
Medusa has a hit-recovery effect (MedusaHitRecoveryComponent) that freezes her for 0.35s whenever the player hits her. The freeze works through the immobilise check in PhysicsMovementComponent.update(), which sets the body's velocity to zero every frame.
In the first version of this fix, the immobilise check ran before the stagger check, so the freeze zeroed Medusa's velocity on the frame after the knockback and she didn't move back at all. Mitchell caught this in review on #410. The stagger check now runs first:
- Hit: Medusa takes damage, gets the 0.35s freeze, and is pushed back.
- 0 to 0.25s: she's staggered, so she slides back.
- 0.25 to 0.35s: the stagger is over and the freeze holds her still.
- After 0.35s: she chases again.
Freeze bombs, freeze traps and petrification still hold enemies completely still when nothing has just knocked them back.
- I didn't want to change the movement maths itself, because every NPC made through
NPCFactory.createBaseNPC()usesPhysicsMovementComponent. A short stagger only changes things right after a hit and leaves normal movement alone. - The stagger check sits in
PhysicsMovementComponent.update()because every steered enemy goes through there anyway, so the AI tasks don't each need to know about knockback. Freeze/immobilise effects are already handled in the same spot, so it fits. - Pushing away from the player is what you'd expect when you hit something. Using the hitbox centre only happened to work for the knife.
- The player doesn't use
PhysicsMovementComponent, so enemies knocking the player around works exactly like before.
| Value | Where | What it does |
|---|---|---|
Knockback 2f
|
PlayerFactory, new WeaponStatsComponent(0.5f, 1f, 2f)
|
How hard every weapon pushes. |
KNOCKBACK_STAGGER = 0.25f |
TouchAttackComponent |
How long (seconds) an enemy stops chasing after being knocked back. |
- Enemies that set their velocity directly in an AI task (like dash or stampede attacks) still get pushed, but they aren't staggered, so the task can override the push.
- Bosses/minibosses with their own movement components don't get the stagger.
- A frozen enemy that gets hit with knockback slides for the stagger time before the freeze holds it again. This is on purpose, so hits on frozen enemies still feel like hits.
- Hits with knockback
0(like the knife slashes) don't stagger anything. That's on purpose.
Claude (Anthropic, Claude Code) was used to help find the cause of the bug and draft the code, tests and this page. I reviewed and tested everything before publishing.
Entities and Components
Input Handling
- Service Locator
- Loading Resources
- Logging
- Unit Testing
- Debug Terminal
- Animations
- Audio
- AI
- Physics
- Terrain
- Concurrency & Threading
- Settings