-
Notifications
You must be signed in to change notification settings - Fork 4
Engineer Combat Task
The EngineerCombatTask
implements the entire shooting functionality of Engineers and enables them to detect and attack enemy mobs. It scans for mobs every second and takes a high priority if a mob is detected.
It is ran by the HumanWanderTask and works in coordination with HumanWaitTask and HumanMovementTask to give the Engineers a fighting chance as a last straw against enemies.
Responsibilities of this task include switching through multiple Engineer states, playing the required animations and sounds, and spawning Projectiles in the enemy's direction.
This task is instantiated by HumanWanderTask through the EngineerCombatTask(float maxRange)
constructor, which takes the maximum shooting range of the Engineer entity as a parameter.
The UML for this class has been detailed in the HumanWanderTask's wiki page.
The following task actions are called based on the game area's current state inside the overridden update()
method, which is handled by the task runner implemented in AITaskComponent
.
- Starting the EngineerCombatTask:
start()
- Stopping the EngineerCombatTask:
stop()
- Updating the EngineerCombatTask:
update()
- Retrieving the closest target:
fetchTarget()
- Getting the task priority:
getPriority()
- Entering the combat state:
combatState()
This method is used to detect any mobs within 5 lanes of the Engineer on either end of the map.
To detect a mob in a certain lane, the inbuilt physics.raycast()
method is used between two vector positions on the map to detect any entities within a certain layer.
If detected, the position of the enemy mob is stored in a list. After the detection sequence finishes running, the method returns true or false depending on whether one or more targets have been found.
public boolean isTargetVisible() {
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();
}
This method runs a state machine for the Engineer entity to enable switching between idle and firing states.
If the Engineer is idle, i.e in the IDLE_RIGHT
(Right facing) state, the state machine detects for any targets using the isTargetVisible()
method. If a target is found, the state is updated to FIRING
and the firing animation is played for the Engineer entity.
If the Engineer is in the FIRING
state, the state machine first detects whether all targets have been downed. If there is no remaining target, the state is switched back to IDLE_RIGHT
with the corresponding animation. If there are existing targets, the FIRING
event is triggered to play the animation, and a new Projectile
is created towards the enemy's direction.
The state machine also checks whether the Engineer has fired a certain number of rounds, defined by weaponCapacity
. Once this number is reached, the state machine will wait out exactly one update sequence (1 second in real time) to make it seem like the Engineer is reloading its weapon. Once this period is over, the shotsFired
variable is reset to allow the engineer to fire consistently again.
public void updateEngineerState() {
// configure engineer state depending on target visibility
switch (engineerState) {
case IDLE_RIGHT -> {
// targets detected in idle mode - start deployment
if (isTargetVisible()) {
combatState();
}
}
case FIRING -> {
// targets gone - stop firing
if (!isTargetVisible()) {
owner.getEntity().getEvents().trigger(IDLE_RIGHT);
engineerState = STATE.IDLE_RIGHT;
} else {
if (shotsFired <= weaponCapacity) {
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.createEngineerBullet(PhysicsLayer.NPC,
new Vector2(100, owner.getEntity().getPosition().y),
new Vector2(4f, 4f));
newProjectile.setScale(0.8f, 0.8f);
newProjectile.setPosition((float) (owner.getEntity().getPosition().x + 0.3),
(float) (owner.getEntity().getPosition().y + 0.15));
ServiceLocator.getEntityService().register(newProjectile);
shotsFired ++;
reloadTime = timeSource.getTime();
} else {
// engineer needs to reload
if (reloadTime < timeSource.getTime()) {
// engineer has reloaded
shotsFired = 0;
reloadTime = timeSource.getTime();
}
}
}
}
}
}
The following methods are used to optimize the primary methods and remove code redundancy.
This method starts the EngineerCombatTask
, stores the position of the 'owner' Engineer entity, and triggers the idle animation for the Engineer.
This method executes the parent's stop method to stop the current task.
This method calls updateEngineerState()
to change the Engineer's state every 1000 milliseconds (1 second).
This method is responsible for returning the nearest target's position from the list of detected targets. To implement this logic, it uses a placeholder integer to store the distance between the Engineer and Mob lanes, and iterates through the stored positions to find the one closest to the Engineer.
This method returns the task's priority, thus deciding if it should be run before any other game tasks or not. It returns the default priority (3) if any enemies are visible, otherwise 0.
This method triggers a FIRING
event, of a string value "firingSingleStart"
. This trigger runs the required firing animation in the Engineer entity that owns this task. The method also changes the Engineer's state to enable the firing routine.