Repository navigation
Pet
The Pet Companion System lets the player acquire a companion through the in-game shop. Three pets are available:
- Bird
- Bat
- Spirit
All pets share the same ownership, following, and target-selection behaviour. Each type has its own sprite, animations, projectile appearance, and projectile movement.
Only one pet can be active in the game world at a time, but the player can own up to two pets in dedicated pet inventory slots. These pet slots are separate from the five standard item inventory slots.
The first acquired pet is automatically activated. A second acquired pet is stored without replacing the current companion. The player can switch between owned pets using keys 6 and 7.
A pet starts attacking after the player hits an enemy. It then continues firing every 0.8 seconds, even when the player stops attacking, until the enemy dies or is removed. When the player hits another enemy, subsequent shots switch to that target.
Attached to the player and manages the currently active companion.
Main responsibilities:
- Listen for
petPurchased(ShopComponent.Pet pet). - Automatically activate the first acquired pet when no companion is currently active.
- Keep the second acquired pet stored without replacing the active companion.
- Allow the player to switch the active companion to a pet they own.
- Reject attempts to switch to an unowned pet or to the pet that is already active.
- Keep track of both the active pet entity and its corresponding
ShopComponent.Pet. - Listen for
gamblingPetReplacedand activate the newly won pet if the replaced pet was the active companion. - Remove and dispose of the active companion when necessary.
- Listen for
playerAttackHit(Entity target)and forward the target to the active pet'sPetCombatComponent.
Switching the active pet replaces only the companion entity in the game world. The stored pets remain in their existing pet inventory slots.
The manager listens on the owner once. Individual pets do not subscribe directly to the owner's hit events, so switching pets does not leave old companions receiving new attack requests.
Defines BIRD, BAT, and SPIRIT. It maps shop names to pet types and provides the corresponding animation prefix. Name matching is case-insensitive; unknown names currently fall back to Bird.
Creates the companion entity with:
PetComponentPetMovementComponentPetCombatComponentPetProjectileSpawnerComponent-
PhysicsComponentusing a kinematic body -
ColliderComponentconfigured as a sensor onPhysicsLayer.NONE AnimationRenderComponent
The factory uses PetType to select the animation prefix and starts the pet's right-facing animation. It loads a separate TextureAtlas instance for each pet; see the resource-ownership note under Notes for Other Teams.
Keeps the pet hovering above and behind its owner. The pet:
- Follows the player's position with smooth interpolation based on game delta time.
- Maintains horizontal and vertical gaps of 0.25 world units around the entities' bounds.
- Teleports to its follow position when it is more than 6 world units away from that position.
- Changes between left/right animations according to the player's horizontal movement.
The animation prefix is provided to the component, so the same movement logic works with all three pet appearances.
Stores the current target, queues target changes, and manages the firing cooldown. It emits petAttack(Entity target) on the pet during update() whenever the target is valid and the cooldown has expired.
The target remains selected between shots. A new player hit can change the target during cooldown without resetting or bypassing that cooldown.
Listens for the pet's petAttack event, creates a projectile through PetProjectileFactory, and registers it with the entity service.
This event is emitted from the combat component's update, after physics collision callbacks have finished, so creating a Box2D body is safe.
Creates a projectile at the pet's centre and selects its movement strategy using PetType:
| Pet | Appearance | Movement strategy | Behaviour |
|---|---|---|---|
| Bird | Gold dart | DirectedProjectileMovementStrategy |
Flies straight towards the target's position at launch. |
| Bat | Purple energy diamond | WaveProjectileMovementStrategy |
Oscillates around the original firing direction. |
| Spirit | Cyan orb | HomingProjectileMovementStrategy |
Turns gradually to follow its original target as it moves. |
Each strategy implements ProjectileMovementStrategy. Movement is applied through the physics body's velocity, preserving collision detection. Bird and Bat shots can miss a moving enemy; subsequent shots aim again from the pet's current position.
Controls projectile movement updates, lifetime, collision damage, and cleanup.
- Only the projectile's intended enemy can take damage; other enemies are ignored.
- Obstacles block the projectile.
- A projectile resolves only once, even if several fixtures report contact.
- Damage uses the player's
CombatStatsComponent, so kills are credited to the player. - Shield handling and enemy death/loot use the existing combat components.
- Pet hits do not emit
playerAttackHit, preventing recursive firing or unintended target changes. - Projectiles expire after their lifetime, or when their target/owner is dead or disposed, or the firing pet is disposed.
- Entity disposal is scheduled through
Gdx.app.postRunnable, avoiding body destruction during a locked Box2D step or removal during entity iteration.
Generates projectile artwork in code using a Pixmap and converts it into a texture. No external projectile sprite files are needed.
The artwork rotates with the projectile's current velocity. The temporary pixmap is released after texture creation, and the generated texture is released when the renderer is disposed.
The player has two dedicated pet inventory slots. These are separate from the five standard item slots managed by InventoryComponent.
| Key | Pet slot |
|---|---|
6 |
Pet slot 1 |
7 |
Pet slot 2 |
InventoryComponent stores the owned pets and provides operations for:
- Adding a pet to the first available pet slot.
- Checking whether a pet is already owned.
- Checking whether both pet slots are occupied.
- Replacing a pet in a specific slot when resolving a gambling reward.
- Exposing the pet slots as a read-only view.
Duplicate pets are not stored. A pet cannot be added when both slots are occupied.
The active pet is not a third inventory slot. It is one of the pets already stored in the two pet slots.
The first acquired pet becomes active automatically. Acquiring a second pet fills the remaining slot but does not replace the current active companion.
The player can switch companions using the pet slot keys:
Items
1. Item
2. Item
3. Empty
4. Empty
5. Empty
Pets
6. Bird
7. Bat
The default pet catalog in ShopComponent contains:
| Pet | Price |
|---|---|
| Bird | 20 Gold |
| Bat | 30 Gold |
| Spirit | 40 Gold |
buyPet validates the purchase before deducting gold.
A direct pet purchase succeeds only when:
- The player has enough gold.
- The pet is not already owned.
- At least one pet inventory slot is available.
The purchased pet is stored in the first available pet slot. The first pet acquired is automatically activated through the petPurchased event. A second pet is stored without replacing the active companion.
If both pet slots are occupied, another direct pet purchase is rejected and no gold is deducted. Duplicate pet purchases are also rejected without deducting gold.
First purchase
-> InventoryComponent.addPet
-> petPurchased(pet)
-> PetManagerComponent
-> create first active companion
Second purchase
-> InventoryComponent.addPet
-> petPurchased(pet)
-> current companion remains active
Pet rewards obtained through the gambling system use the same two pet inventory slots but have different full-inventory behaviour from direct purchases.
If a pet slot is available, the won pet is stored normally.
If both pet slots are occupied, the gambling spin is completed and its cost remains deducted. The won pet becomes a pending replacement and the player can:
- Replace the pet in slot 6.
- Replace the pet in slot 7.
- Cancel the replacement.
Cancelling leaves both owned pets unchanged but does not refund the completed spin.
If the replaced pet was the active companion, the newly won pet becomes active. If the replaced pet was inactive, the current active companion remains unchanged.
Winning a duplicate pet does not create another copy or replace an existing pet. The completed spin cost is still consumed.
There is currently no separate enemy-selection input in the pet system. The player selects a target by hitting it. Attacking empty space does not give the pet a target.
PlayerActions emits playerAttackHit(Entity target) after resolving melee, special, or area hits. PlayerProjectileHitComponent emits the same event after player projectiles hit living NPC targets. The notification is sent after damage is resolved, including shielded and lethal hits.
Player hits enemy A
-> playerAttackHit(A)
-> PetManagerComponent forwards the hit
-> PetCombatComponent queues A
-> update selects A and fires when ready
-> petAttack(A)
-> projectile spawns and deals damage on contact
-> pet fires again after cooldown while A remains valid
There is currently no separate enemy-selection input in the pet system. The player selects a target by hitting it. Attacking empty space does not give the pet a target.
PlayerActions emits playerAttackHit(Entity target) after resolving melee, special, or area hits. PlayerProjectileHitComponent emits the same event after player projectiles hit living NPC targets. The notification is sent after damage is resolved, including shielded and lethal hits.
Player hits enemy A
-> playerAttackHit(A)
-> PetManagerComponent forwards the hit
-> PetCombatComponent queues A
-> update selects A and fires when ready
-> petAttack(A)
-> projectile spawns and deals damage on contact
-> pet fires again after cooldown while A remains valid
- Stopping player attacks does not stop the pet from firing at its selected enemy.
- Hitting enemy B changes the target for subsequent shots, even if the pet is on cooldown.
- Switching targets does not grant an immediate extra shot or restart the timer.
- Projectiles already in flight retain their original target, including Spirit projectiles.
- If several hit events arrive before the next update, the nearest living hit enemy to the player is preferred. Equal distances are resolved using the lower entity ID.
- Nearby enemies that the player has not hit are never selected automatically.
The pet clears its target when that enemy dies or is disposed. It waits for another player hit instead of automatically choosing a replacement.
If the player hits a different enemy and kills it immediately, with no surviving candidate from that update's hit events, the pet clears its old target rather than continuing to attack the previous enemy.
Player death/disposal, disabling pet combat, or disposing/replacing the pet also stops its firing. A replacement pet starts without a target and needs a new player hit. When game delta time is zero, the cooldown does not advance and no new shot is fired.
Buy Bird -> Bird is stored in slot 6 and becomes active
Hit enemy A once -> Bird repeatedly attacks A
Stop player attacks -> Bird continues attacking A
Hit enemy B -> Bird's next shots target B
Enemy B dies -> Bird stops and waits for another player hit
Buy Bat -> Bat is stored in slot 7; Bird remains active
Press 7 -> Bat becomes active and Bird is removed from the game world```
---
## Current Combat Settings
| Setting | Current value | Defined in |
| --- | --- | --- |
| Firing cooldown | 0.8 seconds of game time | `PetCombatComponent` |
| Damage per projectile | 5, before shield handling | `PetProjectileFactory` |
| Projectile lifetime | 4 seconds of game time | `PetProjectileComponent` |
| Bird speed | 8 world units/second | `PetProjectileFactory` |
| Bat forward speed | 8 world units/second | `PetProjectileFactory` |
| Bat wave amplitude | 0.25 world units | `PetProjectileFactory` |
| Bat wave frequency | 2 cycles/second | `PetProjectileFactory` |
| Spirit speed | 6 world units/second | `PetProjectileFactory` |
| Spirit maximum turn rate | 360 degrees/second | `PetProjectileFactory` |
The factory configures projectile hitboxes to detect `NPC` and `OBSTACLE` layers. Projectile bodies are dynamic sensors so they can detect static terrain as well as moving enemies. Movement strategies disable gravity and damping for flight.
---
## Pet Assets
Paths below are relative to the repository root.
- Pet sprites: `source/core/assets/images/pet.png`
- Animation definitions: `source/core/assets/images/pet.atlas`
- Projectile artwork: generated in `PetProjectileRenderComponent`; it does not use the pet atlas.
| Pet type | Animation prefix | Left animation | Right animation |
| --- | --- | --- | --- |
| `BIRD` | `bird` | `bird_left` | `bird_right` |
| `BAT` | `bat` | `bat_left` | `bat_right` |
| `SPIRIT` | `spirit` | `spirit_left` | `spirit_right` |
New pet animations should follow `<pet_name>_left` and `<pet_name>_right`. Adding a pet also requires extending `PetType` and its name mapping, the shop catalog, projectile selection, and artwork selection; adding atlas regions alone is not sufficient.
---
## Important Behaviour
- The player starts without a pet.
- The player can own a maximum of **two pets** at a time.
- Pet inventory slots are separate from the five standard item inventory slots.
- Only **one owned pet is active in the game world at a time**.
- The first acquired pet becomes active automatically.
- The second acquired pet is stored without replacing the active companion.
- Keys **6** and **7** switch between pets stored in the two pet slots.
- Switching pets does not change the player's active item slot.
- Direct purchases are rejected without deducting gold when the pet inventory is full or the pet is already owned.
- A gambling pet reward can replace either stored pet when both slots are occupied.
- Cancelling a gambling pet replacement leaves the pets unchanged but does not refund the completed spin.
- Replacing the active pet with a gambling reward activates the new pet.
- Replacing an inactive pet preserves the current active companion.
- Pet movement and sprite direction follow the player. Projectile direction is determined separately by the combat target.
- A pet needs an initial confirmed player hit to acquire a target, but does not need repeated player attacks to keep firing.
---
## Relevant Files
Java paths below are relative to `source/core/src/main/com/csse3200/game/`.
| Area | Files |
| --- | --- |
| Ownership and type | `components/pet/PetComponent.java`, `components/pet/PetType.java` |
| Active pet management | `components/pet/PetManagerComponent.java` |
| Following | `components/pet/PetMovementComponent.java` |
| Targeting and cooldown | `components/pet/PetCombatComponent.java` |
| Projectile spawning and lifecycle | `components/pet/PetProjectileSpawnerComponent.java`, `components/pet/PetProjectileComponent.java` |
| Factories | `entities/factories/PetFactory.java`, `entities/factories/PetProjectileFactory.java` |
| Movement strategies | `components/projectile/ProjectileMovementStrategy.java`, `DirectedProjectileMovementStrategy.java`, `WaveProjectileMovementStrategy.java`, `HomingProjectileMovementStrategy.java` in that package |
| Projectile rendering | `rendering/PetProjectileRenderComponent.java` |
| Shop integration | `components/player/ShopComponent.java`, `components/player/ShopDisplay.java` |
| Player hit notifications | `components/player/PlayerActions.java`, `components/player/PlayerProjectileHitComponent.java` |
| Damage and death | `components/CombatStatsComponent.java`, `components/npc/EnemyDeathComponent.java` |
| Pet inventory | `components/player/InventoryComponent.java`, `components/player/InventoryDisplay.java` |
| Pet selection input | `components/player/KeyboardPlayerInputComponent.java` |
---
## Testing
Tests are under `source/core/src/test/com/csse3200/game/`.
| Test | Main coverage |
| --- | --- |
| `PetComponentTest` | Owner storage and validation. |
| `PetManagerComponentTest` | Acquisition, replacement, event routing, and stopping the old pet. |
| `PetMovementComponentTest` | Hovering, following, turning, and teleport recovery. |
| `PetCombatComponentTest` | Repeated firing, cooldown, target switching, multiple hit candidates, death, disabling, and pause behaviour. |
| `PetProjectileComponentTest` | Real Box2D contacts; repeated shots after one player hit for all three pets; retargeting; movement; variable frame times; shields; terrain; kill credit; loot; cleanup. |
| `PetShopIntegrationTest` | Purchasing, replacing, moving, and rendering all pets with the shop UI and real pet atlas. |
| `PetProjectileRenderComponentTest` | Artwork rotation and generated texture disposal. |
| `PlayerAttackHitEventTest`, `PlayerProjectileHitComponentTest`, `PlayerActionsSpecialAttackTest` | Player hit notifications used to select the pet's target. |
| `ShopComponentTest`, `ShopGamblingTest` | Shop purchases and prize delivery. |
| `InventoryComponentTest` | Dedicated pet slots, duplicate prevention, full inventory handling, replacement, and read-only pet slot access. |
| `PetManagerComponentTest` | Acquisition, activation, owned-pet switching, replacement, event routing, and disposal. |
| `PetShopIntegrationTest` | Pet purchasing, inventory storage, active companion behaviour, movement, animation, physics, and disposal. |
| `KeyboardPlayerInputComponentTest` | Pet selection through slots 6 and 7 and independence from standard item selection. |
| `ShopComponentTest`, `ShopGamblingTest` | Direct pet purchases, full/duplicate handling, gambling pet prizes, replacement, and cancellation. |
### Running tests
From the repository's `source` directory, run the full test task on Windows:
```powershell
.\gradlew.bat test
To run only the pet and projectile-rendering tests:
.\gradlew.bat :core:test --tests "com.csse3200.game.components.pet.*" --tests "com.csse3200.game.rendering.PetProjectileRenderComponentTest"On macOS/Linux, use ./gradlew in place of .\gradlew.bat. The project uses the Java 21 toolchain.
The latest targeted verification during implementation passed 102 related tests, the desktop classes build, and formatting checks on the changed Java files. This result is for the selected pet/combat/rendering tests, not a claim that the complete repository test task was run. Direct in-game playtesting remains a separate check.
- Start without a pet, acquire Bird, then replace it with Bat and Spirit. Check that only one companion is active.
- Walk left/right and move far enough to trigger follow-position recovery. Check hovering and animations.
- Hit enemy A once, release the attack input, and confirm that the pet keeps firing at A.
- Hit enemy B during cooldown and confirm that subsequent shots target B.
- Let the selected enemy die. Check that the pet stops and does not choose a nearby enemy by itself.
- Replace a pet during combat. Check that the replacement waits for another player hit.
- Check each projectile's shape and movement, terrain blocking, shields, and enemy death/loot behaviour.
- Pause/resume and change scenes while projectiles are active to check timing and cleanup in the running game.
- Use
PetManagerComponentas the owner of companion activation and replacement; avoid a separate pet spawn path inLevelGameArea. - Pass the
ShopComponent.Petargument withpetPurchasedand anEntityargument withplayerAttackHitandpetAttack. - Emit
playerAttackHitafter resolving a player hit. Do not emit it from pet projectile damage. - Keep target notifications lightweight. Queue them and create projectile physics bodies from the update flow, outside collision callbacks.
- Defer projectile disposal until Box2D and entity iteration have finished.
- Reuse the player's combat stats when applying pet damage so kill credit and existing death/loot handling remain consistent.
- Extend
ProjectileMovementStrategyfor new flight patterns, andPetProjectileRenderComponentfor new artwork. - Preserve animation naming conventions and explicit texture ownership when adding pet appearances.
Resource-ownership note: PetFactory currently loads an independent atlas but passes it to the one-argument AnimationRenderComponent constructor. That constructor does not set ownsAtlas; automatic disposal of the pet atlas should therefore not be assumed. Atlas disposal ownership is a follow-up to review when changing resource handling. Generated projectile textures have their own explicit cleanup in PetProjectileRenderComponent.