package com.amaya.game.entities;
import com.amaya.game.Spacefish;
import com.amaya.game.entities.environment.Alien;
import com.amaya.game.entities.environment.Asteroid;
import com.amaya.game.entities.environment.Drop;
import com.badlogic.gdx.Gdx;
import java.util.ArrayList;
import java.util.List;
/** Describe the Level behavior. */
public class Level {
/* [ CONSTANTS ] ========================================================================================================================================= */
/** Logging tag. */
public static final String TAG = Spacefish.LOG_TAG;
/** Minimalistic score of the level. */
public static final int MINIMUM_LEVEL_POINTS = Alien.GREEN.getPoints() + Alien.ORANGE.getPoints() + Alien.YELLOW.getPoints();
/** cheapest alien. */
public static final int MINIMUM_ALIEN_POINTS = Alien.GREEN.getPoints();
/* [ MEMBERS ] =========================================================================================================================================== */
/** Level total time. */
private final float mTotalTime;
/** Level total drops. */
private final int mTotalDrops;
/** Initial proportions of Asteroids. */
private final int[] mAsteroids;
/** Initial proportions of Aliens. */
private final int[] mAliens;
/* [ RUNTIME ] =========================================================================================================================================== */
/** Visible asteroids. */
public final List<Asteroid> Asteroids = new ArrayList<Asteroid>();
/** Visible aliens. */
public final List<Alien> Aliens = new ArrayList<Alien>();
/** Quantity of totally done drops. */
private int mAlreadyDropped;
/** Quantity of available drops of each type. */
private final List<Integer> mAvailable = new ArrayList<Integer>();
/** Level status - State machine. */
private KnownStates mState = KnownStates.DONE;
/* [ CONSTRUCTORS ] ====================================================================================================================================== */
/**
* Construct level with set of important configuration parameters.
*
* @param time level total time in seconds.
* @param asteroids amount of asteroids.
* @param aliens amount of aliens.
*/
private Level(final float time, final int[] asteroids, final int[] aliens) {
// sum 'entities'
int total = 0;
for (int i : asteroids) total += i;
for (int i : aliens) total += i;
// convert second to millis
mTotalTime = toMillis(time);
mTotalDrops = total;
mAsteroids = asteroids;
mAliens = aliens;
reset();
}
/* [ API METHODS ] ======================================================================================================================================= */
/** Reset level runtime state to the initial one. */
public void reset() {
mState = KnownStates.RUNNING;
mAlreadyDropped = 0;
mAvailable.clear();
Asteroids.clear();
Aliens.clear();
for (int i : mAsteroids) {
mAvailable.add(i);
}
for (int j : mAliens) {
mAvailable.add(j);
}
}
/**
* Based on delta and current time from level beginning calculate collection of objects to show.
*
* @param gameTime - total game time in seconds
* @param delta - time between calls in seconds
* @return Newly created objects. All those objects are accessible over {@link #Asteroids} and {@link #Aliens} collections.
*/
public List<Drop> update(final float gameTime, final float delta) {
final List<Drop> results = new ArrayList<Drop>();
// game pass own total time
if (KnownStates.DONE == mState) {
return results;
}
// update current State of the level
if (toMillis(gameTime) > mTotalTime) {
mState = KnownStates.DONE;
if (Spacefish.Debug.LEVEL_STATE)
Gdx.app.log(TAG, "Level State changed to: " + mState);
}
// quantity of drops per millis
final double ratio = (double) mTotalDrops / mTotalTime;
final double amount = Math.min(mTotalDrops, ratio * toMillis(gameTime));
final int toDrop = (int) (amount - mAlreadyDropped);
// generate required quantity of entities
for (int i = 0; i < toDrop; i++) {
int index = Spacefish.randomInt(mAsteroids.length + mAliens.length);
// resolve empty slots
if (0 == mAvailable.get(index)) {
index = findNearest(index);
}
if (0 <= index) {
// reduce the value
mAvailable.set(index, mAvailable.get(index) - 1);
results.add(newDropInstance(index));
}
}
mAlreadyDropped += toDrop;
// update main collections
for (Drop drop : results) {
if (drop instanceof Alien) {
Aliens.add((Alien) drop);
} else if (drop instanceof Asteroid) {
Asteroids.add((Asteroid) drop);
}
}
if (Spacefish.Debug.LEVEL_DROPS && 0 < results.size())
Gdx.app.log(TAG, "[level] new drops: " + results.size());
return results;
}
/* [ GETTER / SETTER METHODS ] =========================================================================================================================== */
/** get current state of the game level. {@link KnownStates} */
public KnownStates getState() {
return mState;
}
/* [ STATIC METHODS ] ==================================================================================================================================== */
/** generate random asteroids configuration for level. */
private static int[] randomAsteroid(final int total) {
final int withSound = Spacefish.randomInt(total / 2); // up to 50%
final int withDeath = Spacefish.randomInt((int) (total * 0.4)); // up to 40%
return asteroids(withSound, total - withSound - withDeath, withDeath);
}
/** pack properly asteroids configuration into array. */
private static int[] asteroids(final int withSound, final int withSpeed, final int withDeath) {
if (Spacefish.Debug.LEVEL_NUMBERS)
Gdx.app.log(TAG, "[level] asteroids - green: " + withSound + ", yellow: " + withSpeed + ", orange: " + withDeath);
return new int[]{withSound, withSpeed, withDeath};
}
/** generate random aliens configuration for level. */
private static int[] randomAliens(final int totalScore) {
int green = 0, yellow = 0, orange = 0;
int total = Math.max(totalScore, Level.MINIMUM_LEVEL_POINTS);
int countdown = 1 + total / MINIMUM_ALIEN_POINTS;
// NOTE: a little dummy algorithm of fill. We just need something simple to randomize levels.
while (total > 0 && countdown > 0) {
switch (Spacefish.randomInt(3)) {
case 2:
if (total - Alien.ORANGE.getPoints() > 0) {
orange++;
total -= Alien.ORANGE.getPoints();
break;
}
// goto to next SWITCH/CASE, fall to cheaper alien
case 1:
if (total - Alien.YELLOW.getPoints() > 0) {
yellow++;
total -= Alien.YELLOW.getPoints();
break;
}
// goto to next SWITCH/CASE, fall to cheaper alien
default:
if (total - Alien.GREEN.getPoints() > 0) {
green++;
total -= Alien.GREEN.getPoints();
}
break;
}
// potential infinite loop breaker
countdown--;
}
return aliens(green, yellow, orange);
}
/** pack properly aliens configuration into array. */
private static int[] aliens(final int green, final int yellow, final int orange) {
if (Spacefish.Debug.LEVEL_NUMBERS)
Gdx.app.log(TAG, "[level] aliens - green: " + green + ", yellow: " + yellow + ", orange: " + orange);
return new int[]{green, yellow, orange};
}
/** Utility. convert seconds to millis. */
private static float toMillis(final float seconds) {
return seconds * 1000 /* millis */;
}
/* [ IMPLEMENTATION & HELPERS ] ========================================================================================================================== */
private int findNearest(int index) {
boolean found = false;
int oldIndex = index;
// failure, all drops of specified type are already in game
for (; index < mAvailable.size(); index++) {
if ((found = (0 < mAvailable.get(index)))) {
break;
}
}
if (!found) {
index = oldIndex;
for (; index >= 0; index--) {
if (0 < mAvailable.get(index)) {
break;
}
}
}
return index;
}
/** based on configuration index/position create new instance of game entity. */
private Drop newDropInstance(final int index) {
final int xLimit = (int) (Spacefish.Dimensions.VIRTUAL_SCREEN_WIDTH - Spacefish.Dimensions.ICON_WIDTH - Spacefish.Dimensions.SPACE);
float xOffset = Spacefish.Dimensions.SPACE + Spacefish.randomInt(xLimit);
// TODO: refactor 'magic numbers'
switch (index) {
case 0:
return Asteroid.sound(xOffset);
case 1:
return Asteroid.speed(xOffset);
case 2:
return Asteroid.death(xOffset);
case 3:
return Alien.green(xOffset);
case 4:
return Alien.yellow(xOffset);
default:
return Alien.orange(xOffset);
}
}
/* [ NESTED DECLARATIONS ] =============================================================================================================================== */
/** Level known states: Running and Done. */
public static enum KnownStates {
RUNNING,
DONE
}
/** Level builder. 'builder' pattern. */
public static class Builder {
private int[] mAsteroids = new int[Asteroid.KnownAsteroids.values().length];
private int[] mAliens = new int[Alien.KnownAliens.values().length];
private float mTotalTime;
public Builder setAliensRaw(int[] aliens) {
System.arraycopy(aliens, 0, mAliens, 0, mAliens.length);
return this;
}
public Builder setAsteroidsRaw(int[] asteroids) {
System.arraycopy(asteroids, 0, mAsteroids, 0, mAsteroids.length);
return this;
}
public Builder setTotalTime(final float time) {
mTotalTime = time;
return this;
}
public Builder randomize() {
setAsteroidsRaw(randomAsteroid((int) mTotalTime));
setAliensRaw(randomAliens(MINIMUM_LEVEL_POINTS * (int) mTotalTime / 4));
return this;
}
public Level build() {
return new Level(mTotalTime, mAsteroids, mAliens);
}
}
}