diff --git a/source/core/assets/configs/Engineers.json b/source/core/assets/configs/Engineers.json new file mode 100644 index 000000000..35848e3c2 --- /dev/null +++ b/source/core/assets/configs/Engineers.json @@ -0,0 +1,6 @@ +{ + "engineer" : { + "health": 100, + "baseAttack": 5 +} +} \ No newline at end of file diff --git a/source/core/assets/images/engineers/engineer.atlas b/source/core/assets/images/engineers/engineer.atlas new file mode 100644 index 000000000..2b98261bd --- /dev/null +++ b/source/core/assets/images/engineers/engineer.atlas @@ -0,0 +1,391 @@ + +engineer.png +size: 2048, 128 +format: RGBA8888 +filter: Nearest, Nearest +repeat: none +death + rotate: false + xy: 503, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 6 +death + rotate: false + xy: 902, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 3 +death + rotate: false + xy: 1358, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 5 +death + rotate: false + xy: 1814, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 2 +death + rotate: false + xy: 454, 42 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 4 +death + rotate: false + xy: 796, 42 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 1 +firing_auto + rotate: false + xy: 25, 80 + size: 52, 31 + orig: 52, 31 + offset: 0, 0 + index: 2 +firing_auto + rotate: false + xy: 25, 44 + size: 52, 31 + orig: 52, 31 + offset: 0, 0 + index: 4 +firing_auto + rotate: false + xy: 102, 80 + size: 52, 31 + orig: 52, 31 + offset: 0, 0 + index: 1 +firing_auto + rotate: false + xy: 25, 8 + size: 52, 31 + orig: 52, 31 + offset: 0, 0 + index: 3 +firing_single + rotate: false + xy: 102, 42 + size: 52, 33 + orig: 45, 33 + offset: 0, 0 + index: 2 +firing_single + rotate: false + xy: 179, 78 + size: 52, 33 + orig: 45, 33 + offset: 0, 0 + index: 4 +firing_single + rotate: false + xy: 249, 78 + size: 52, 33 + orig: 45, 33 + offset: 0, 0 + index: 1 +firing_single + rotate: false + xy: 319, 78 + size: 52, 33 + orig: 45, 33 + offset: 0, 0 + index: 3 +firing_single + rotate: false + xy: 343, 40 + size: 52, 33 + orig: 29, 33 + offset: 0, 0 + index: 5 +hit + rotate: false + xy: 560, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 3 +hit + rotate: false + xy: 1415, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 2 +hit + rotate: false + xy: 511, 42 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 1 +idle_left + rotate: false + xy: 674, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 2 +idle_left + rotate: false + xy: 1130, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 4 +idle_left + rotate: false + xy: 1529, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 1 +idle_left + rotate: false + xy: 102, 5 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 3 +idle_right + rotate: false + xy: 1016, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 2 +idle_right + rotate: false + xy: 1586, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 4 +default + rotate: false + xy: 1928, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 1 +idle_right + rotate: false + xy: 1928, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 1 +idle_right + rotate: false + xy: 568, 42 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 3 +prep + rotate: false + xy: 343, 40 + size: 52, 33 + orig: 29, 33 + offset: 0, 0 + index: 4 +prep + rotate: false + xy: 853, 41 + size: 52, 33 + orig: 29, 33 + offset: 0, 0 + index: 1 +prep + rotate: false + xy: 907, 41 + size: 52, 33 + orig: 29, 33 + offset: 0, 0 + index: 3 +prep + rotate: false + xy: 961, 41 + size: 52, 33 + orig: 29, 33 + offset: 0, 0 + index: 2 +walk_left + rotate: false + xy: 446, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 6 +walk_left + rotate: false + xy: 845, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 3 +walk_left + rotate: false + xy: 959, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 8 +walk_left + rotate: false + xy: 1301, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 5 +walk_left + rotate: false + xy: 1700, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 2 +walk_left + rotate: false + xy: 1871, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 7 +walk_left + rotate: false + xy: 397, 42 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 4 +walk_left + rotate: false + xy: 739, 42 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 1 +walk_prep + rotate: false + xy: 389, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 8 +walk_prep + rotate: false + xy: 731, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 5 +walk_prep + rotate: false + xy: 1073, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 2 +walk_prep + rotate: false + xy: 1244, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 7 +walk_prep + rotate: false + xy: 1643, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 4 +walk_prep + rotate: false + xy: 1985, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 1 +walk_prep + rotate: false + xy: 229, 41 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 6 +walk_prep + rotate: false + xy: 625, 42 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 3 +walk_right + rotate: false + xy: 617, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 2 +walk_right + rotate: false + xy: 788, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 7 +walk_right + rotate: false + xy: 1187, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 4 +walk_right + rotate: false + xy: 1472, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 1 +walk_right + rotate: false + xy: 1757, 79 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 6 +walk_right + rotate: false + xy: 172, 41 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 3 +walk_right + rotate: false + xy: 286, 41 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 8 +walk_right + rotate: false + xy: 682, 42 + size: 52, 32 + orig: 32, 32 + offset: 0, 0 + index: 5 diff --git a/source/core/assets/images/engineers/engineer.png b/source/core/assets/images/engineers/engineer.png new file mode 100644 index 000000000..c2df5eddd Binary files /dev/null and b/source/core/assets/images/engineers/engineer.png differ diff --git a/source/core/assets/sounds/engineers/firing_auto.mp3 b/source/core/assets/sounds/engineers/firing_auto.mp3 new file mode 100644 index 000000000..f0b68569d Binary files /dev/null and b/source/core/assets/sounds/engineers/firing_auto.mp3 differ diff --git a/source/core/assets/sounds/engineers/firing_single.mp3 b/source/core/assets/sounds/engineers/firing_single.mp3 new file mode 100644 index 000000000..73a991588 Binary files /dev/null and b/source/core/assets/sounds/engineers/firing_single.mp3 differ diff --git a/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java b/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java index b8f318de2..698c3c282 100644 --- a/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java +++ b/source/core/src/main/com/csse3200/game/areas/ForestGameArea.java @@ -1,12 +1,10 @@ package com.csse3200.game.areas; -import com.badlogic.gdx.Gdx; import com.badlogic.gdx.audio.Music; import com.badlogic.gdx.math.GridPoint2; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector2; import com.csse3200.game.components.ProjectileEffects; -import com.csse3200.game.input.DropInputComponent; import com.csse3200.game.areas.terrain.TerrainFactory; import com.csse3200.game.areas.terrain.TerrainFactory.TerrainType; import com.csse3200.game.entities.Entity; @@ -20,7 +18,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Timer; -import java.util.TimerTask; import static com.csse3200.game.entities.factories.NPCFactory.createGhost; @@ -115,7 +112,9 @@ public class ForestGameArea extends GameArea { "sounds/Impact4.ogg", "sounds/towers/gun_shot_trimmed.mp3", "sounds/towers/deploy.mp3", - "sounds/towers/stow.mp3" + "sounds/towers/stow.mp3", + "sounds/engineers/firing_auto.mp3", + "sounds/engineers/firing_single.mp3" }; private static final String backgroundMusic = "sounds/background/Sci-Fi1.ogg"; private static final String[] forestMusic = {backgroundMusic}; @@ -145,36 +144,26 @@ public ForestGameArea(TerrainFactory terrainFactory) { /** Create the game area, including terrain, static entities (trees), dynamic entities (player) */ @Override public void create() { + // Load game assets loadAssets(); - displayUI(); - spawnTerrain(); -// spawnBuilding1(); -// spawnBuilding2(); -// spawnMountains(); + player = spawnPlayer(); player.getEvents().addListener("spawnWave", this::spawnXenoGrunts); playMusic(); // Types of projectile - spawnEffectProjectile(new Vector2(0, 10), PhysicsLayer.PLAYER, towardsMobs, new Vector2(2f, 2f), ProjectileEffects.BURN, true); -// spawnProjectile(new Vector2(0, 10), player, towardsMobs, new Vector2(2f, 2f)); -// spawnMultiProjectile(new Vector2(0, 10), player, towardsMobs, 20, new Vector2(2f, 2f), 7); + spawnEffectProjectile(new Vector2(0, 10), PhysicsLayer.HUMANS, towardsMobs, new Vector2(2f, 2f), ProjectileEffects.BURN, true); spawnXenoGrunts(); spawnGhosts(); spawnWeaponTower(); - spawnIncome(); - spawnScrap(); - + spawnEngineer(); bossKing1 = spawnBossKing1(); bossKing2 = spawnBossKing2(); - spawnTNTTower(); - - playMusic(); } private void displayUI() { @@ -502,6 +491,18 @@ private void spawnIncome() { spawnEntityAt(towerfactory, randomPos, true, true); } } + + private void spawnEngineer() { - + for (int i = 0; i < terrain.getMapBounds(0).x; i += 3) { + Entity engineer = EngineerFactory.createEngineer(); + spawnEntityAt(engineer, new GridPoint2(1, i), true, true); + } +// GridPoint2 minPos = new GridPoint2(0, 0); +// GridPoint2 maxPos = new GridPoint2(5, terrain.getMapBounds(0).sub(2, 2).y); +// GridPoint2 randomPos = RandomUtils.random(minPos, maxPos); +// +// Entity engineer = EngineerFactory.createEngineer(); +// spawnEntityAt(engineer, randomPos, true, true); + } } \ No newline at end of file diff --git a/source/core/src/main/com/csse3200/game/components/CombatStatsComponent.java b/source/core/src/main/com/csse3200/game/components/CombatStatsComponent.java index 282deac9b..0aae20c67 100644 --- a/source/core/src/main/com/csse3200/game/components/CombatStatsComponent.java +++ b/source/core/src/main/com/csse3200/game/components/CombatStatsComponent.java @@ -130,12 +130,18 @@ public void setBaseAttack(int attack) { public void hit(Integer damage) { int newHealth = getHealth() - damage; setHealth(newHealth); + if (entity != null && !this.isDead()) { + entity.getEvents().trigger("hitStart"); + } changeState(); } // Default CombatStatsComponent that relies on the attacker's combatStatsComponent. public void hit(CombatStatsComponent attacker) { int newHealth = getHealth() - attacker.getBaseAttack(); + if (entity != null && !this.isDead()) { + entity.getEvents().trigger("hitStart"); + } setHealth(newHealth); changeState(); } diff --git a/source/core/src/main/com/csse3200/game/components/player/HumanAnimationController.java b/source/core/src/main/com/csse3200/game/components/player/HumanAnimationController.java new file mode 100644 index 000000000..e65fc8763 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/player/HumanAnimationController.java @@ -0,0 +1,138 @@ +package com.csse3200.game.components.player; + +import com.badlogic.gdx.audio.Sound; +import com.csse3200.game.components.Component; +import com.csse3200.game.rendering.AnimationRenderComponent; +import com.csse3200.game.services.ServiceLocator; + +/** + * Listens for events relevant to a Human character (Just engineers at this stage) + * Each event will trigger a certain animation + */ +public class HumanAnimationController extends Component { + // Event name constants + private static final String IDLEL = "idleLeft"; + private static final String IDLER = "idleRight"; + private static final String WALKL = "walkLeftStart"; + private static final String WALKR = "walkRightStart"; + private static final String WALK_PREP = "walkPrepStart"; + private static final String PREP = "prepStart"; + private static final String FIRING_AUTO = "firingAutoStart"; + private static final String FIRING_SINGLE = "firingSingleStart"; + private static final String HIT = "hitStart"; + private static final String DEATH = "deathStart"; + // Animation name constants + private static final String IDLEL_ANIM = "idle_left"; + private static final String IDLER_ANIM = "idle_right"; + private static final String WALKL_ANIM = "walk_left"; + private static final String WALKR_ANIM = "walk_right"; + private static final String WALK_PREP_ANIM = "walk_prep"; + private static final String FIRE_AUTO_ANIM = "firing_auto"; + private static final String FIRE_SINGLE_ANIM = "firing_single"; + private static final String HIT_ANIM = "hit"; + private static final String DEATH_ANIM = "death"; + // Sound effects constants + private static final String FIRE_AUTO_SFX = "sounds/engineers/firing_auto.mp3"; + private static final String FIRE_SINGLE_SFX = "sounds/engineers/firing_single.mp3"; + + AnimationRenderComponent animator; + Sound fireAutoSound = ServiceLocator.getResourceService().getAsset( + FIRE_AUTO_SFX, Sound.class); + Sound fireSingleSound = ServiceLocator.getResourceService().getAsset( + FIRE_SINGLE_SFX, Sound.class); + + /** + * Instantiates a HumanAnimationController and adds all the event listeners for the + * Human entity - Just engineers at this stage. + */ + @Override + public void create() { + super.create(); + animator = this.entity.getComponent(AnimationRenderComponent.class); + entity.getEvents().addListener(IDLEL, this::animateIdleLeft); + entity.getEvents().addListener(IDLER, this::animateIdleRight); + entity.getEvents().addListener(WALKL, this::animateLeftWalk); + entity.getEvents().addListener(WALKR, this::animateRightWalk); + entity.getEvents().addListener(PREP, this::animatePrep); + entity.getEvents().addListener(WALK_PREP, this::animatePrepWalk); + entity.getEvents().addListener(FIRING_SINGLE, this::animateSingleFiring); + entity.getEvents().addListener(FIRING_AUTO, this::animateFiring); + entity.getEvents().addListener(HIT, this::animateHit); + entity.getEvents().addListener(DEATH, this::animateDeath); + } + + /** + * Callback that starts the idle animation facing left + */ + void animateIdleLeft() { + animator.startAnimation(IDLEL_ANIM); + } + + /** + * Callback that starts the idle animation facing right + */ + void animateIdleRight() { + animator.startAnimation(IDLER_ANIM); + } + + /** + * Callback that starts the walk animation for left movement + */ + void animateLeftWalk() { + animator.startAnimation(WALKL_ANIM); +// runSound.play(); + } + + /** + * Callback that starts the walk animation for right movement + */ + void animateRightWalk() { + animator.startAnimation(WALKR_ANIM); + } + + /** + * Callback that starts the walk animation in the 'prepared' state, i.e., weapon up and ready to fight - currently + * unused, but intended to be incorporated as engineer functionality expands + */ + void animatePrepWalk() { + animator.startAnimation(WALK_PREP_ANIM); + } + + /** + * Callback that starts the shoot animation in single fire mode, and plays the single fire sound + */ + void animateSingleFiring() { + animator.startAnimation(FIRE_SINGLE_ANIM); + fireSingleSound.play(); + } + + /** + * Callback that starts the shoot animation in auto mode and plays the auto fire sound. + * Currently unused, but intended to be incorporated as engineer functionality expands. + */ + void animateFiring() { + animator.startAnimation(FIRE_AUTO_ANIM); + fireAutoSound.play(); + } + + /** + * Callback that starts the 'prep' animation, i.e., raising weapon in preparation for firing + */ + void animatePrep() { + animator.startAnimation(PREP); + } + + /** + * Callback that starts the 'hit' animation when engineer is damaged + */ + void animateHit() { + animator.startAnimation(HIT_ANIM); + } + + /** + * Callback that starts the 'death' animation when the engineer entity's health reaches zero. + */ + void animateDeath() { + animator.startAnimation(DEATH_ANIM); + } +} \ No newline at end of file diff --git a/source/core/src/main/com/csse3200/game/components/tasks/EngineerCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/EngineerCombatTask.java new file mode 100644 index 000000000..40e91edea --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/tasks/EngineerCombatTask.java @@ -0,0 +1,218 @@ +package com.csse3200.game.components.tasks; + +import com.badlogic.gdx.math.Vector2; +import com.csse3200.game.ai.tasks.DefaultTask; +import com.csse3200.game.ai.tasks.PriorityTask; +import com.csse3200.game.components.CombatStatsComponent; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.factories.ProjectileFactory; +import com.csse3200.game.physics.PhysicsEngine; +import com.csse3200.game.physics.PhysicsLayer; +import com.csse3200.game.physics.raycast.RaycastHit; +import com.csse3200.game.services.GameTime; +import com.csse3200.game.services.ServiceLocator; + +import java.util.ArrayList; + +/** + * The AI Task for the Engineer entity. The Engineer will scan for targets within its detection range + * and trigger events to change its state accordingly. This task must be called once the Engineer has + * appropiately moved into position. + */ +public class EngineerCombatTask extends DefaultTask implements PriorityTask { + + private static final int INTERVAL = 1; // The time interval for each target scan from the Engineer. + private static final int PRIORITY = 3; // Default priority of the combat task when mobs are in range. + private static final short TARGET = PhysicsLayer.NPC; // The type of targets that the Engineer will detect. + + // Animation event names for the Engineer's state machine. + private static final String STOW = ""; + private static final String DEPLOY = ""; + private static final String FIRING = "firingSingleStart"; + private static final String IDLE_LEFT = "idleLeft"; + private static final String IDLE_RIGHT = "idleRight"; + private static final String DYING = "deathStart"; + + // The Engineer's attributes. + private final float maxRange; // The maximum range of the Engineer's weapon. + // weaponCapacity is the number of shots fired before the engineer has to reload + private static final int weaponCapacity = 10; + private int shotsFired = 0; // Tracks the number of shots fired in the current cycle + + private Vector2 engineerPosition = new Vector2(10, 50); // Placeholder value for the Engineer's position. + private final Vector2 maxRangePosition = new Vector2(); + private PhysicsEngine physics; + private GameTime timeSource; + private long endTime; + private long reloadTime; + + private ArrayList hits = new ArrayList<>(); + private final RaycastHit hit = new RaycastHit(); + private ArrayList targets = new ArrayList<>(); + + /** The Engineer's states. */ + private enum STATE { + IDLE_LEFT, IDLE_RIGHT, DEPLOY, FIRING, STOW + } + private STATE engineerState = STATE.IDLE_RIGHT; + + /** + * @param maxRange The maximum range of the Engineer's weapon. + */ + public EngineerCombatTask(float maxRange) { + this.maxRange = maxRange; + physics = ServiceLocator.getPhysicsService().getPhysics(); + timeSource = ServiceLocator.getTimeSource(); + } + + /** + * Runs the task and triggers Engineer's idle animation. + */ + @Override + public void start() { + super.start(); + // Set the tower's coordinates + this.engineerPosition = owner.getEntity().getCenterPosition(); + this.maxRangePosition.set(engineerPosition.x + maxRange, engineerPosition.y); + // Default to idle mode + owner.getEntity().getEvents().trigger(IDLE_RIGHT); + + endTime = timeSource.getTime() + (INTERVAL * 500); + } + + /** + * The update method is what is run every time the TaskRunner in the AiTaskComponent calls update(). + * triggers events depending on the presence or otherwise of targets in the detection range + */ + @Override + public void update() { + if (timeSource.getTime() >= endTime) { + updateEngineerState(); + endTime = timeSource.getTime() + (INTERVAL * 1000); + } + } + + /** + * Engineer state machine + */ + public void updateEngineerState() { + // configure tower state depending on target visibility + switch (engineerState) { + case IDLE_LEFT -> { + // targets detected in idle mode - start deployment + if (isTargetVisible()) { + owner.getEntity().getEvents().trigger(FIRING); + engineerState = STATE.FIRING; + } else { + + } + } + case IDLE_RIGHT -> { + // targets detected in idle mode - start deployment + if (isTargetVisible()) { + combatState(); + } + } + case DEPLOY -> { + // currently deploying, + if (isTargetVisible()) { + combatState(); + } else { + owner.getEntity().getEvents().trigger(STOW); + engineerState = STATE.STOW; + } + } + case FIRING -> { + // targets gone - stop firing + if (!isTargetVisible()) { + owner.getEntity().getEvents().trigger(IDLE_RIGHT); + engineerState = STATE.IDLE_RIGHT; + } else { + if (shotsFired <= 10) { + owner.getEntity().getEvents().trigger(FIRING); + // this might be changed to an event which gets triggered everytime the tower enters the firing state + Entity newProjectile = ProjectileFactory.createFireBall(PhysicsLayer.NPC, + new Vector2(100, owner.getEntity().getPosition().y), + new Vector2(4f, 4f)); + newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.75), (float) (owner.getEntity().getPosition().y + 0.4)); + ServiceLocator.getEntityService().register(newProjectile); + shotsFired += 1; + reloadTime = timeSource.getTime(); + } else { + // engineer needs to reload + if (reloadTime < timeSource.getTime()) { + // engineer has reloaded + shotsFired = 0; + reloadTime = timeSource.getTime(); + } + } + } + } + } + } + + /** + * Puts the engineerCombatTask state into combat mode + */ + private void combatState() { + owner.getEntity().getEvents().trigger(FIRING); + engineerState = STATE.FIRING; + } + /** + * For stopping the running task + */ + @Override + public void stop() { + super.stop(); + } + + /** + * Simplified getPriority function, returns the priority of the task + * @return priority as an integer value. If mobs are visible, return the current priority, otherwise return 0. + */ + @Override + public int getPriority() { + return isTargetVisible() ? PRIORITY : 0; + } + + /** + * Uses a raycast to determine whether there are any targets in detection range. Performs multiple raycasts + * to a range of points at x = engineer.x + maxRange, and a range of y values above and below current y position. + * Allows the engineer entity to detect mobs in adjacent lanes. + * @return true if a target is detected, false otherwise + */ + public boolean isTargetVisible() { + // If there is an obstacle in the path to the max range point, mobs visible. + Vector2 position = owner.getEntity().getCenterPosition(); + + for (int i = 5; i > -5; i--) { + if (physics.raycast(position, new Vector2(position.x + maxRange, position.y + i), TARGET, hit)) { + hits.add(hit); + targets.add(new Vector2(position.x + maxRange, position.y + i)); + } + } + return !hits.isEmpty(); + } + + /** + * Fetches the nearest target from the array of detected target positions created during the last call of + * isTargetVisible + * @return a Vector2 position of the nearest mob detected. + */ + public Vector2 fetchTarget() { + // Initial nearest position for comparison + int lowest = 10; + + Vector2 nearest = new Vector2(owner.getEntity().getCenterPosition().x, + owner.getEntity().getCenterPosition().y); + + // Find the nearest target from the array of targets + for (Vector2 tgt : targets){ + if (Math.abs(tgt.y - nearest.y) < lowest) { + lowest = (int)Math.abs(tgt.y - nearest.y); + nearest = tgt; + } + } + return nearest; + } +} diff --git a/source/core/src/main/com/csse3200/game/components/tasks/MobAttackTask.java b/source/core/src/main/com/csse3200/game/components/tasks/MobAttackTask.java index aaba29da7..e6a83c610 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/MobAttackTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/MobAttackTask.java @@ -126,7 +126,7 @@ public void updateMobState() { mobState = STATE.STOW; } else { owner.getEntity().getEvents().trigger(FIRING); - Entity newProjectile = ProjectileFactory.createMobBall(PhysicsLayer.PLAYER, new Vector2(0, owner.getEntity().getPosition().y), new Vector2(2f,2f)); + Entity newProjectile = ProjectileFactory.createMobBall(PhysicsLayer.HUMANS, new Vector2(0, owner.getEntity().getPosition().y), new Vector2(2f,2f)); newProjectile.setPosition((float) (owner.getEntity().getPosition().x), (float) (owner.getEntity().getPosition().y)); newProjectile.setScale(-1f, 0.5f); ServiceLocator.getEntityService().register(newProjectile); diff --git a/source/core/src/main/com/csse3200/game/components/tasks/TowerCombatTask.java b/source/core/src/main/com/csse3200/game/components/tasks/TowerCombatTask.java index 82cde5225..752a67859 100644 --- a/source/core/src/main/com/csse3200/game/components/tasks/TowerCombatTask.java +++ b/source/core/src/main/com/csse3200/game/components/tasks/TowerCombatTask.java @@ -112,12 +112,12 @@ public void updateTowerState() { } else { owner.getEntity().getEvents().trigger(FIRING); // this might be changed to an event which gets triggered everytime the tower enters the firing state - Entity newProjectile = ProjectileFactory.createFireBall(PhysicsLayer.PLAYER, new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f,2f)); + Entity newProjectile = ProjectileFactory.createFireBall(PhysicsLayer.NPC, new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f,2f)); // * TEMPORARYYYYYYY // Entity newProjectile = ProjectileFactory.createRicochetFireball(PhysicsLayer.NPC, new Vector2(100, owner.getEntity().getPosition().y), new Vector2(2f,2f)); - newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.75), (float) (owner.getEntity().getPosition().y + 0.75)); + newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.75), (float) (owner.getEntity().getPosition().y + 0.4)); ServiceLocator.getEntityService().register(newProjectile); } } diff --git a/source/core/src/main/com/csse3200/game/components/tasks/human/HumanMovementTask.java b/source/core/src/main/com/csse3200/game/components/tasks/human/HumanMovementTask.java new file mode 100644 index 000000000..6e957e311 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/tasks/human/HumanMovementTask.java @@ -0,0 +1,97 @@ +package com.csse3200.game.components.tasks.human; + +import com.badlogic.gdx.math.Vector2; +import com.csse3200.game.ai.tasks.DefaultTask; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.physics.components.PhysicsMovementComponent; +import com.csse3200.game.services.GameTime; +import com.csse3200.game.services.ServiceLocator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Move to a given position, finishing when you get close enough. Requires an entity with a + * PhysicsMovementComponent. + */ +public class HumanMovementTask extends DefaultTask { + private static final Logger logger = LoggerFactory.getLogger(HumanMovementTask.class); + + private final GameTime gameTime; + private Vector2 target; + private float stopDistance = 0.01f; + private long lastTimeMoved; + private Vector2 lastPos; + private PhysicsMovementComponent movementComponent; + + public HumanMovementTask(Vector2 target) { + this.target = target; + this.gameTime = ServiceLocator.getTimeSource(); + } + + public HumanMovementTask(Vector2 target, float stopDistance) { + this(target); + this.stopDistance = stopDistance; + } + + @Override + public void start() { + super.start(); + this.movementComponent = owner.getEntity().getComponent(PhysicsMovementComponent.class); + movementComponent.setTarget(target); + movementComponent.setMoving(true); + + // Trigger the correct walk animation depending on the target location. + if (target.x < owner.getEntity().getPosition().x) { + owner.getEntity().getEvents().trigger("walkLeftStart"); + } else { + owner.getEntity().getEvents().trigger("walkRightStart"); + } + + logger.debug("Starting movement towards {}", target); + lastTimeMoved = gameTime.getTime(); + lastPos = owner.getEntity().getPosition(); + } + + @Override + public void update() { + if (isAtTarget()) { + movementComponent.setMoving(false); + owner.getEntity().getEvents().trigger("idleStart"); + status = Status.FINISHED; + logger.debug("Finished moving to {}", target); + } else { + checkIfStuck(); + } + } + + public void setTarget(Vector2 target) { + this.target = target; + movementComponent.setTarget(target); + } + + @Override + public void stop() { + super.stop(); + movementComponent.setMoving(false); + logger.debug("Stopping movement"); + } + + private boolean isAtTarget() { + return owner.getEntity().getPosition().dst(target) <= stopDistance; + } + + private void checkIfStuck() { + if (didMove()) { + lastTimeMoved = gameTime.getTime(); + lastPos = owner.getEntity().getPosition(); + } else if (gameTime.getTimeSince(lastTimeMoved) > 500L) { + movementComponent.setMoving(false); + status = Status.FAILED; + logger.debug("Got stuck! Failing movement task"); + } + } + + private boolean didMove() { + return owner.getEntity().getPosition().dst2(lastPos) > 0.001f; + } +} diff --git a/source/core/src/main/com/csse3200/game/components/tasks/human/HumanWaitTask.java b/source/core/src/main/com/csse3200/game/components/tasks/human/HumanWaitTask.java new file mode 100644 index 000000000..06072c46b --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/tasks/human/HumanWaitTask.java @@ -0,0 +1,39 @@ +package com.csse3200.game.components.tasks.human; + +import com.csse3200.game.ai.tasks.DefaultTask; +import com.csse3200.game.services.GameTime; +import com.csse3200.game.services.ServiceLocator; + +/** + * Task that does nothing other than waiting for a given time. Status is Finished + * after the time has passed. + */ +public class HumanWaitTask extends DefaultTask { + private final GameTime timeSource; + private final float duration; + private long endTime; + + /** + * @param duration How long to wait for, in seconds. + */ + public HumanWaitTask(float duration) { + timeSource = ServiceLocator.getTimeSource(); + this.duration = duration; + } + + /** + * Start waiting from now until duration has passed. + */ + @Override + public void start() { + super.start(); + endTime = timeSource.getTime() + (int)(duration * 1000); + } + + @Override + public void update() { + if (timeSource.getTime() >= endTime) { + status = Status.FINISHED; + } + } +} diff --git a/source/core/src/main/com/csse3200/game/components/tasks/human/HumanWanderTask.java b/source/core/src/main/com/csse3200/game/components/tasks/human/HumanWanderTask.java new file mode 100644 index 000000000..be2c570f7 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/components/tasks/human/HumanWanderTask.java @@ -0,0 +1,133 @@ +package com.csse3200.game.components.tasks.human; + +import com.badlogic.gdx.math.Vector2; +import com.csse3200.game.ai.tasks.DefaultTask; +import com.csse3200.game.ai.tasks.PriorityTask; +import com.csse3200.game.ai.tasks.Task; +import com.csse3200.game.components.CombatStatsComponent; +import com.csse3200.game.components.tasks.EngineerCombatTask; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.physics.PhysicsLayer; +import com.csse3200.game.physics.components.ColliderComponent; +import com.csse3200.game.physics.components.HitboxComponent; +import com.csse3200.game.rendering.AnimationRenderComponent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wander around by moving a random position within a range of the starting position. Wait a little + * bit between movements. Requires an entity with a PhysicsMovementComponent. + */ +public class HumanWanderTask extends DefaultTask implements PriorityTask { + private static final Logger logger = LoggerFactory.getLogger(HumanWanderTask.class); + + private float maxRange; + private Vector2 wanderRange; + private final float waitTime; + private Vector2 startPos; + private HumanMovementTask movementTask; + private HumanWaitTask waitTask; + + private EngineerCombatTask combatTask; + private Task currentTask; + + private boolean isDead = false; + + /** + * @param waitTime How long in seconds to wait between wandering. + */ + public HumanWanderTask(float waitTime, float maxRange) { + this.waitTime = waitTime; + this.maxRange = maxRange; + } + + @Override + public int getPriority() { + return 1; // Low priority task + } + + @Override + public void start() { + super.start(); + startPos = owner.getEntity().getPosition(); + this.wanderRange = owner.getEntity().getCenterPosition(); + waitTask = new HumanWaitTask(waitTime); + waitTask.create(owner); + + movementTask = new HumanMovementTask(this.wanderRange, 1f); + movementTask.create(owner); + movementTask.start(); + + combatTask = new EngineerCombatTask(maxRange); + combatTask.create(owner); + combatTask.start(); + + currentTask = movementTask; + } + + @Override + public void update() { + // Check if engineer has died since last update + if (!isDead && owner.getEntity().getComponent(CombatStatsComponent.class).isDead()) { + owner.getEntity().getEvents().trigger("deathStart"); + owner.getEntity().getComponent(ColliderComponent.class).setLayer(PhysicsLayer.NONE); + owner.getEntity().getComponent(HitboxComponent.class).setLayer(PhysicsLayer.NONE); + currentTask.stop(); + // Add a time delay here to allow animation to play? + isDead = true; + } + // Check if engineer has finished dying + else if (isDead && owner.getEntity().getComponent(AnimationRenderComponent.class).isFinished()) { + owner.getEntity().setFlagForDelete(true); + // TODO: make the appropriate calls to decrement the human count. + } + // otherwise doing engineer things + else if (!isDead) { + if (currentTask.getStatus() != Status.ACTIVE) { + + if (currentTask == movementTask) { + startWaiting(); + owner.getEntity().getEvents().trigger("idleRight"); + } else if (combatTask.isTargetVisible()) { + if (combatTask.fetchTarget().y < owner.getEntity().getCenterPosition().y + 2 && + combatTask.fetchTarget().y > owner.getEntity().getCenterPosition().y - 2) { + startCombat(); + } else { + startMoving(new Vector2(owner.getEntity().getCenterPosition().x, combatTask.fetchTarget().y)); + } + } + } + currentTask.update(); + } + } + + private void startWaiting() { + logger.debug("Starting waiting"); + swapTask(waitTask); + } + + private void startMoving(Vector2 destination) { + logger.debug("Starting moving"); + movementTask.setTarget(destination); + swapTask(movementTask); + } + + private void startCombat() { + logger.debug("Starting Combat"); + swapTask(combatTask); + } + + private void swapTask(Task newTask) { + if (currentTask != null) { + currentTask.stop(); + } + currentTask = newTask; + currentTask.start(); + } + + private Vector2 getDirection() { +// float y = startPos.y; +// return new Vector2(0, y); + return this.wanderRange; + } +} diff --git a/source/core/src/main/com/csse3200/game/entities/configs/EngineerConfig.java b/source/core/src/main/com/csse3200/game/entities/configs/EngineerConfig.java new file mode 100644 index 000000000..121fb0b9c --- /dev/null +++ b/source/core/src/main/com/csse3200/game/entities/configs/EngineerConfig.java @@ -0,0 +1,6 @@ +package com.csse3200.game.entities.configs; +/** Defines the basic set of properties for an Engineer entity to be loaded by EngineerFactory */ +public class EngineerConfig extends BaseEntityConfig { + public int health = 1; + public int baseAttack = 0; +} \ No newline at end of file diff --git a/source/core/src/main/com/csse3200/game/entities/configs/EngineerConfigs.java b/source/core/src/main/com/csse3200/game/entities/configs/EngineerConfigs.java new file mode 100644 index 000000000..21113b7a0 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/entities/configs/EngineerConfigs.java @@ -0,0 +1,10 @@ +package com.csse3200.game.entities.configs; + +/** + * Defines the properties stored in Engineer config files to be loaded by the Engineer Factory. + */ +public class EngineerConfigs extends BaseEntityConfig { + public BaseEntityConfig engineer = new BaseEntityConfig(); + public int health = 10; + public int baseAttack = 1; +} diff --git a/source/core/src/main/com/csse3200/game/entities/factories/BossKingFactory.java b/source/core/src/main/com/csse3200/game/entities/factories/BossKingFactory.java index 1bd29a628..b4260b2be 100644 --- a/source/core/src/main/com/csse3200/game/entities/factories/BossKingFactory.java +++ b/source/core/src/main/com/csse3200/game/entities/factories/BossKingFactory.java @@ -101,7 +101,7 @@ public static Entity createBaseBoss(Entity target) { .addComponent(new ColliderComponent()) .addComponent(new PhysicsMovementComponent()) .addComponent(new HitboxComponent().setLayer(PhysicsLayer.NPC)) - .addComponent(new TouchAttackComponent(PhysicsLayer.PLAYER, 1.5f)); + .addComponent(new TouchAttackComponent(PhysicsLayer.HUMANS, 1.5f)); PhysicsUtils.setScaledCollider(boss, 0.9f, 0.4f); diff --git a/source/core/src/main/com/csse3200/game/entities/factories/EngineerFactory.java b/source/core/src/main/com/csse3200/game/entities/factories/EngineerFactory.java new file mode 100644 index 000000000..3f0edc0a5 --- /dev/null +++ b/source/core/src/main/com/csse3200/game/entities/factories/EngineerFactory.java @@ -0,0 +1,103 @@ +package com.csse3200.game.entities.factories; + +import com.badlogic.gdx.graphics.g2d.Animation; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.csse3200.game.ai.tasks.AITaskComponent; +import com.csse3200.game.components.CombatStatsComponent; +import com.csse3200.game.components.TouchAttackComponent; +import com.csse3200.game.components.player.HumanAnimationController; +import com.csse3200.game.components.tasks.human.HumanWanderTask; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.entities.configs.*; +import com.csse3200.game.files.FileLoader; +import com.csse3200.game.physics.PhysicsLayer; +import com.csse3200.game.physics.PhysicsUtils; +import com.csse3200.game.physics.components.ColliderComponent; +import com.csse3200.game.physics.components.HitboxComponent; +import com.csse3200.game.physics.components.PhysicsComponent; +import com.csse3200.game.physics.components.PhysicsMovementComponent; +import com.csse3200.game.rendering.AnimationRenderComponent; + +/** + * Factory to create non-playable human character (NPC) entities with predefined components. + * + * These may be modified to become controllable characters in future sprints. + * + *

Each NPC entity type should have a creation method that returns a corresponding entity. + * Predefined entity properties can be loaded from configs stored as json files which are defined in + * "NPCConfigs". + * + *

If needed, this factory can be separated into more specific factories for entities with + * similar characteristics. + */ +public class EngineerFactory { + + private static final int COMBAT_TASK_PRIORITY = 2; + private static final int ENGINEER_RANGE = 10; + private static final EngineerConfigs configs = + FileLoader.readClass(EngineerConfigs.class, "configs/Engineers.json"); + + private static final float HUMAN_SCALE_X = 1f; + private static final float HUMAN_SCALE_Y = 0.8f; + + /** + * Creates an Engineer entity, based on a base Human entity, with the appropriate components and animations + * + * + * @return entity + */ + public static Entity createEngineer() { + Entity engineer = createBaseHumanNPC(); + BaseEntityConfig config = configs.engineer; + + AnimationRenderComponent animator = new AnimationRenderComponent( + new TextureAtlas("images/engineers/engineer.atlas")); + animator.addAnimation("walk_left", 0.2f, Animation.PlayMode.LOOP); + animator.addAnimation("walk_right", 0.2f, Animation.PlayMode.LOOP); + animator.addAnimation("walk_prep", 0.2f, Animation.PlayMode.LOOP); + animator.addAnimation("idle_right", 0.2f, Animation.PlayMode.LOOP); + animator.addAnimation("firing_auto", 0.05f, Animation.PlayMode.NORMAL); + animator.addAnimation("firing_single", 0.05f, Animation.PlayMode.NORMAL); + animator.addAnimation("prep", 0.05f, Animation.PlayMode.NORMAL); + animator.addAnimation("hit", 0.01f, Animation.PlayMode.NORMAL); + animator.addAnimation("death", 0.1f, Animation.PlayMode.NORMAL); + + AITaskComponent aiComponent = new AITaskComponent(); + + engineer + .addComponent(new CombatStatsComponent(config.health, config.baseAttack)) + .addComponent(animator) + .addComponent(new HumanAnimationController()) + .addComponent(aiComponent); + + engineer.getComponent(AITaskComponent.class).addTask(new HumanWanderTask(COMBAT_TASK_PRIORITY, ENGINEER_RANGE)); + engineer.getComponent(AnimationRenderComponent.class).scaleEntity(); + engineer.setScale(HUMAN_SCALE_X, HUMAN_SCALE_Y); + return engineer; + } + + /** + * Creates a generic human npc to be used as a base entity by more specific NPC creation methods. + * + * @return entity + */ + public static Entity createBaseHumanNPC() { + + + Entity human = + new Entity() + .addComponent(new PhysicsComponent()) + .addComponent(new PhysicsMovementComponent()) + .addComponent(new ColliderComponent()) + .addComponent(new HitboxComponent().setLayer(PhysicsLayer.ENGINEER)) + .addComponent(new TouchAttackComponent(PhysicsLayer.NPC, 1.5f)); + + + PhysicsUtils.setScaledCollider(human, 0.9f, 0.4f); + return human; + } + + private EngineerFactory() { + throw new IllegalStateException("Instantiating static util class"); + } +} diff --git a/source/core/src/main/com/csse3200/game/entities/factories/NPCFactory.java b/source/core/src/main/com/csse3200/game/entities/factories/NPCFactory.java index 316cf8190..c3735c269 100644 --- a/source/core/src/main/com/csse3200/game/entities/factories/NPCFactory.java +++ b/source/core/src/main/com/csse3200/game/entities/factories/NPCFactory.java @@ -122,7 +122,6 @@ public static Entity createXenoGrunt(Entity target) { animator.addAnimation("xeno_melee_1", 0.1f, Animation.PlayMode.NORMAL); animator.addAnimation("xeno_melee_2", 0.1f, Animation.PlayMode.NORMAL); animator.addAnimation("xeno_die", 0.1f, Animation.PlayMode.NORMAL); - xenoGrunt .addComponent(new CombatStatsComponent(config.fullHeath, config.baseAttack, drops, melee, projectiles)) .addComponent(animator) @@ -144,14 +143,13 @@ public static Entity createBaseNPC(Entity target) { .addTask(new WanderTask(new Vector2(2f, 2f), 2f)) .addTask(new MobAttackTask(2, 40)) .addTask(new MobDeathTask(2)); - //.addTask(new ChaseTask(target, 10, 3f, 4f)); Entity npc = new Entity() .addComponent(new PhysicsComponent()) .addComponent(new PhysicsMovementComponent()) .addComponent(new ColliderComponent()) .addComponent(new HitboxComponent().setLayer(PhysicsLayer.NPC)) - .addComponent(new TouchAttackComponent(PhysicsLayer.PLAYER, 1.5f)) + .addComponent(new TouchAttackComponent(PhysicsLayer.HUMANS, 1.5f)) .addComponent(aiComponent); PhysicsUtils.setScaledCollider(npc, 0.9f, 0.4f); diff --git a/source/core/src/main/com/csse3200/game/entities/factories/PlayerFactory.java b/source/core/src/main/com/csse3200/game/entities/factories/PlayerFactory.java index a76b2a6ca..1f4f2f3f1 100644 --- a/source/core/src/main/com/csse3200/game/entities/factories/PlayerFactory.java +++ b/source/core/src/main/com/csse3200/game/entities/factories/PlayerFactory.java @@ -46,7 +46,7 @@ public static Entity createPlayer() { .addComponent(new TextureRenderComponent("images/box_boy_leaf.png")) .addComponent(new PhysicsComponent()) .addComponent(new ColliderComponent()) - .addComponent(new HitboxComponent().setLayer(PhysicsLayer.PLAYER)) + .addComponent(new HitboxComponent().setLayer(PhysicsLayer.ENGINEER)) .addComponent(new PlayerActions()) .addComponent(new CombatStatsComponent(stats.health, stats.baseAttack)) .addComponent(new InventoryComponent(stats.gold)) diff --git a/source/core/src/main/com/csse3200/game/entities/factories/ProjectileFactory.java b/source/core/src/main/com/csse3200/game/entities/factories/ProjectileFactory.java index 503df0b93..58a705f0a 100644 --- a/source/core/src/main/com/csse3200/game/entities/factories/ProjectileFactory.java +++ b/source/core/src/main/com/csse3200/game/entities/factories/ProjectileFactory.java @@ -113,9 +113,6 @@ public static Entity createFireBall(short targetLayer, Vector2 destination, Vect .addComponent(new ProjectileAnimationController()); // .addComponent(new SelfDestructOnHitComponent(PhysicsLayer.OBSTACLE)); -// projectile -// .getComponent(TextureRenderComponent.class).scaleEntity(); - return projectile; } diff --git a/source/core/src/main/com/csse3200/game/entities/factories/TowerFactory.java b/source/core/src/main/com/csse3200/game/entities/factories/TowerFactory.java index 93aa93794..51de34897 100644 --- a/source/core/src/main/com/csse3200/game/entities/factories/TowerFactory.java +++ b/source/core/src/main/com/csse3200/game/entities/factories/TowerFactory.java @@ -41,6 +41,7 @@ public class TowerFactory { private static final int TNT_TOWER_RANGE = 5; private static final int TNT_KNOCK_BACK_FORCE = 10; private static final String WALL_IMAGE = "images/towers/wallTower.png"; + private static final String RESOURCE_TOWER = "images/towers/mine_tower.png"; private static final String TURRET_ATLAS = "images/towers/turret01.atlas"; private static final String TNT_ATLAS = "images/towers/TNTTower.atlas"; private static final String DEFAULT_ANIM = "default"; @@ -57,7 +58,7 @@ public class TowerFactory { private static final float STOW_SPEED = 0.2f; private static final String FIRE_ANIM = "firing"; private static final float FIRE_SPEED = 0.25f; - private static final int INCOME_INTERVAL = 300; + private static final int INCOME_INTERVAL = 3; private static final int INCOME_TASK_PRIORITY = 1; private static final baseTowerConfigs configs = @@ -71,7 +72,7 @@ public static Entity createIncomeTower() { IncomeTowerConfig config = configs.income; // Create the CurrencyIncomeTask and add it to the AITaskComponent - CurrencyTask currencyTask = new CurrencyTask(INCOME_TASK_PRIORITY, 3); + CurrencyTask currencyTask = new CurrencyTask(INCOME_TASK_PRIORITY, INCOME_INTERVAL); int updatedInterval = 1; currencyTask.setInterval(updatedInterval); @@ -80,7 +81,7 @@ public static Entity createIncomeTower() { income .addComponent(new CombatStatsComponent(config.health, config.baseAttack)) .addComponent(new CostComponent(config.cost)) - .addComponent(new TextureRenderComponent("images/towers/mine_tower.png")) + .addComponent(new TextureRenderComponent(RESOURCE_TOWER)) .addComponent(aiTaskComponent); @@ -175,7 +176,7 @@ public static Entity createBaseTower() { // we're going to add more components later on Entity tower = new Entity() .addComponent(new ColliderComponent()) - .addComponent(new HitboxComponent().setLayer(PhysicsLayer.OBSTACLE)) // TODO: we might have to change the names of the layers + .addComponent(new HitboxComponent().setLayer(PhysicsLayer.TOWER)) // TODO: we might have to change the names of the layers .addComponent(new PhysicsComponent().setBodyType(BodyType.StaticBody)); return tower; diff --git a/source/core/src/main/com/csse3200/game/physics/PhysicsEngine.java b/source/core/src/main/com/csse3200/game/physics/PhysicsEngine.java index f4db690cb..882ad4935 100644 --- a/source/core/src/main/com/csse3200/game/physics/PhysicsEngine.java +++ b/source/core/src/main/com/csse3200/game/physics/PhysicsEngine.java @@ -45,7 +45,7 @@ public PhysicsEngine(World world, GameTime timeSource) { public void update() { // Check for deleted bodies and joints -// checkAndDeleteBodies(); + // checkAndDeleteBodies(); // Updating physics isn't as easy as triggering an update every frame. Each frame could take a // different amount of time to run, but physics simulations are only stable if computed at a @@ -91,16 +91,18 @@ public void checkAndDeleteBodies() { world.getBodies(bodies); // Check for bodies to be deleted - for(Body body : bodies) { - Entity entity = ((BodyUserData) body.getUserData()).entity; - - // If the entity is flagged for deletion, destroy the body before world.step() is called - if(entity.getFlagForDelete()) { - logger.debug("Destroying physics body {}", body); - ProjectileDestructors.destroyProjectile(entity); - - // Make sure not to delete the body twice - entity.setFlagForDelete(false); + for (Body body : bodies) { + // check for null values + if (body.getUserData() != null) { + Entity entity = ((BodyUserData) body.getUserData()).entity; + // If the entity is flagged for deletion, destroy the body before world.step() is called + if (entity.getFlagForDelete()) { + logger.debug("Destroying physics body {}", body); + ProjectileDestructors.destroyProjectile(entity); + + // Make sure not to delete the body twice + entity.setFlagForDelete(false); + } } } } diff --git a/source/core/src/main/com/csse3200/game/physics/PhysicsLayer.java b/source/core/src/main/com/csse3200/game/physics/PhysicsLayer.java index 637776166..73c5904aa 100644 --- a/source/core/src/main/com/csse3200/game/physics/PhysicsLayer.java +++ b/source/core/src/main/com/csse3200/game/physics/PhysicsLayer.java @@ -3,12 +3,15 @@ public class PhysicsLayer { public static final short NONE = 0; public static final short DEFAULT = (1 << 0); - public static final short PLAYER = (1 << 1); + public static final short ENGINEER = (1 << 1); // Terrain obstacle, e.g. trees public static final short OBSTACLE = (1 << 2); // NPC (Non-Playable Character) colliders public static final short NPC = (1 << 3); public static final short PROJECTILE = (1 << 4); + public static final short TOWER = (1 << 5); + + public static final short HUMANS = (1 << 1) | (1 << 5); public static final short ALL = ~0; public static boolean contains(short filterBits, short layer) { diff --git a/source/core/src/test/com/csse3200/game/components/player/HumanAnimationControllerTest.java b/source/core/src/test/com/csse3200/game/components/player/HumanAnimationControllerTest.java new file mode 100644 index 000000000..bf104bfbb --- /dev/null +++ b/source/core/src/test/com/csse3200/game/components/player/HumanAnimationControllerTest.java @@ -0,0 +1,50 @@ +package com.csse3200.game.components.player; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class HumanAnimationControllerTest { + + @BeforeEach + void setUp() { + } + + @AfterEach + void tearDown() { + } + + @Test + void create() { + } + + @Test + void animateIdleLeft() { + } + + @Test + void animateIdleRight() { + } + + @Test + void animateLeftWalk() { + } + + @Test + void animateRightWalk() { + } + + @Test + void animateFiring() { + } + + @Test + void animateHit() { + } + + @Test + void animateDeath() { + } +} \ No newline at end of file diff --git a/source/core/src/test/com/csse3200/game/components/tasks/human/HumanMovementTaskTest.java b/source/core/src/test/com/csse3200/game/components/tasks/human/HumanMovementTaskTest.java new file mode 100644 index 000000000..4deb83386 --- /dev/null +++ b/source/core/src/test/com/csse3200/game/components/tasks/human/HumanMovementTaskTest.java @@ -0,0 +1,34 @@ +package com.csse3200.game.components.tasks.human; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class HumanMovementTaskTest { + + @BeforeEach + void setUp() { + } + + @AfterEach + void tearDown() { + } + + @Test + void start() { + } + + @Test + void update() { + } + + @Test + void setTarget() { + } + + @Test + void stop() { + } +} \ No newline at end of file diff --git a/source/core/src/test/com/csse3200/game/components/tasks/human/HumanWanderTaskTest.java b/source/core/src/test/com/csse3200/game/components/tasks/human/HumanWanderTaskTest.java new file mode 100644 index 000000000..5a24deb7c --- /dev/null +++ b/source/core/src/test/com/csse3200/game/components/tasks/human/HumanWanderTaskTest.java @@ -0,0 +1,30 @@ +package com.csse3200.game.components.tasks.human; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class HumanWanderTaskTest { + + @BeforeEach + void setUp() { + } + + @AfterEach + void tearDown() { + } + + @Test + void getPriority() { + } + + @Test + void start() { + } + + @Test + void update() { + } +} \ No newline at end of file diff --git a/source/core/src/test/com/csse3200/game/entities/factories/EngineerFactoryTest.java b/source/core/src/test/com/csse3200/game/entities/factories/EngineerFactoryTest.java new file mode 100644 index 000000000..7be8641f4 --- /dev/null +++ b/source/core/src/test/com/csse3200/game/entities/factories/EngineerFactoryTest.java @@ -0,0 +1,186 @@ +package com.csse3200.game.entities.factories; + +import com.badlogic.gdx.audio.Sound; +import com.badlogic.gdx.graphics.Texture; +import com.badlogic.gdx.graphics.g2d.TextureAtlas; +import com.csse3200.game.ai.tasks.AITaskComponent; +import com.csse3200.game.components.CombatStatsComponent; +import com.csse3200.game.components.TouchAttackComponent; +import com.csse3200.game.components.player.HumanAnimationController; +import com.csse3200.game.entities.Entity; +import com.csse3200.game.extensions.GameExtension; +import com.csse3200.game.physics.PhysicsService; +import com.csse3200.game.physics.components.ColliderComponent; +import com.csse3200.game.physics.components.HitboxComponent; +import com.csse3200.game.physics.components.PhysicsComponent; +import com.csse3200.game.physics.components.PhysicsMovementComponent; +import com.csse3200.game.rendering.AnimationRenderComponent; +import com.csse3200.game.rendering.DebugRenderer; +import com.csse3200.game.rendering.RenderService; +import com.csse3200.game.services.GameTime; +import com.csse3200.game.services.ResourceService; +import com.csse3200.game.services.ServiceLocator; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + + +import java.util.ArrayList; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(GameExtension.class) +class EngineerFactoryTest { + + private String[] atlas = {"images/engineers/engineer.atlas"}; + private static final String[] sounds = { + "sounds/engineers/firing_auto.mp3", + "sounds/engineers/firing_single.mp3" + }; + + private String[] animations = { + "idle_right", + "walk_left", + "walk_right", + "walk_prep", + "prep", + "firing_auto", + "firing_single", + "hit", + "death" + };; + + @BeforeEach + void setUp() { + GameTime gameTime = mock(GameTime.class); + when(gameTime.getDeltaTime()).thenReturn(0.02f); + ServiceLocator.registerTimeSource(gameTime); + ServiceLocator.registerPhysicsService(new PhysicsService()); + RenderService render = new RenderService(); + render.setDebug(mock(DebugRenderer.class)); + ServiceLocator.registerRenderService(render); + ResourceService resourceService = new ResourceService(); + ServiceLocator.registerResourceService(resourceService); + resourceService.loadTextureAtlases(atlas); + resourceService.loadSounds(sounds); + resourceService.loadAll(); + } + + @Test + void createBaseHumanNPC() { + Entity human = EngineerFactory.createBaseHumanNPC(); + assertNotNull(human); + } + + @Test + void testCreateEngineer() { + // Check engineer exists after creation + Entity engineer = EngineerFactory.createEngineer(); + assertNotNull(engineer, "Engineer entity should not be null after creation"); + } + + @Test + void testDeleteEngineer() { + Entity engineer = EngineerFactory.createEngineer(); + assertFalse(engineer.getFlagForDelete(), "Engineer flagForDelete should be false on creation"); + engineer.setFlagForDelete(true); + assertTrue(engineer.getFlagForDelete(), "Engineer getflagForDelete should return true after being set"); + } + + @Test + void testEngineerPhysicsComponents() { + Entity engineer = EngineerFactory.createEngineer(); + assertNotNull(engineer.getComponent(PhysicsComponent.class), + "Engineer should have a PhysicsComponent"); + assertNotNull(engineer.getComponent(PhysicsMovementComponent.class), + "Engineer should have a PhysicsMovementComponent"); + } + + @Test + void testEngineerColliderAndHitboxComponents() { + Entity engineer = EngineerFactory.createEngineer(); + assertNotNull(engineer.getComponent(ColliderComponent.class), + "Engineer should have a ColliderComponent"); + assertNotNull(engineer.getComponent(HitboxComponent.class), + "Engineer should have a HitBoxComponent"); + } + + @Test + void testEngineerTouchAttackAndCombatStatsComponents() { + Entity engineer = EngineerFactory.createEngineer(); + assertNotNull(engineer.getComponent(TouchAttackComponent.class), + "Engineer should have a TouchAttackComponent"); + assertNotNull(engineer.getComponent(CombatStatsComponent.class), + "Engineer should have a CombatStatsComponent"); + } + + @Test + void testEngineerAnimationAndAITaskComponents() { + Entity engineer = EngineerFactory.createEngineer(); + assertNotNull(engineer.getComponent(HumanAnimationController.class), + "Engineer should have a HumanAnimationController"); + assertNotNull(engineer.getComponent(AITaskComponent.class), + "Engineer should have an AITaskComponent"); + assertNotNull(engineer.getComponent(AnimationRenderComponent.class), + "Engineer should have an AnimationRenderComponent"); + } + + @Test + void testEngineerAnimations() { + Entity engineer = EngineerFactory.createEngineer(); + for (String animation : this.animations) { + assertTrue(engineer.getComponent(AnimationRenderComponent.class).hasAnimation(animation), + ("Engineer AnimationRenderComponent should contain animation [" + animation + "]")); + } + for (String animation : this.animations) { + engineer.getComponent(AnimationRenderComponent.class).startAnimation(animation); + assert(Objects.equals(engineer.getComponent(AnimationRenderComponent.class).getCurrentAnimation(), animation)); + } + } + + @Test + void testEngineerConfig() { + Entity engineer = EngineerFactory.createEngineer(); + assert(engineer.getComponent(CombatStatsComponent.class).getHealth() == 100); + assert(engineer.getComponent(CombatStatsComponent.class).getBaseAttack() == 5); + } + + /** + * Adapted from TowerFactoryTest testAttackerCollisionWithWall by MohamadDab11 + */ + @Test + void testEngineerCollisions() { + Entity engineer = EngineerFactory.createEngineer(); + + Entity attacker = createAttacker(engineer.getComponent(HitboxComponent.class).getLayer()); + + engineer.setPosition(10f,10f); + attacker.setPosition(10f,10f); + engineer.create(); + + assertEquals(100, engineer.getComponent(CombatStatsComponent.class).getHealth()); + + ServiceLocator.getPhysicsService().getPhysics().update(); + + assertEquals(90, engineer.getComponent(CombatStatsComponent.class).getHealth()); + + } + + Entity createAttacker(short targetLayer) { + Entity entity = + new Entity() + .addComponent(new TouchAttackComponent(targetLayer)) + .addComponent(new CombatStatsComponent(0, 10)) + .addComponent(new PhysicsComponent()) + .addComponent(new HitboxComponent()); + entity.create(); + return entity; + } +} diff --git a/source/core/src/test/com/csse3200/game/entities/factories/ProjectileFactoryTest.java b/source/core/src/test/com/csse3200/game/entities/factories/ProjectileFactoryTest.java index 040278787..925f44e03 100644 --- a/source/core/src/test/com/csse3200/game/entities/factories/ProjectileFactoryTest.java +++ b/source/core/src/test/com/csse3200/game/entities/factories/ProjectileFactoryTest.java @@ -56,7 +56,7 @@ public void setUp() { // ServiceLocator.getResourceService() // .getAsset("images/projectiles/basic_projectile.atlas", TextureAtlas.class); Vector2 destination = new Vector2(0.1f, 0.1f); - short targetLayer = PhysicsLayer.PLAYER; + short targetLayer = PhysicsLayer.HUMANS; Vector2 speed = new Vector2(2f, 2f); projectile = ProjectileFactory.createBaseProjectile(targetLayer, destination, speed); }