CustomerAISystem.java
package com.devcharles.piazzapanic.componentsystems;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import com.badlogic.ashley.core.Engine;
import com.badlogic.ashley.core.Entity;
import com.badlogic.ashley.core.Family;
import com.badlogic.ashley.systems.IteratingSystem;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.ai.steer.Proximity;
import com.badlogic.gdx.ai.steer.behaviors.Arrive;
import com.badlogic.gdx.ai.steer.behaviors.CollisionAvoidance;
import com.badlogic.gdx.ai.steer.behaviors.PrioritySteering;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.World;
import com.devcharles.piazzapanic.GameScreen;
import com.devcharles.piazzapanic.components.*;
import com.devcharles.piazzapanic.components.FoodComponent.FoodType;
import com.devcharles.piazzapanic.scene2d.Hud;
import com.devcharles.piazzapanic.utility.EntityFactory;
import com.devcharles.piazzapanic.utility.Difficulty;
import com.devcharles.piazzapanic.utility.GdxTimer;
import com.devcharles.piazzapanic.utility.Mappers;
import com.devcharles.piazzapanic.utility.box2d.Box2dLocation;
import com.devcharles.piazzapanic.utility.box2d.Box2dRadiusProximity;
/**
* Controls the AI Customers, creates orders.
*/
public class CustomerAISystem extends IteratingSystem {
private final Map<Integer, Box2dLocation> objectives;
private final Map<Integer, Boolean> objectiveTaken;
private final World world;
private GdxTimer spawnTimer;
private final EntityFactory factory;
private int numOfCustomerTotal = 0;
private final Hud hud;
private final Difficulty difficulty;
private final Integer[] reputationPoints;
private final Float[] tillBalance;
private final Integer[] customersServed;
private int CUSTOMER;
private boolean firstSpawn = true;
private GameScreen gameScreen;
private Integer timeFrozen = 30000;
private Integer DoubleRepCounter = 30000;
// List of customers, on removal we move the other customers up a place
// (queueing).
private final ArrayList<Entity> customers = new ArrayList<Entity>() {
@Override
public boolean remove(Object o) {
for (Entity entity : customers) {
if (entity != o) {
AIAgentComponent aiAgent = Mappers.aiAgent.get(entity);
if (aiAgent.currentObjective - 1 >= 0) {
if (!objectiveTaken.get(aiAgent.currentObjective - 1)) {
makeItGoThere(aiAgent, aiAgent.currentObjective - 1);
}
}
}
}
return super.remove(o);
}
};
/**
* Instantiate the system.
*
* @param objectives Map of objectives available
* @param world Box2D {@link World} for AI and disposing of customer
* entities.
* @param factory {@link EntityFactory} for creating new customers
* @param hud {@link HUD} for updating orders, reputation
* @param reputationPoints array-wrapped integer reputation passed by-reference
* See {@link Hud}
*/
public CustomerAISystem(Map<Integer, Box2dLocation> objectives, World world, EntityFactory factory, Hud hud,
Integer[] reputationPoints, int customer, Difficulty difficulty, Float[] tillBalance,
Integer[] customersServed, GameScreen gameScreen) {
super(Family.all(AIAgentComponent.class, CustomerComponent.class).get());
this.CUSTOMER = customer;
this.hud = hud;
this.objectives = objectives;
this.objectiveTaken = new HashMap<Integer, Boolean>();
this.reputationPoints = reputationPoints;
this.difficulty = difficulty;
this.tillBalance = tillBalance;
this.customersServed = customersServed;
this.gameScreen = gameScreen;
// Use a reference to the world to destroy box2d bodies when despawning
// customers
this.world = world;
this.factory = factory;
this.spawnTimer = new GdxTimer(difficulty.getSpawnFrequency(), true, true);
// spawnTimer.start();
}
@Override
public void update(float deltaTime) {
if (firstSpawn || (spawnTimer.tick(deltaTime) && CUSTOMER > 0)) {
firstSpawn = false;
// Only add a customer is there is space in the queue and there are customers
// still remaining.
// The number of customers in the queue cannot be more than the number of
// customers remaining.
// There are 5 queue spots on the map.
if (numOfCustomerTotal < 5 && !(numOfCustomerTotal + 1 > CUSTOMER)) {
// The first customer will arrive alone but after that there is a chance
// customers
// will arrive in groups of two or three.
int customersToSpawn = getRandomCustomerGroupSize();
if (numOfCustomerTotal + customersToSpawn > 5 || firstSpawn) {
customersToSpawn = 1;
}
Vector2 pos = new Vector2(objectives.get(-2).getPosition());
// Each customer in group will have spawn point offset to stop entity overlap
// and queue blocking.
for (int i = 0; i < customersToSpawn; i++) {
pos.x += 0.5;
Entity newCustomer = factory.createCustomer(pos);
customers.add(newCustomer);
numOfCustomerTotal++;
Mappers.customer.get(newCustomer).timer.start();
processEntity(newCustomer, deltaTime);
}
Gdx.app.log("Info", customersToSpawn + " customer(s) have arrived.");
}
// If endless mode then decrease customer spawn frequency by one second every
// time a customer is served.
// Result is customers will arrive more often over time in endless mode.
// If the time freeze powerup has been purchased pause the spawn timer until the
// powerup time has passed.
if (firstSpawn == false && difficulty != Difficulty.SCENARIO) {
if (gameScreen.TimeFreeze) {
if (timeFrozen <= 0) {
spawnTimer.start();
timeFrozen = 30000;
}
spawnTimer.stop();
timeFrozen = timeFrozen - 25;
}
spawnTimer = new GdxTimer((difficulty.getSpawnFrequency() - ((999 - CUSTOMER) * 1000)), true, true);
Gdx.app.log("Info",
"Spawn frequency is now " + (difficulty.getSpawnFrequency() - ((999 - CUSTOMER) * 1000)));
}
}
FoodType[] orders = new FoodType[customers.size()];
int[] orderTimes = new int[customers.size()];
int i = 0;
for (Entity customer : customers) {
orders[i] = Mappers.customer.get(customer).order;
orderTimes[i] = (120000 - Mappers.customer.get(customer).timer.getElapsed()) / 1000;
i++;
}
if ((!hud.gameOver && customers.size() == 0 && CUSTOMER == 0) || reputationPoints[0] == 0) {
hud.triggerGameOver = true;
}
super.update(deltaTime);
hud.updateOrders(orders, orderTimes);
}
@Override
protected void processEntity(Entity entity, float deltaTime) {
AIAgentComponent aiAgent = Mappers.aiAgent.get(entity);
CustomerComponent customer = Mappers.customer.get(entity);
TransformComponent transform = Mappers.transform.get(entity);
if (customer.food != null && transform.position.x >= (objectives.get(-1).getPosition().x - 2)) {
destroyCustomer(entity);
return;
}
if (aiAgent.steeringBody.getSteeringBehavior() == null) {
Gdx.app.log("customer", "this customer is moving to objective" + (customers.size() - 1));
makeItGoThere(aiAgent, customers.size() - 1);
}
aiAgent.steeringBody.update(deltaTime);
// Lower reputation points only in endless if they have waited longer than time
// alloted.
if (customer.timer.tick(deltaTime)) {
if (reputationPoints[0] > 0) {
if (difficulty != Difficulty.SCENARIO) {
reputationPoints[0]--;
}
}
customer.timer.stop();
}
// Remove a customer upon activation of the BinACustomer powerup.
if (gameScreen.BinACustomer) {
if (CUSTOMER == 0) {
gameScreen.BinOff();
}
fulfillOrder(entity, customer, entity, gameScreen.BinACustomer, gameScreen.DoubleRep);
gameScreen.BinOff();
}
// Freeze the customer timers for all customers whilst the powerup is active.
if (gameScreen.TimeFreeze) {
timeFreeze(customer);
}
// Decrease the timer for the double money powerup.
if (gameScreen.DoubleRep) {
DoubleRepCounter -= 17;
if (DoubleRepCounter <= 0) {
gameScreen.DoubleOff();
DoubleRepCounter = 30000;
}
}
if (customer.interactingCook != null) {
PlayerComponent player = Mappers.player.get(customer.interactingCook);
// In order, check if the player is touching and pressing
// the correct key to interact with the customer.
if (player == null || !player.putDown) {
return;
}
player.putDown = false;
ControllableComponent cook = Mappers.controllable.get(customer.interactingCook);
if (cook.currentFood.isEmpty()) {
return;
}
Entity food = cook.currentFood.pop();
if (Mappers.food.get(food).type == customer.order) {
// Fulfill order
Gdx.app.log("Order success", customer.order.name());
fulfillOrder(entity, customer, food, gameScreen.BinACustomer, gameScreen.DoubleRep);
gameScreen.audio.playThanks();
} else {
getEngine().removeEntity(food);
gameScreen.audio.playSigh();
}
}
}
/**
*
* @param customer for freezeing the customers order timer whilst the powerup is
* active
*/
private void timeFreeze(CustomerComponent customer) {
customer.timer.stop();
if (timeFrozen <= 0) {
customer.timer.start();
gameScreen.TimeOff();
}
}
/**
* Remove the customer from the {@link World} and remove their entity.
*/
private void destroyCustomer(Entity customer) {
getEngine().removeEntity(Mappers.customer.get(customer).food);
world.destroyBody(Mappers.b2body.get(customer).body);
getEngine().removeEntity(customer);
}
/**
* Give the customer an objetive to go to.
*
* @param locationID and id from {@link CustomerAISystem.objectives}
*/
private void makeItGoThere(AIAgentComponent aiAgent, int locationID) {
objectiveTaken.put(aiAgent.currentObjective, false);
Box2dLocation there = objectives.get(locationID);
Arrive<Vector2> arrive = new Arrive<Vector2>(aiAgent.steeringBody)
.setTimeToTarget(0.1f)
.setArrivalTolerance(0.25f)
.setDecelerationRadius(2)
.setTarget(there);
Proximity<Vector2> proximity = new Box2dRadiusProximity(aiAgent.steeringBody, world, 1f);
CollisionAvoidance<Vector2> collisionAvoidance = new CollisionAvoidance<Vector2>(
aiAgent.steeringBody, proximity);
PrioritySteering<Vector2> prioritySteering = new PrioritySteering<Vector2>(aiAgent.steeringBody)
.add(collisionAvoidance)
.add(arrive);
aiAgent.steeringBody.setSteeringBehavior(prioritySteering);
aiAgent.currentObjective = locationID;
objectiveTaken.put(aiAgent.currentObjective, true);
if (locationID == -1) {
aiAgent.steeringBody.setOrientation(0);
} else {
aiAgent.steeringBody.setOrientation((float) (1.5f * Math.PI));
}
}
/**
* Give customer food, send them away and remove the order from the list
*/
private void fulfillOrder(Entity entity, CustomerComponent customer, Entity foodEntity, Boolean BinACustomer,
Boolean DoubleRep) {
if (BinACustomer) {
getEngine().removeEntity(entity);
customer.timer.stop();
customer.timer.reset();
customer.order = null;
customers.remove(entity);
numOfCustomerTotal--;
CUSTOMER--;
}
Engine engine = getEngine();
if (customer.order != null) {
float customerTip = getRandomCustomerTip(customer.order.getPrice());
if (customerTip > 0) {
hud.displayInfoMessage("Customer has tipped $ " + Float.toString(customerTip));
}
// if double rep powerup is active double the price of the order
if (DoubleRep) {
float doublePrice = customer.order.getPrice() * 2f;
tillBalance[0] += doublePrice + customerTip;
}
tillBalance[0] += customer.order.getPrice() + customerTip;
customer.order = null;
ItemComponent heldItem = engine.createComponent(ItemComponent.class);
heldItem.holderTransform = Mappers.transform.get(entity);
foodEntity.add(heldItem);
customer.food = foodEntity;
AIAgentComponent aiAgent = Mappers.aiAgent.get(entity);
makeItGoThere(aiAgent, -1);
customer.timer.stop();
customer.timer.reset();
customers.remove(entity);
numOfCustomerTotal--;
CUSTOMER--;
customersServed[0]++;
}
}
/**
* Calculates how many customers should arrive at once.
* Weighted so that customers arrive alone most of the time.
*
* @return groupSize
*/
private int getRandomCustomerGroupSize() {
if (difficulty == Difficulty.SCENARIO) {
return 1;
}
double x = Math.random();
if (x < 0.7) {
return 1;
}
if (x >= 0.7 && x < 0.9) {
return 2;
}
if (x >= 0.9) {
return 3;
}
return 1;
}
/**
* Calculates the amount a customer will tip.
* Customers will tip a random amount up to the price of their dish and
* will do so 20% of the time.
* In scenario mode there are no tips.
*
* @param dishPrice The price of the customer's meal which is used to determine
* their tip.
* @return The calculated tip.
*/
private int getRandomCustomerTip(float dishPrice) {
if (difficulty == Difficulty.SCENARIO) {
return 0;
}
double x = Math.random();
if (x > 0.8) {
x = (1 + Math.random());
return Math.round((float) dishPrice * (float) x);
}
return 0;
}
}