Rogue: Savage Rats, a retro-themed dungeon crawler
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
rogue-savage-rats/src/mightypork/rogue/world/level/Level.java

693 lines
13 KiB

package mightypork.rogue.world.level;
import java.io.IOException;
import java.util.*;
import mightypork.rogue.world.World;
import mightypork.rogue.world.entity.Entities;
import mightypork.rogue.world.entity.Entity;
import mightypork.rogue.world.entity.EntityType;
import mightypork.rogue.world.entity.impl.EntityPlayer;
import mightypork.rogue.world.item.Item;
import mightypork.rogue.world.tile.Tile;
import mightypork.rogue.world.tile.TileModel;
import mightypork.rogue.world.tile.Tiles;
import mightypork.utils.eventbus.EventBus;
import mightypork.utils.eventbus.clients.DelegatingClient;
import mightypork.utils.eventbus.clients.ToggleableClient;
import mightypork.utils.interfaces.Updateable;
import mightypork.utils.ion.IonBinary;
import mightypork.utils.ion.IonDataBundle;
import mightypork.utils.ion.IonInput;
import mightypork.utils.ion.IonOutput;
import mightypork.utils.logging.Log;
import mightypork.utils.math.Calc;
import mightypork.utils.math.algo.Coord;
import mightypork.utils.math.algo.Move;
import mightypork.utils.math.algo.Moves;
import mightypork.utils.math.algo.floodfill.FloodFill;
import mightypork.utils.math.constraints.vect.Vect;
import mightypork.utils.math.noise.NoiseGen;
/**
* One level of the dungeon
*
* @author Ondřej Hruška (MightyPork)
*/
public class Level implements Updateable, DelegatingClient, ToggleableClient, IonBinary {
private final FloodFill exploreFiller = new FloodFill() {
@Override
public List<Move> getSpreadSides()
{
return Moves.ALL_SIDES;
}
@Override
public double getMaxDistance()
{
return 5.4;
}
@Override
public boolean canSpreadFrom(Coord pos)
{
final Tile t = getTile(pos);
return t.isWalkable() && !t.isDoor();
}
@Override
public boolean canEnter(Coord pos)
{
final Tile t = getTile(pos);
return !t.isNull();
}
@Override
public boolean forceSpreadStart()
{
return true;
}
};
public static final int ION_MARK = 53;
private static final Comparator<Entity> ENTITY_RENDER_CMP = new EntityRenderComparator();
private Coord size = Coord.zero();
private World world;
private Coord enterPoint = Coord.zero();
private Coord exitPoint = Coord.zero();
/** Array of tiles [y][x] */
private Tile[][] tiles;
private final Map<Integer, Entity> entityMap = new HashMap<>();
private final List<Entity> entityList = new LinkedList<>();
private int playerCount = 0;
/** Level seed (used for generation and tile variation) */
public long seed;
private transient NoiseGen noiseGen;
private double timeSinceLastEntitySort;
public Level() {
}
public Level(int width, int height) {
size.setTo(width, height);
buildArray();
}
private void buildArray()
{
this.tiles = new Tile[size.y][size.x];
}
/**
* Fill whole map with tile type
*
* @param model tile model
*/
public void fill(TileModel model)
{
for (final Coord c = Coord.zero(); c.x < size.x; c.x++) {
for (c.y = 0; c.y < size.y; c.y++) {
setTile(c, model.createTile());
}
}
}
/**
* Ge tile at X,Y
*
* @param pos
* @return tile
*/
public final Tile getTile(Coord pos)
{
if (!pos.isInRange(0, 0, size.x - 1, size.y - 1)) return Tiles.NULL.createTile(); // out of range
return tiles[pos.y][pos.x];
}
/**
* Set tile at pos
*
* @param pos tile pos
* @param tile the tile instance to set
*/
public final void setTile(Coord pos, Tile tile)
{
if (!pos.isInRange(0, 0, size.x - 1, size.y - 1)) {
Log.w("Invalid tile coord to set: " + pos + ", map size: " + size);
return; // out of range
}
tiles[pos.y][pos.x] = tile;
// assign level (tile logic may need it)
tile.setLevel(this);
}
/**
* @return map width in tiles
*/
public final int getWidth()
{
return size.x;
}
/**
* @return map height in tiles
*/
public final int getHeight()
{
return size.y;
}
/**
* Set level seed (used for visuals; the seed used for generation)
*
* @param seed seed
*/
public void setSeed(long seed)
{
this.seed = seed;
}
/**
* @return map seed
*/
public long getSeed()
{
return seed;
}
@Override
public void load(IonInput in) throws IOException
{
// -- metadata --
final IonDataBundle ib = in.readBundle();
seed = ib.get("seed", 0L);
size = ib.get("size");
enterPoint = ib.get("enter_point", enterPoint);
exitPoint = ib.get("exit_point", exitPoint);
// -- binary data --
// load tiles
buildArray();
for (final Coord c = Coord.zero(); c.x < size.x; c.x++) {
for (c.y = 0; c.y < size.y; c.y++) {
setTile(c, Tiles.loadTile(in));
}
}
// load entities
Entities.loadEntities(in, entityList);
// prepare entities
for (final Entity ent : entityList) {
ent.setLevel(this);
occupyTile(ent.getCoord());
entityMap.put(ent.getEntityId(), ent);
if (ent instanceof EntityPlayer) {
playerCount++;
}
}
}
@Override
public void save(IonOutput out) throws IOException
{
// -- metadata --
final IonDataBundle ib = new IonDataBundle();
ib.put("seed", seed);
ib.put("size", size);
ib.put("enter_point", enterPoint);
ib.put("exit_point", exitPoint);
out.writeBundle(ib);
// -- binary data --
// tiles
for (final Coord c = Coord.zero(); c.x < size.x; c.x++) {
for (c.y = 0; c.y < size.y; c.y++) {
Tiles.saveTile(out, getTile(c));
}
}
Entities.saveEntities(out, entityList);
}
@Override
public void update(double delta)
{
timeSinceLastEntitySort += delta;
if (timeSinceLastEntitySort > 0.2) {
Collections.sort(entityList, ENTITY_RENDER_CMP);
timeSinceLastEntitySort = 0;
}
// just update them all
for (final Coord c = Coord.zero(); c.x < size.x; c.x++) {
for (c.y = 0; c.y < size.y; c.y++) {
getTile(c).updateTile(delta);
}
}
final List<Entity> toRemove = new ArrayList<>();
for (final Entity e : entityList) {
if (e.isDead() && e.canRemoveCorpse()) toRemove.add(e);
}
for (final Entity e : toRemove) {
removeEntity(e);
e.onCorpseRemoved();
}
}
/**
* @return level-specific noise generator
*/
public NoiseGen getNoiseGen()
{
if (noiseGen == null) {
noiseGen = new NoiseGen(0.2, 0, 0.5, 1, seed);
}
return noiseGen;
}
/**
* Get entity by ID
*
* @param eid entity ID
* @return the entity, or null
*/
public Entity getEntity(int eid)
{
return entityMap.get(eid);
}
/**
* Try to add entity at given pos, then near the pos.
*
* @param entity the entity
* @param pos pos
* @return true if added
*/
public boolean addEntityNear(Entity entity, Coord pos)
{
if (addEntity(entity, pos)) return true;
// closer
for (int i = 0; i < 20; i++) {
final Coord c = pos.add(Calc.randInt(-1, 1), Calc.randInt(-1, 1));
if (addEntity(entity, c)) return true;
}
// further
for (int i = 0; i < 20; i++) {
final Coord c = pos.add(Calc.randInt(-2, 2), Calc.randInt(-2, 2));
if (addEntity(entity, c)) return true;
}
return false;
}
/**
* Try to add entity at given pos
*
* @param entity the entity
* @param pos pos
* @return true if added (false if void, wall etc)
*/
public boolean addEntity(Entity entity, Coord pos)
{
final Tile t = getTile(pos);
if (!t.isWalkable() || t.isOccupied()) return false;
// set level to init EID
entity.setLevel(this);
if (entityMap.containsKey(entity.getEntityId())) {
Log.w("Entity already in level.");
return false;
}
entityMap.put(entity.getEntityId(), entity);
entityList.add(entity);
if (entity instanceof EntityPlayer) playerCount++;
// join to level & world
occupyTile(entity.getCoord());
entity.setCoord(pos);
return true;
}
/**
* Remove an entity from the level, if present
*
* @param entity entity
*/
public void removeEntity(Entity entity)
{
removeEntity(entity.getEntityId());
}
/**
* Remove an entity from the level, if present
*
* @param eid entity id
*/
public void removeEntity(int eid)
{
final Entity removed = entityMap.remove(eid);
if (removed == null) throw new NullPointerException("No such entity in level: " + eid);
if (removed instanceof EntityPlayer) playerCount--;
entityList.remove(removed);
// upon kill, entities free tile themselves.
if (!removed.isDead()) {
freeTile(removed.getCoord());
}
}
/**
* Check tile walkability
*
* @param pos tile coord
* @return true if the tile is walkable by entity
*/
public boolean isWalkable(Coord pos)
{
final Tile t = getTile(pos);
return t.isWalkable() && !t.isOccupied();
}
/**
* Mark tile as occupied (entity entered)
*
* @param pos tile pos
*/
public void occupyTile(Coord pos)
{
getTile(pos).setOccupied(true);
}
/**
* Mark tile as free (entity left)
*
* @param pos tile pos
*/
public void freeTile(Coord pos)
{
getTile(pos).setOccupied(false);
}
/**
* Check entity on tile
*
* @param pos tile coord
* @return true if some entity is standing there
*/
public boolean isOccupied(Coord pos)
{
return getTile(pos).isOccupied();
}
/**
* Set level entry point
*
* @param pos pos where the player appears upon descending to this level
*/
public void setEnterPoint(Coord pos)
{
this.enterPoint.setTo(pos);
}
/**
* Get location where the player appears upon descending to this level
*
* @return pos
*/
public Coord getEnterPoint()
{
return enterPoint;
}
/**
* Set level exit point
*
* @param pos pos where the player appears upon ascending to this level
*/
public void setExitPoint(Coord pos)
{
this.exitPoint.setTo(pos);
}
/**
* Get location where the player appears upon ascending to this level
*
* @return pos
*/
public Coord getExitPoint()
{
return exitPoint;
}
/**
* Get the level's world
*
* @return world
*/
public World getWorld()
{
return world;
}
/**
* Assign a world
*
* @param world new world
*/
public void setWorld(World world)
{
this.world = world;
}
/**
* Mark tile and surrounding area as explored
*
* @param center center the explored tile
*/
public void explore(Coord center)
{
final Collection<Coord> filled = new HashSet<>();
exploreFiller.fill(center, filled);
for (final Coord c : filled) {
getTile(c).setExplored();
}
}
/**
* Get entity of type closest to coord
*
* @param pos the attack origin
* @param type wanted entity type
* @param radius search radius; -1 for unlimited.
* @return
*/
public Entity getClosestEntity(Vect pos, EntityType type, double radius)
{
Entity closest = null;
double minDist = Double.MAX_VALUE;
for (final Entity e : entityList) {
if (e.isDead()) continue;
if (e.getType() == type) {
final double dist = e.pos.getVisualPos().dist(pos).value();
if (dist <= radius && dist < minDist) {
minDist = dist;
closest = e;
}
}
}
return closest;
}
/**
* Free a tile. If entity is present, remove it.
*
* @param pos the tile pos
*/
public void forceFreeTile(Coord pos)
{
if (getTile(pos).isOccupied()) {
final Set<Entity> toButcher = new HashSet<>();
for (final Entity e : entityList) {
if (e.getCoord().equals(pos)) {
toButcher.add(e);
break;
}
}
for (final Entity e : toButcher) {
removeEntity(e);
freeTile(pos);
}
}
if (!getTile(pos).isWalkable()) {
// this should never happen.
setTile(pos, Tiles.BRICK_FLOOR.createTile());
}
}
/**
* Check if entity is in the level
*
* @param entity entity
* @return is present
*/
public boolean isEntityPresent(Entity entity)
{
return entityList.contains(entity);
}
/**
* Check if entity is in the level
*
* @param eid entity ID
* @return true if present
*/
public boolean isEntityPresent(int eid)
{
return entityMap.containsKey(eid);
}
/**
* Get entity collection (for rendering)
*
* @return entities
*/
public Collection<Entity> getEntities()
{
return entityList;
}
@Override
public boolean isListening()
{
return playerCount > 0; // listen only when player is in this level
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Collection getChildClients()
{
return entityList;
}
@Override
public boolean doesDelegate()
{
return isListening();
}
public boolean dropNear(Coord coord, Item itm)
{
if (getTile(coord).dropItem(itm)) return true;
for (int i = 0; i < 6; i++) {
final Coord c = coord.add(Calc.randInt(-1, 1), Calc.randInt(-1, 1));
if (getTile(c).dropItem(itm)) return true;
}
return false;
}
private static class EntityRenderComparator implements Comparator<Entity> {
@Override
public int compare(Entity o1, Entity o2)
{
if (o1.isDead() && !o2.isDead()) {
return -1;
}
if (!o1.isDead() && o2.isDead()) {
return 1;
}
int c = Double.compare(o1.pos.getVisualPos().y(), o1.pos.getVisualPos().y());
if (c == 0) c = Double.compare(o1.pos.getVisualPos().x(), o1.pos.getVisualPos().x());
return c;
}
}
}