mirror of
https://github.com/2006-Scape/apollo.git
synced 2026-07-03 00:38:21 +00:00
Add support for dynamic collision detection
This commit implements collision detection using the map files loaded from the cache, and adds support for modifying the collision matrices at runtime when the game world is updated. All checks to see if a tile is reachable should now be done via. World#traversable, instead of Region#traversable, as the World object can handle checking tiles across multiple regions. These are done for the WalkingQueue and Pathfinder implementations.
This commit is contained in:
@@ -40,6 +40,20 @@ public final class MapObject {
|
|||||||
this.orientation = orientation;
|
this.orientation = orientation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@code MapObject}.
|
||||||
|
*
|
||||||
|
* @param id The object ID of this map object.
|
||||||
|
* @param x The local X coordinate of this object.
|
||||||
|
* @param y The local Y coordinate of this object.
|
||||||
|
* @param height The height level of this object.
|
||||||
|
* @param type The type of this object.
|
||||||
|
* @param orientation The orientation of this object.
|
||||||
|
*/
|
||||||
|
public MapObject(int id, int x, int y, int height, int type, int orientation) {
|
||||||
|
this(id, (height & 0x3f) << 12 | (x & 0x3f) << 6 | (y & 0x3f), type, orientation);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the object ID of this map object.
|
* Get the object ID of this map object.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,286 +0,0 @@
|
|||||||
package org.apollo.game.fs.decoder;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.UncheckedIOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.apollo.cache.IndexedFileSystem;
|
|
||||||
import org.apollo.cache.decoder.ObjectDefinitionDecoder;
|
|
||||||
import org.apollo.cache.def.ObjectDefinition;
|
|
||||||
import org.apollo.cache.map.MapIndex;
|
|
||||||
import org.apollo.cache.map.MapIndexDecoder;
|
|
||||||
import org.apollo.game.io.player.PlayerSerializer;
|
|
||||||
import org.apollo.game.model.Position;
|
|
||||||
import org.apollo.game.model.World;
|
|
||||||
import org.apollo.game.model.area.Region;
|
|
||||||
import org.apollo.game.model.area.RegionRepository;
|
|
||||||
import org.apollo.game.model.area.collision.CollisionMatrix;
|
|
||||||
import org.apollo.game.model.entity.obj.GameObject;
|
|
||||||
import org.apollo.game.model.entity.obj.ObjectType;
|
|
||||||
import org.apollo.game.model.entity.obj.StaticGameObject;
|
|
||||||
import org.apollo.util.BufferUtil;
|
|
||||||
import org.apollo.util.CompressionUtil;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses static object definitions, which include map tiles and landscapes.
|
|
||||||
*
|
|
||||||
* @author Ryley
|
|
||||||
* @author Major
|
|
||||||
*/
|
|
||||||
public final class GameObjectDecoder implements Runnable {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A bit flag that indicates that the tile at the current Position is blocked.
|
|
||||||
*/
|
|
||||||
private static final int BLOCKED_TILE = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A bit flag that indicates that the tile at the current Position is a bridge tile.
|
|
||||||
*/
|
|
||||||
private static final int BRIDGE_TILE = 2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link IndexedFileSystem}.
|
|
||||||
*/
|
|
||||||
private final IndexedFileSystem fs;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link List} of decoded GameObjects.
|
|
||||||
*/
|
|
||||||
private final List<GameObject> objects = new ArrayList<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The RegionRepository.
|
|
||||||
*/
|
|
||||||
private final RegionRepository regions;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The World to place the objects in.
|
|
||||||
*/
|
|
||||||
private final World world;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The most-recently used Region.
|
|
||||||
*/
|
|
||||||
private Region previous;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the GameObjectDecoder.
|
|
||||||
*
|
|
||||||
* @param fs The {@link IndexedFileSystem}.
|
|
||||||
* @param world The {@link World} to place the objects in.
|
|
||||||
*/
|
|
||||||
public GameObjectDecoder(IndexedFileSystem fs, World world) {
|
|
||||||
this.fs = fs;
|
|
||||||
this.world = world;
|
|
||||||
regions = world.getRegionRepository();
|
|
||||||
previous = regions.fromPosition(PlayerSerializer.TUTORIAL_ISLAND_SPAWN); // dummy, so 'previous' is never null.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
ObjectDefinitionDecoder decoder = new ObjectDefinitionDecoder(fs);
|
|
||||||
decoder.run();
|
|
||||||
|
|
||||||
MapIndexDecoder mapIndexDecoder = new MapIndexDecoder(fs);
|
|
||||||
mapIndexDecoder.run();
|
|
||||||
|
|
||||||
try {
|
|
||||||
Map<Integer, MapIndex> indices = MapIndex.getIndices();
|
|
||||||
|
|
||||||
for (MapIndex definition : indices.values()) {
|
|
||||||
int packed = definition.getPackedCoordinates();
|
|
||||||
int x = (packed >> 8 & 0xFF) * (Region.SIZE * Region.SIZE);
|
|
||||||
int y = (packed & 0xFF) * (Region.SIZE * Region.SIZE);
|
|
||||||
|
|
||||||
ByteBuffer objects = fs.getFile(4, definition.getObjectFile());
|
|
||||||
ByteBuffer decompressed = ByteBuffer.wrap(CompressionUtil.degzip(objects));
|
|
||||||
decodeObjects(decompressed, x, y);
|
|
||||||
|
|
||||||
ByteBuffer terrain = fs.getFile(4, definition.getMapFile());
|
|
||||||
decompressed = ByteBuffer.wrap(CompressionUtil.degzip(terrain));
|
|
||||||
decodeTerrain(decompressed, x, y);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new UncheckedIOException("Error decoding StaticGameObjects.", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
objects.forEach(object -> regions.fromPosition(object.getPosition()).addEntity(object, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Blocks tiles covered by a GameObject, if applicable.
|
|
||||||
*
|
|
||||||
* @param object The {@link GameObject}.
|
|
||||||
* @param position The position of the GameObject.
|
|
||||||
*/
|
|
||||||
private void block(GameObject object, Position position) {
|
|
||||||
ObjectDefinition definition = ObjectDefinition.lookup(object.getId());
|
|
||||||
int type = object.getType();
|
|
||||||
|
|
||||||
int x = position.getX(), y = position.getY(), height = position.getHeight();
|
|
||||||
|
|
||||||
if (!previous.contains(position)) {
|
|
||||||
previous = regions.fromPosition(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
CollisionMatrix matrix = previous.getMatrix(height);
|
|
||||||
if (unwalkable(definition, type)) {
|
|
||||||
int width = definition.getWidth(), length = definition.getLength();
|
|
||||||
|
|
||||||
for (int dx = 0; dx < width; dx++) {
|
|
||||||
for (int dy = 0; dy < length; dy++) {
|
|
||||||
int localX = x % Region.SIZE + dx, localY = y % Region.SIZE + dy;
|
|
||||||
|
|
||||||
if (localX > 7 || localY > 7) {
|
|
||||||
int nextLocalX = localX > 7 ? x + localX - 7 : x + localX;
|
|
||||||
int nextLocalY = localY > 7 ? y + localY - 7 : y - localY;
|
|
||||||
Region next = regions.fromPosition(new Position(nextLocalX, nextLocalY));
|
|
||||||
|
|
||||||
int nextX = nextLocalX % Region.SIZE + dx;
|
|
||||||
int nextY = nextLocalY % Region.SIZE + dy;
|
|
||||||
|
|
||||||
if (nextX > 7) {
|
|
||||||
nextX -= 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextY > 7) {
|
|
||||||
nextY -= 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
next.getMatrix(height).block(nextX, nextY);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
matrix.block(localX, localY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes the attributes of a terrain file, blocking the tile if necessary.
|
|
||||||
*
|
|
||||||
* @param attributes The terrain attributes.
|
|
||||||
* @param x The x coordinate of the tile the attributes belong to.
|
|
||||||
* @param y The y coordinate of the tile the attributes belong to.
|
|
||||||
* @param height The level level of the tile the attributes belong to.
|
|
||||||
*/
|
|
||||||
private void decodeAttributes(int attributes, int x, int y, int height) {
|
|
||||||
boolean block = false;
|
|
||||||
if ((attributes & BLOCKED_TILE) != 0) {
|
|
||||||
block = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((attributes & BRIDGE_TILE) != 0 && height >0) {
|
|
||||||
block = true;
|
|
||||||
height--;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (block) {
|
|
||||||
int localX = x % Region.SIZE, localY = y % Region.SIZE;
|
|
||||||
Position position = new Position(x, y, height);
|
|
||||||
|
|
||||||
if (!previous.contains(position)) {
|
|
||||||
previous = regions.fromPosition(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
previous.getMatrix(height).block(localX, localY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes object data stored in the specified {@link ByteBuffer}.
|
|
||||||
*
|
|
||||||
* @param buffer The ByteBuffer to decode data from.
|
|
||||||
* @param x The x coordinate of the top left tile of the map file.
|
|
||||||
* @param y The y coordinate of the top left tile of the map file.
|
|
||||||
*/
|
|
||||||
private void decodeObjects(ByteBuffer buffer, int x, int y) {
|
|
||||||
int id = -1;
|
|
||||||
int idOffset = BufferUtil.readSmart(buffer);
|
|
||||||
|
|
||||||
while (idOffset != 0) {
|
|
||||||
id += idOffset;
|
|
||||||
|
|
||||||
int packed = 0;
|
|
||||||
int positionOffset = BufferUtil.readSmart(buffer);
|
|
||||||
|
|
||||||
while (positionOffset != 0) {
|
|
||||||
packed += positionOffset - 1;
|
|
||||||
|
|
||||||
int localY = packed & 0x3F;
|
|
||||||
int localX = packed >> 6 & 0x3F;
|
|
||||||
int height = packed >> 12 & 0x3;
|
|
||||||
|
|
||||||
int attributes = buffer.get() & 0xFF;
|
|
||||||
int type = attributes >> 2;
|
|
||||||
int orientation = attributes & 0x3;
|
|
||||||
Position position = new Position(x + localX, y + localY, height);
|
|
||||||
|
|
||||||
GameObject object = new StaticGameObject(world, id, position, type, orientation);
|
|
||||||
objects.add(object);
|
|
||||||
|
|
||||||
block(object, position);
|
|
||||||
positionOffset = BufferUtil.readSmart(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
idOffset = BufferUtil.readSmart(buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes terrain data stored in the specified {@link ByteBuffer}.
|
|
||||||
*
|
|
||||||
* @param buffer The ByteBuffer.
|
|
||||||
* @param x The x coordinate of the top left tile of the map file.
|
|
||||||
* @param y The y coordinate of the top left tile of the map file.
|
|
||||||
*/
|
|
||||||
private void decodeTerrain(ByteBuffer buffer, int x, int y) {
|
|
||||||
for (int height = 0; height < Position.HEIGHT_LEVELS; height++) {
|
|
||||||
for (int localX = 0; localX < Region.SIZE * Region.SIZE; localX++) {
|
|
||||||
for (int localY = 0; localY < Region.SIZE * Region.SIZE; localY++) {
|
|
||||||
int attributes = 0;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
int attributeId = buffer.get() & 0xFF;
|
|
||||||
|
|
||||||
if (attributeId == 0) {
|
|
||||||
decodeAttributes(attributes, x + localX, y + localY, height);
|
|
||||||
break;
|
|
||||||
} else if (attributeId == 1) {
|
|
||||||
buffer.get();
|
|
||||||
decodeAttributes(attributes, x + localX, y + localY, height);
|
|
||||||
break;
|
|
||||||
} else if (attributeId <= 49) {
|
|
||||||
buffer.get();
|
|
||||||
} else if (attributeId <= 81) {
|
|
||||||
attributes = attributeId - 49;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether or not an object with the specified {@link ObjectDefinition} and {@code type} should result in
|
|
||||||
* the tile(s) it is located on being blocked.
|
|
||||||
*
|
|
||||||
* @param definition The {@link ObjectDefinition} of the object.
|
|
||||||
* @param type The type of the object.
|
|
||||||
* @return {@code true} iff the tile(s) the object is on should be blocked.
|
|
||||||
*/
|
|
||||||
private boolean unwalkable(ObjectDefinition definition, int type) {
|
|
||||||
// TODO figure out the other ObjectTypes and get rid of all the getValue() calls
|
|
||||||
return (type == ObjectType.FLOOR_DECORATION.getValue() && definition.isInteractive()) ||
|
|
||||||
(type >= ObjectType.LENGTHWISE_WALL.getValue() && type <= ObjectType.RECTANGULAR_CORNER.getValue()) ||
|
|
||||||
(type > ObjectType.DIAGONAL_INTERACTABLE.getValue() && type < ObjectType.FLOOR_DECORATION.getValue()) ||
|
|
||||||
(type == ObjectType.INTERACTABLE.getValue() && definition.isSolid()) ||
|
|
||||||
type == ObjectType.DIAGONAL_WALL.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package org.apollo.game.fs.decoder;
|
||||||
|
|
||||||
|
import org.apollo.cache.IndexedFileSystem;
|
||||||
|
import org.apollo.cache.map.MapConstants;
|
||||||
|
import org.apollo.cache.map.MapFile;
|
||||||
|
import org.apollo.cache.map.MapFileDecoder;
|
||||||
|
import org.apollo.cache.map.MapIndex;
|
||||||
|
import org.apollo.cache.map.MapPlane;
|
||||||
|
import org.apollo.cache.map.Tile;
|
||||||
|
import org.apollo.game.model.Position;
|
||||||
|
import org.apollo.game.model.area.collision.CollisionManager;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A decoder which loads {@link MapFile}s and notifies the {@link CollisionManager} of tiles which are blocked,
|
||||||
|
* or on a bridge.
|
||||||
|
*/
|
||||||
|
public final class WorldMapDecoder implements Runnable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bit flag that indicates that the tile at the current Position is blocked.
|
||||||
|
*/
|
||||||
|
private static final int BLOCKED_TILE = 0x1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bit flag that indicates that the tile at the current Position is a bridge tile.
|
||||||
|
*/
|
||||||
|
private static final int BRIDGE_TILE = 0x2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link IndexedFileSystem}.
|
||||||
|
*/
|
||||||
|
private IndexedFileSystem fs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link CollisionManager} to notify of bridged / blocked tiles.
|
||||||
|
*/
|
||||||
|
private CollisionManager collisionManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link WorldMapDecoder}.
|
||||||
|
*
|
||||||
|
* @param fs The {@link IndexedFileSystem} to load {@link MapFile}s. from.
|
||||||
|
* @param collisionManager The {@link CollisionManager} to register tiles with.
|
||||||
|
*/
|
||||||
|
public WorldMapDecoder(IndexedFileSystem fs, CollisionManager collisionManager) {
|
||||||
|
this.fs = fs;
|
||||||
|
this.collisionManager = collisionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode all {@link MapFile}s and notify the {@link CollisionManager} of any tiles that are
|
||||||
|
* flagged as blocked or on a bridge.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Map<Integer, MapIndex> mapIndices = MapIndex.getIndices();
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (MapIndex index : mapIndices.values()) {
|
||||||
|
MapFileDecoder decoder = MapFileDecoder.create(fs, index);
|
||||||
|
MapFile mapFile = decoder.decode();
|
||||||
|
MapPlane[] mapPlanes = mapFile.getPlanes();
|
||||||
|
|
||||||
|
int mapX = index.getX(), mapY = index.getY();
|
||||||
|
for (MapPlane plane : mapPlanes) {
|
||||||
|
markTiles(mapX, mapY, plane);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new UncheckedIOException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark any tiles in the given {@link MapPlane} as blocked or bridged in the {@link CollisionManager}.
|
||||||
|
*
|
||||||
|
* @param mapX The X coordinate of the map file.
|
||||||
|
* @param mapY The Y coordinate of the map file.
|
||||||
|
* @param plane The {@link MapPlane} to load tiles from.
|
||||||
|
*/
|
||||||
|
private void markTiles(int mapX, int mapY, MapPlane plane) {
|
||||||
|
for (int x = 0; x < MapConstants.MAP_WIDTH; x++) {
|
||||||
|
for (int y = 0; y < MapConstants.MAP_WIDTH; y++) {
|
||||||
|
Tile tile = plane.getTile(x, y);
|
||||||
|
Position position = new Position(mapX + x, mapY + y, plane.getLevel());
|
||||||
|
|
||||||
|
if ((tile.getAttributes() & BLOCKED_TILE) == BLOCKED_TILE) {
|
||||||
|
collisionManager.markBlocked(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((tile.getAttributes() & BRIDGE_TILE) == BRIDGE_TILE) {
|
||||||
|
collisionManager.markBridged(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package org.apollo.game.fs.decoder;
|
||||||
|
|
||||||
|
import org.apollo.cache.IndexedFileSystem;
|
||||||
|
import org.apollo.cache.map.MapIndex;
|
||||||
|
import org.apollo.cache.map.MapObject;
|
||||||
|
import org.apollo.cache.map.MapObjectsDecoder;
|
||||||
|
import org.apollo.game.model.Position;
|
||||||
|
import org.apollo.game.model.World;
|
||||||
|
import org.apollo.game.model.area.Region;
|
||||||
|
import org.apollo.game.model.area.RegionRepository;
|
||||||
|
import org.apollo.game.model.entity.obj.StaticGameObject;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A decoder which decodes {@link MapObject}s and registers them with the game world.
|
||||||
|
*/
|
||||||
|
public final class WorldObjectsDecoder implements Runnable {
|
||||||
|
/**
|
||||||
|
* The IndexedFileSystem.
|
||||||
|
*/
|
||||||
|
private final IndexedFileSystem fs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link RegionRepository} to lookup {@link Region}s from.
|
||||||
|
*/
|
||||||
|
private final RegionRepository regionRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link World} to register {@link StaticGameObject}s with.
|
||||||
|
*/
|
||||||
|
private final World world;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link WorldObjectsDecoder}.
|
||||||
|
*
|
||||||
|
* @param fs The {@link IndexedFileSystem} to load object files from.
|
||||||
|
* @param world The {@link World} to register objects with.
|
||||||
|
* @param regionRepository The {@link RegionRepository} to lookup {@link Region}s from.
|
||||||
|
*/
|
||||||
|
public WorldObjectsDecoder(IndexedFileSystem fs, World world, RegionRepository regionRepository) {
|
||||||
|
this.fs = fs;
|
||||||
|
this.world = world;
|
||||||
|
this.regionRepository = regionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode the {@code MapObject}s from the cache and register them with the world.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Map<Integer, MapIndex> mapIndices = MapIndex.getIndices();
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (MapIndex index : mapIndices.values()) {
|
||||||
|
MapObjectsDecoder decoder = MapObjectsDecoder.create(fs, index);
|
||||||
|
List<MapObject> objects = decoder.decode();
|
||||||
|
|
||||||
|
int mapX = index.getX(), mapY = index.getY();
|
||||||
|
|
||||||
|
for (MapObject object : objects) {
|
||||||
|
Position position = new Position(mapX + object.getLocalX(), mapY + object.getLocalY(),
|
||||||
|
object.getHeight());
|
||||||
|
|
||||||
|
StaticGameObject gameObject = new StaticGameObject(world, object.getId(), position,
|
||||||
|
object.getType(), object.getOrientation());
|
||||||
|
|
||||||
|
regionRepository.fromPosition(position).addEntity(gameObject, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new UncheckedIOException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,6 +57,23 @@ public enum Direction {
|
|||||||
*/
|
*/
|
||||||
public static final Direction[] EMPTY_DIRECTION_ARRAY = new Direction[0];
|
public static final Direction[] EMPTY_DIRECTION_ARRAY = new Direction[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of directions without any diagonal directions.
|
||||||
|
*/
|
||||||
|
public final static Direction[] NESW = { NORTH, EAST, SOUTH, WEST };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of directions without any diagonal directions, and one step counter-clockwise, as used by
|
||||||
|
* the clients collision mapping.
|
||||||
|
*/
|
||||||
|
public final static Direction[] WNES = { WEST, NORTH, EAST, SOUTH };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of diagonal directions, and one step counter-clockwise, as used by the clients collision
|
||||||
|
* mapping.
|
||||||
|
*/
|
||||||
|
public final static Direction[] WNES_DIAGONAL = { NORTH_WEST, NORTH_EAST, SOUTH_EAST, SOUTH_WEST};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the Direction between the two {@link Position}s..
|
* Gets the Direction between the two {@link Position}s..
|
||||||
*
|
*
|
||||||
@@ -68,6 +85,17 @@ public enum Direction {
|
|||||||
int deltaX = next.getX() - current.getX();
|
int deltaX = next.getX() - current.getX();
|
||||||
int deltaY = next.getY() - current.getY();
|
int deltaY = next.getY() - current.getY();
|
||||||
|
|
||||||
|
return fromDeltas(deltaX, deltaY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a direction from the differences between X and Y.
|
||||||
|
*
|
||||||
|
* @param deltaX The difference between two X coordinates.
|
||||||
|
* @param deltaY The difference between two Y coordinates.
|
||||||
|
* @return The direction.
|
||||||
|
*/
|
||||||
|
public static Direction fromDeltas(int deltaX, int deltaY) {
|
||||||
if (deltaY == 1) {
|
if (deltaY == 1) {
|
||||||
if (deltaX == 1) {
|
if (deltaX == 1) {
|
||||||
return NORTH_EAST;
|
return NORTH_EAST;
|
||||||
@@ -97,6 +125,27 @@ public enum Direction {
|
|||||||
throw new IllegalArgumentException("Difference between Positions must be [-1, 1].");
|
throw new IllegalArgumentException("Difference between Positions must be [-1, 1].");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the 2 directions which make up a diagonal direction (i.e., NORTH and EAST for NORTH_EAST).
|
||||||
|
*
|
||||||
|
* @param direction The direction to get the components for.
|
||||||
|
* @return The components for the given direction.
|
||||||
|
*/
|
||||||
|
public static Direction[] diagonalComponents(Direction direction) {
|
||||||
|
switch (direction) {
|
||||||
|
case NORTH_EAST:
|
||||||
|
return new Direction[] { NORTH, EAST };
|
||||||
|
case NORTH_WEST:
|
||||||
|
return new Direction[] { NORTH, WEST };
|
||||||
|
case SOUTH_EAST:
|
||||||
|
return new Direction[] { SOUTH, EAST };
|
||||||
|
case SOUTH_WEST:
|
||||||
|
return new Direction[] { SOUTH, WEST };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Must provide a diagonal direction.");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The direction as an integer.
|
* The direction as an integer.
|
||||||
*/
|
*/
|
||||||
@@ -111,6 +160,83 @@ public enum Direction {
|
|||||||
this.intValue = intValue;
|
this.intValue = intValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the opposite direction of the this direction.
|
||||||
|
*
|
||||||
|
* @return The opposite direction.
|
||||||
|
*/
|
||||||
|
public Direction opposite() {
|
||||||
|
switch (this) {
|
||||||
|
case NORTH:
|
||||||
|
return SOUTH;
|
||||||
|
case SOUTH:
|
||||||
|
return NORTH;
|
||||||
|
case EAST:
|
||||||
|
return WEST;
|
||||||
|
case WEST:
|
||||||
|
return EAST;
|
||||||
|
case NORTH_WEST:
|
||||||
|
return SOUTH_EAST;
|
||||||
|
case NORTH_EAST:
|
||||||
|
return SOUTH_WEST;
|
||||||
|
case SOUTH_EAST:
|
||||||
|
return NORTH_WEST;
|
||||||
|
case SOUTH_WEST:
|
||||||
|
return NORTH_EAST;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the X delta from a {@link Position} of (0, 0).
|
||||||
|
*
|
||||||
|
* @return The delta of X from (0, 0).
|
||||||
|
*/
|
||||||
|
public int deltaX() {
|
||||||
|
switch (this) {
|
||||||
|
case SOUTH_EAST:
|
||||||
|
case NORTH_EAST:
|
||||||
|
case EAST:
|
||||||
|
return 1;
|
||||||
|
case SOUTH_WEST:
|
||||||
|
case NORTH_WEST:
|
||||||
|
case WEST:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the Y delta from a {@link Position} of (0, 0).
|
||||||
|
*
|
||||||
|
* @return The delta of Y from (0, 0).
|
||||||
|
*/
|
||||||
|
public int deltaY() {
|
||||||
|
switch (this) {
|
||||||
|
case NORTH_WEST:
|
||||||
|
case NORTH_EAST:
|
||||||
|
case NORTH:
|
||||||
|
return 1;
|
||||||
|
case SOUTH_WEST:
|
||||||
|
case SOUTH_EAST:
|
||||||
|
case SOUTH:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this direction is a diagonal direction.
|
||||||
|
*
|
||||||
|
* @return {@code true} if this direction is a diagonal direction, {@code false} otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isDiagonal() {
|
||||||
|
return this == SOUTH_EAST || this == SOUTH_WEST || this == NORTH_EAST || this == NORTH_WEST;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the direction as an integer which the client can understand.
|
* Gets the direction as an integer which the client can understand.
|
||||||
*
|
*
|
||||||
@@ -120,4 +246,28 @@ public enum Direction {
|
|||||||
return intValue;
|
return intValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the direction as an integer as used orientation in the client maps (WNES as opposed to NESW).
|
||||||
|
*
|
||||||
|
* @return The direction as an integer.
|
||||||
|
*/
|
||||||
|
public int toOrientationInteger() {
|
||||||
|
switch(this) {
|
||||||
|
case WEST:
|
||||||
|
case NORTH_WEST:
|
||||||
|
return 0;
|
||||||
|
case NORTH:
|
||||||
|
case NORTH_EAST:
|
||||||
|
return 1;
|
||||||
|
case EAST:
|
||||||
|
case SOUTH_EAST:
|
||||||
|
return 2;
|
||||||
|
case SOUTH:
|
||||||
|
case SOUTH_WEST:
|
||||||
|
return 3;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Only a valid direction can have an orientation value");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -224,9 +224,19 @@ public final class Position {
|
|||||||
return deltaX <= distance && deltaY <= distance && getHeight() == other.getHeight();
|
return deltaX <= distance && deltaY <= distance && getHeight() == other.getHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
public String toString() {
|
* Creates a new position {@code num} steps from this position in the given direction.
|
||||||
return MoreObjects.toStringHelper(this).add("x", getX()).add("y", getY()).add("height", getHeight()).add("region", getRegionCoordinates()).toString();
|
*
|
||||||
|
* @param num The number of steps to make.
|
||||||
|
* @param direction The direction to make steps in.
|
||||||
|
* @return A new {@code Position} that is {@code num} steps in {@code direction} ahead of this one.
|
||||||
|
*/
|
||||||
|
public Position step(int num, Direction direction) {
|
||||||
|
return new Position(getX() + (num * direction.deltaX()), getY() + (num * direction.deltaY()), getHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return MoreObjects.toStringHelper(this).add("x", getX()).add("y", getY()).add("height", getHeight()).add("map", getRegionCoordinates()).toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
package org.apollo.game.model;
|
package org.apollo.game.model;
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
import java.util.*;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
@@ -12,12 +8,17 @@ import org.apollo.Service;
|
|||||||
import org.apollo.cache.IndexedFileSystem;
|
import org.apollo.cache.IndexedFileSystem;
|
||||||
import org.apollo.cache.decoder.ItemDefinitionDecoder;
|
import org.apollo.cache.decoder.ItemDefinitionDecoder;
|
||||||
import org.apollo.cache.decoder.NpcDefinitionDecoder;
|
import org.apollo.cache.decoder.NpcDefinitionDecoder;
|
||||||
|
import org.apollo.cache.decoder.ObjectDefinitionDecoder;
|
||||||
|
import org.apollo.cache.map.MapIndexDecoder;
|
||||||
import org.apollo.game.command.CommandDispatcher;
|
import org.apollo.game.command.CommandDispatcher;
|
||||||
import org.apollo.game.fs.decoder.GameObjectDecoder;
|
|
||||||
import org.apollo.game.fs.decoder.SynchronousDecoder;
|
import org.apollo.game.fs.decoder.SynchronousDecoder;
|
||||||
|
import org.apollo.game.fs.decoder.WorldMapDecoder;
|
||||||
|
import org.apollo.game.fs.decoder.WorldObjectsDecoder;
|
||||||
import org.apollo.game.io.EquipmentDefinitionParser;
|
import org.apollo.game.io.EquipmentDefinitionParser;
|
||||||
import org.apollo.game.model.area.Region;
|
import org.apollo.game.model.area.Region;
|
||||||
import org.apollo.game.model.area.RegionRepository;
|
import org.apollo.game.model.area.RegionRepository;
|
||||||
|
import org.apollo.game.model.area.collision.CollisionManager;
|
||||||
|
import org.apollo.game.model.area.collision.GameObjectCollisionUpdateListener;
|
||||||
import org.apollo.game.model.entity.Entity;
|
import org.apollo.game.model.entity.Entity;
|
||||||
import org.apollo.game.model.entity.EntityType;
|
import org.apollo.game.model.entity.EntityType;
|
||||||
import org.apollo.game.model.entity.MobRepository;
|
import org.apollo.game.model.entity.MobRepository;
|
||||||
@@ -110,6 +111,11 @@ public final class World {
|
|||||||
*/
|
*/
|
||||||
private final RegionRepository regions = RegionRepository.immutable();
|
private final RegionRepository regions = RegionRepository.immutable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This world's {@link CollisionManager}.
|
||||||
|
*/
|
||||||
|
private final CollisionManager collisionManager = new CollisionManager(regions);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The scheduler.
|
* The scheduler.
|
||||||
*/
|
*/
|
||||||
@@ -130,6 +136,13 @@ public final class World {
|
|||||||
*/
|
*/
|
||||||
private int releaseNumber;
|
private int releaseNumber;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the collision manager.
|
||||||
|
*
|
||||||
|
* @return The collision manager
|
||||||
|
*/
|
||||||
|
public CollisionManager getCollisionManager() { return collisionManager; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the command dispatcher.
|
* Gets the command dispatcher.
|
||||||
*
|
*
|
||||||
@@ -207,13 +220,28 @@ public final class World {
|
|||||||
public void init(int release, IndexedFileSystem fs, PluginManager manager) throws Exception {
|
public void init(int release, IndexedFileSystem fs, PluginManager manager) throws Exception {
|
||||||
releaseNumber = release;
|
releaseNumber = release;
|
||||||
|
|
||||||
SynchronousDecoder decoder = new SynchronousDecoder(new ItemDefinitionDecoder(fs),
|
SynchronousDecoder firstStageDecoder = new SynchronousDecoder(
|
||||||
new NpcDefinitionDecoder(fs), new GameObjectDecoder(fs, this),
|
new NpcDefinitionDecoder(fs),
|
||||||
EquipmentDefinitionParser.fromFile("data/equipment-" + release + "" + ".dat"));
|
new ItemDefinitionDecoder(fs),
|
||||||
|
new ObjectDefinitionDecoder(fs),
|
||||||
|
new MapIndexDecoder(fs),
|
||||||
|
EquipmentDefinitionParser.fromFile("data/equipment-" + release + "" + ".dat")
|
||||||
|
);
|
||||||
|
|
||||||
decoder.block();
|
firstStageDecoder.block();
|
||||||
|
|
||||||
npcMovement = new NpcMovementTask(regions); // Must be exactly here because of ordering issues.
|
SynchronousDecoder secondStageDecoder = new SynchronousDecoder(
|
||||||
|
new WorldObjectsDecoder(fs, this, regions),
|
||||||
|
new WorldMapDecoder(fs, collisionManager)
|
||||||
|
);
|
||||||
|
|
||||||
|
secondStageDecoder.block();
|
||||||
|
|
||||||
|
// Build collision matrices for the first time
|
||||||
|
collisionManager.build(false);
|
||||||
|
regions.addRegionListener(new GameObjectCollisionUpdateListener(collisionManager));
|
||||||
|
|
||||||
|
npcMovement = new NpcMovementTask(collisionManager); // Must be exactly here because of ordering issues.
|
||||||
scheduler.schedule(npcMovement);
|
scheduler.schedule(npcMovement);
|
||||||
|
|
||||||
manager.start();
|
manager.start();
|
||||||
|
|||||||
@@ -300,6 +300,15 @@ public final class Region {
|
|||||||
return matrices[height];
|
return matrices[height];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all {@link CollisionMatrix}'s in this {@code Region}.
|
||||||
|
*
|
||||||
|
* @return The collision matrices of this region.
|
||||||
|
*/
|
||||||
|
public CollisionMatrix[] getMatrices() {
|
||||||
|
return matrices;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the {@link Set} of {@link RegionUpdateMessage}s that have occurred in the last pulse. This method can
|
* Gets the {@link Set} of {@link RegionUpdateMessage}s that have occurred in the last pulse. This method can
|
||||||
* only be called <strong>once</strong> per pulse.
|
* only be called <strong>once</strong> per pulse.
|
||||||
|
|||||||
@@ -9,45 +9,85 @@ import org.apollo.game.model.entity.EntityType;
|
|||||||
*/
|
*/
|
||||||
public enum CollisionFlag {
|
public enum CollisionFlag {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The walk north west flag.
|
||||||
|
*/
|
||||||
|
MOB_NORTH_WEST(1),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The walk north flag.
|
* The walk north flag.
|
||||||
*/
|
*/
|
||||||
MOB_NORTH(0),
|
MOB_NORTH(2),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The walk north east flag.
|
||||||
|
*/
|
||||||
|
MOB_NORTH_EAST(3),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The walk east flag.
|
* The walk east flag.
|
||||||
*/
|
*/
|
||||||
MOB_EAST(1),
|
MOB_EAST(4),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The walk south east flag.
|
||||||
|
*/
|
||||||
|
MOB_SOUTH_EAST(5),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The walk south flag.
|
* The walk south flag.
|
||||||
*/
|
*/
|
||||||
MOB_SOUTH(2),
|
MOB_SOUTH(6),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The walk south west flag.
|
||||||
|
*/
|
||||||
|
MOB_SOUTH_WEST(7),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The walk west flag.
|
* The walk west flag.
|
||||||
*/
|
*/
|
||||||
MOB_WEST(3),
|
MOB_WEST(8),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The projectile north west flag.
|
||||||
|
*/
|
||||||
|
PROJECTILE_NORTH_WEST(9),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The projectile north flag.
|
* The projectile north flag.
|
||||||
*/
|
*/
|
||||||
PROJECTILE_NORTH(4),
|
PROJECTILE_NORTH(10),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The projectile north east flag.
|
||||||
|
*/
|
||||||
|
PROJECTILE_NORTH_EAST(11),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The projectile east flag.
|
* The projectile east flag.
|
||||||
*/
|
*/
|
||||||
PROJECTILE_EAST(5),
|
PROJECTILE_EAST(12),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The projectile south east flag.
|
||||||
|
*/
|
||||||
|
PROJECTILE_SOUTH_EAST(13),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The projectile south flag.
|
* The projectile south flag.
|
||||||
*/
|
*/
|
||||||
PROJECTILE_SOUTH(6),
|
PROJECTILE_SOUTH(14),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The projectile south west flag.
|
||||||
|
*/
|
||||||
|
PROJECTILE_SOUTH_WEST(15),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The projectile west flag.
|
* The projectile west flag.
|
||||||
*/
|
*/
|
||||||
PROJECTILE_WEST(7);
|
PROJECTILE_WEST(16);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an array of CollisionFlags that indicate if the specified {@link EntityType} can be positioned on a tile.
|
* Returns an array of CollisionFlags that indicate if the specified {@link EntityType} can be positioned on a tile.
|
||||||
@@ -65,7 +105,16 @@ public enum CollisionFlag {
|
|||||||
* @return The array of CollisionFlags.
|
* @return The array of CollisionFlags.
|
||||||
*/
|
*/
|
||||||
public static CollisionFlag[] mobs() {
|
public static CollisionFlag[] mobs() {
|
||||||
return new CollisionFlag[] { MOB_NORTH, MOB_EAST, MOB_SOUTH, MOB_WEST };
|
return new CollisionFlag[] {
|
||||||
|
MOB_NORTH_WEST,
|
||||||
|
MOB_NORTH,
|
||||||
|
MOB_NORTH_EAST,
|
||||||
|
MOB_WEST,
|
||||||
|
MOB_EAST,
|
||||||
|
MOB_SOUTH_WEST,
|
||||||
|
MOB_SOUTH,
|
||||||
|
MOB_SOUTH_EAST
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,7 +123,16 @@ public enum CollisionFlag {
|
|||||||
* @return The array of CollisionFlags.
|
* @return The array of CollisionFlags.
|
||||||
*/
|
*/
|
||||||
public static CollisionFlag[] projectiles() {
|
public static CollisionFlag[] projectiles() {
|
||||||
return new CollisionFlag[] { PROJECTILE_NORTH, PROJECTILE_EAST, PROJECTILE_SOUTH, PROJECTILE_WEST };
|
return new CollisionFlag[] {
|
||||||
|
PROJECTILE_NORTH_WEST,
|
||||||
|
PROJECTILE_NORTH,
|
||||||
|
PROJECTILE_NORTH_EAST,
|
||||||
|
PROJECTILE_WEST,
|
||||||
|
PROJECTILE_EAST,
|
||||||
|
PROJECTILE_SOUTH_WEST,
|
||||||
|
PROJECTILE_SOUTH,
|
||||||
|
PROJECTILE_SOUTH_EAST
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,12 +150,12 @@ public enum CollisionFlag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets this CollisionFlag, as a {@code byte}.
|
* Gets this CollisionFlag, as a {@code short}.
|
||||||
*
|
*
|
||||||
* @return The value, as a {@code byte}.
|
* @return The value, as a {@code short}.
|
||||||
*/
|
*/
|
||||||
public byte asByte() {
|
public short asShort() {
|
||||||
return (byte) (1 << bit);
|
return (short) (1 << bit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
package org.apollo.game.model.area.collision;
|
||||||
|
|
||||||
|
import org.apollo.game.model.Direction;
|
||||||
|
import org.apollo.game.model.Position;
|
||||||
|
import org.apollo.game.model.area.Region;
|
||||||
|
import org.apollo.game.model.area.RegionRepository;
|
||||||
|
import org.apollo.game.model.area.collision.CollisionUpdate.DirectionFlag;
|
||||||
|
import org.apollo.game.model.entity.EntityType;
|
||||||
|
import org.apollo.game.model.entity.obj.GameObject;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages applying {@link CollisionUpdate}s to the respective {@link CollisionMatrix} instances, and keeping
|
||||||
|
* a record of collision state (i.e., which tiles are bridged).
|
||||||
|
*/
|
||||||
|
public final class CollisionManager {
|
||||||
|
/**
|
||||||
|
* A comparator which sorts {@link Position}s by their X coordinate, then Y, then height.
|
||||||
|
*/
|
||||||
|
private static final Comparator<Position> POSITION_COMPARATOR =
|
||||||
|
Comparator.comparingInt(Position::getX).thenComparingInt(Position::getY).thenComparingInt(Position::getHeight);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@code SortedSet} of positions where the tile is part of a bridged structure.
|
||||||
|
*/
|
||||||
|
private final SortedSet<Position> bridgeTiles = new TreeSet<>(POSITION_COMPARATOR);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@code SortedSet} of positions where the tile is completely blocked.
|
||||||
|
*/
|
||||||
|
private final SortedSet<Position> blockedTiles = new TreeSet<>(POSITION_COMPARATOR);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link RegionRepository} containing {@link Region}s, used to lookup {@link CollisionMatrix}'s.
|
||||||
|
*/
|
||||||
|
private final RegionRepository regionRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@code CollisionManager}.
|
||||||
|
*
|
||||||
|
* @param regionRepository The {@link RegionRepository} to lookup {@link Region} and {@link CollisionMatrix} instances
|
||||||
|
* from.
|
||||||
|
*/
|
||||||
|
public CollisionManager(RegionRepository regionRepository) {
|
||||||
|
this.regionRepository = regionRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apples the first initial {@link CollisionUpdate} to the {@link CollisionMatrix}es for all objects and tiles loaded from
|
||||||
|
* the cache.
|
||||||
|
*
|
||||||
|
* @param rebuilding A flag indicating whether {@link CollisionMatrix}es are being rebuilt, or built for the first time.
|
||||||
|
*/
|
||||||
|
public void build(boolean rebuilding) {
|
||||||
|
if (rebuilding) {
|
||||||
|
for (Region region : regionRepository.getRegions()) {
|
||||||
|
for (CollisionMatrix matrix : region.getMatrices()) {
|
||||||
|
matrix.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollisionUpdate.Builder tileUpdateBuilder = new CollisionUpdate.Builder();
|
||||||
|
tileUpdateBuilder.type(CollisionUpdateType.ADDING);
|
||||||
|
|
||||||
|
for (Position tile : blockedTiles) {
|
||||||
|
int x = tile.getX(), y = tile.getY();
|
||||||
|
int height = tile.getHeight();
|
||||||
|
|
||||||
|
if (bridgeTiles.contains(new Position(x, y, 1))) {
|
||||||
|
height--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (height >= 0) {
|
||||||
|
tileUpdateBuilder.tile(new Position(x, y, height), false, Direction.NESW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollisionUpdate tileUpdate = tileUpdateBuilder.build();
|
||||||
|
apply(tileUpdate);
|
||||||
|
|
||||||
|
for (Region region : regionRepository.getRegions()) {
|
||||||
|
CollisionUpdate.Builder regionObjectsUpdateBuilder = new CollisionUpdate.Builder();
|
||||||
|
regionObjectsUpdateBuilder.type(CollisionUpdateType.ADDING);
|
||||||
|
|
||||||
|
region.getEntities(EntityType.STATIC_OBJECT, EntityType.DYNAMIC_OBJECT)
|
||||||
|
.forEach(entity -> regionObjectsUpdateBuilder.object((GameObject) entity));
|
||||||
|
|
||||||
|
CollisionUpdate regionObjectsUpdate = regionObjectsUpdateBuilder.build();
|
||||||
|
apply(regionObjectsUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a {@link CollisionUpdate} to the game world.
|
||||||
|
*
|
||||||
|
* @param update The update to apply.
|
||||||
|
*/
|
||||||
|
public void apply(CollisionUpdate update) {
|
||||||
|
Region prev = null;
|
||||||
|
|
||||||
|
CollisionUpdateType type = update.getType();
|
||||||
|
Map<Position, Collection<DirectionFlag>> flags = update.getFlags().asMap();
|
||||||
|
|
||||||
|
for (Map.Entry<Position, Collection<DirectionFlag>> flag : flags.entrySet()) {
|
||||||
|
Position position = flag.getKey();
|
||||||
|
Collection<DirectionFlag> directionFlags = flag.getValue();
|
||||||
|
|
||||||
|
int height = position.getHeight();
|
||||||
|
if (bridgeTiles.contains(new Position(position.getX(), position.getY(), 1))) {
|
||||||
|
if (--height < 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev == null || !prev.contains(position)) {
|
||||||
|
prev = regionRepository.fromPosition(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
int localX = position.getX() % Region.SIZE, localY = position.getY() % Region.SIZE;
|
||||||
|
|
||||||
|
CollisionMatrix matrix = prev.getMatrix(height);
|
||||||
|
CollisionFlag[] mobs = CollisionFlag.mobs();
|
||||||
|
CollisionFlag[] projectiles = CollisionFlag.projectiles();
|
||||||
|
|
||||||
|
for (DirectionFlag directionFlag : directionFlags) {
|
||||||
|
Direction direction = directionFlag.getDirection();
|
||||||
|
if (direction == Direction.NONE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int orientation = direction.toInteger();
|
||||||
|
if (directionFlag.isImpenetrable()) {
|
||||||
|
flag(type, matrix, localX, localY, projectiles[orientation]);
|
||||||
|
}
|
||||||
|
|
||||||
|
flag(type, matrix, localX, localY, mobs[orientation]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a {@link CollisionUpdate} flag to a {@link CollisionMatrix}.
|
||||||
|
*
|
||||||
|
* @param type The type of update to apply.
|
||||||
|
* @param matrix The matrix the update is being applied to.
|
||||||
|
* @param localX The local X position of the tile the flag represents.
|
||||||
|
* @param localY The local Y position of the tile the flag represents.
|
||||||
|
* @param flag The flag to update.
|
||||||
|
*/
|
||||||
|
private void flag(CollisionUpdateType type, CollisionMatrix matrix, int localX, int localY, CollisionFlag flag) {
|
||||||
|
if (type == CollisionUpdateType.ADDING) {
|
||||||
|
matrix.flag(localX, localY, flag);
|
||||||
|
} else {
|
||||||
|
matrix.clear(localX, localY, flag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks a tile as completely untraversable from all directions.
|
||||||
|
*
|
||||||
|
* @param position The {@link Position} of the tile.
|
||||||
|
*/
|
||||||
|
public void markBlocked(Position position) {
|
||||||
|
blockedTiles.add(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks a tile as part of a bridge.
|
||||||
|
*
|
||||||
|
* @param position The {@link Position} of the tile.
|
||||||
|
*/
|
||||||
|
public void markBridged(Position position) {
|
||||||
|
bridgeTiles.add(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given {@link EntityType} can traverse to the next tile from {@code position} in the given
|
||||||
|
* {@code direction}.
|
||||||
|
*
|
||||||
|
* @param position The current position of the entity.
|
||||||
|
* @param type The type of the entity.
|
||||||
|
* @param direction The direction the entity is travelling.
|
||||||
|
* @return {@code true} if next tile is traversable, {@code false} otherwise.
|
||||||
|
*/
|
||||||
|
public boolean traversable(Position position, EntityType type, Direction direction) {
|
||||||
|
Position next = position.step(1, direction);
|
||||||
|
Region region = regionRepository.fromPosition(next);
|
||||||
|
|
||||||
|
if (!region.traversable(next, type, direction)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction.isDiagonal()) {
|
||||||
|
for (Direction component : Direction.diagonalComponents(direction)) {
|
||||||
|
next = position.step(1, component);
|
||||||
|
|
||||||
|
if (!region.contains(next)) {
|
||||||
|
region = regionRepository.fromPosition(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!region.traversable(next, type, component)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -18,12 +18,17 @@ public final class CollisionMatrix {
|
|||||||
/**
|
/**
|
||||||
* Indicates that all types of traversal are allowed.
|
* Indicates that all types of traversal are allowed.
|
||||||
*/
|
*/
|
||||||
private static final byte ALL_ALLOWED = 0b0000_0000;
|
private static final short ALL_ALLOWED = 0b00000000_00000000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that no types of traversal are allowed.
|
* Indicates that no types of traversal are allowed.
|
||||||
*/
|
*/
|
||||||
private static final byte ALL_BLOCKED = (byte) 0b1111_1111;
|
private static final short ALL_BLOCKED = (short) 0b11111111_11111111;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that projectiles may traverse this tile, but mobs may not.
|
||||||
|
*/
|
||||||
|
private static final short ALL_MOBS_BLOCKED = (short) 0b11111111_00000000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an array of CollisionMatrix objects, all of the specified width and length.
|
* Creates an array of CollisionMatrix objects, all of the specified width and length.
|
||||||
@@ -45,9 +50,9 @@ public final class CollisionMatrix {
|
|||||||
private final int length;
|
private final int length;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The collision matrix, as a {@code byte} array.
|
* The collision matrix, as a {@code short} array.
|
||||||
*/
|
*/
|
||||||
private final byte[] matrix;
|
private final short[] matrix;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The width of the matrix.
|
* The width of the matrix.
|
||||||
@@ -63,7 +68,7 @@ public final class CollisionMatrix {
|
|||||||
public CollisionMatrix(int width, int length) {
|
public CollisionMatrix(int width, int length) {
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.length = length;
|
this.length = length;
|
||||||
matrix = new byte[width * length];
|
matrix = new short[width * length];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,7 +128,7 @@ public final class CollisionMatrix {
|
|||||||
* @param y The y coordinate.
|
* @param y The y coordinate.
|
||||||
*/
|
*/
|
||||||
public void block(int x, int y) {
|
public void block(int x, int y) {
|
||||||
set(x, y, ALL_BLOCKED);
|
block(x, y, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -135,7 +140,18 @@ public final class CollisionMatrix {
|
|||||||
* @param flag The CollisionFlag.
|
* @param flag The CollisionFlag.
|
||||||
*/
|
*/
|
||||||
public void clear(int x, int y, CollisionFlag flag) {
|
public void clear(int x, int y, CollisionFlag flag) {
|
||||||
set(x, y, (byte) (matrix[indexOf(x, y)] & ~flag.asByte()));
|
set(x, y, (short) (matrix[indexOf(x, y)] & ~flag.asShort()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an additional {@link CollisionFlag} for the specified coordinate pair.
|
||||||
|
*
|
||||||
|
* @param x The x coordinate.
|
||||||
|
* @param y The y coordinate.
|
||||||
|
* @param flag The CollisionFlag.
|
||||||
|
*/
|
||||||
|
public void flag(int x, int y, CollisionFlag flag) {
|
||||||
|
matrix[indexOf(x, y)] |= flag.asShort();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -147,7 +163,7 @@ public final class CollisionMatrix {
|
|||||||
* @return {@code true} iff the CollisionFlag is set.
|
* @return {@code true} iff the CollisionFlag is set.
|
||||||
*/
|
*/
|
||||||
public boolean flagged(int x, int y, CollisionFlag flag) {
|
public boolean flagged(int x, int y, CollisionFlag flag) {
|
||||||
return (get(x, y) & flag.asByte()) != 0;
|
return (get(x, y) & flag.asShort()) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -158,7 +174,7 @@ public final class CollisionMatrix {
|
|||||||
* @return The value.
|
* @return The value.
|
||||||
*/
|
*/
|
||||||
public int get(int x, int y) {
|
public int get(int x, int y) {
|
||||||
return matrix[indexOf(x, y)] & 0xFF;
|
return matrix[indexOf(x, y)] & 0xFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -171,6 +187,17 @@ public final class CollisionMatrix {
|
|||||||
set(x, y, ALL_ALLOWED);
|
set(x, y, ALL_ALLOWED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets all cells in this matrix.
|
||||||
|
*/
|
||||||
|
public void reset() {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
for (int y = 0; y < width; y++) {
|
||||||
|
reset(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets (i.e. sets to {@code true}) the value of the specified {@link CollisionFlag} for the specified coordinate
|
* Sets (i.e. sets to {@code true}) the value of the specified {@link CollisionFlag} for the specified coordinate
|
||||||
* pair.
|
* pair.
|
||||||
@@ -180,13 +207,13 @@ public final class CollisionMatrix {
|
|||||||
* @param flag The CollisionFlag.
|
* @param flag The CollisionFlag.
|
||||||
*/
|
*/
|
||||||
public void set(int x, int y, CollisionFlag flag) {
|
public void set(int x, int y, CollisionFlag flag) {
|
||||||
set(x, y, flag.asByte());
|
set(x, y, flag.asShort());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return MoreObjects.toStringHelper(this).add("width", width).add("length", length)
|
return MoreObjects.toStringHelper(this).add("width", width).add("length", length)
|
||||||
.add("matrix", Arrays.toString(matrix)).toString();
|
.add("matrix", Arrays.toString(matrix)).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -201,23 +228,23 @@ public final class CollisionMatrix {
|
|||||||
*/
|
*/
|
||||||
public boolean untraversable(int x, int y, EntityType entity, Direction direction) {
|
public boolean untraversable(int x, int y, EntityType entity, Direction direction) {
|
||||||
CollisionFlag[] flags = CollisionFlag.forType(entity);
|
CollisionFlag[] flags = CollisionFlag.forType(entity);
|
||||||
int north = 0, east = 1, south = 2, west = 3;
|
int northwest = 0, north = 1, northeast = 2, west = 3, east = 4, southwest = 5, south = 6, southeast = 7;
|
||||||
|
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case NORTH_WEST:
|
case NORTH_WEST:
|
||||||
return flagged(x, y, flags[south]) || flagged(x, y, flags[east]);
|
return flagged(x, y, flags[southeast]) || flagged(x, y, flags[south]) || flagged(x, y, flags[east]);
|
||||||
case NORTH:
|
case NORTH:
|
||||||
return flagged(x, y, flags[south]);
|
return flagged(x, y, flags[south]);
|
||||||
case NORTH_EAST:
|
case NORTH_EAST:
|
||||||
return flagged(x, y, flags[south]) || flagged(x, y, flags[west]);
|
return flagged(x, y, flags[southwest]) || flagged(x, y, flags[south]) || flagged(x, y, flags[west]);
|
||||||
case EAST:
|
case EAST:
|
||||||
return flagged(x, y, flags[west]);
|
return flagged(x, y, flags[west]);
|
||||||
case SOUTH_EAST:
|
case SOUTH_EAST:
|
||||||
return flagged(x, y, flags[north]) || flagged(x, y, flags[west]);
|
return flagged(x, y, flags[northwest]) || flagged(x, y, flags[north]) || flagged(x, y, flags[west]);
|
||||||
case SOUTH:
|
case SOUTH:
|
||||||
return flagged(x, y, flags[north]);
|
return flagged(x, y, flags[north]);
|
||||||
case SOUTH_WEST:
|
case SOUTH_WEST:
|
||||||
return flagged(x, y, flags[north]) || flagged(x, y, flags[east]);
|
return flagged(x, y, flags[northeast]) || flagged(x, y, flags[north]) || flagged(x, y, flags[east]);
|
||||||
case WEST:
|
case WEST:
|
||||||
return flagged(x, y, flags[east]);
|
return flagged(x, y, flags[east]);
|
||||||
default:
|
default:
|
||||||
@@ -246,7 +273,7 @@ public final class CollisionMatrix {
|
|||||||
* @param y The y coordinate.
|
* @param y The y coordinate.
|
||||||
* @param value The value.
|
* @param value The value.
|
||||||
*/
|
*/
|
||||||
private void set(int x, int y, byte value) {
|
private void set(int x, int y, short value) {
|
||||||
matrix[indexOf(x, y)] = value;
|
matrix[indexOf(x, y)] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package org.apollo.game.model.area.collision;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.collect.*;
|
||||||
|
import org.apollo.cache.def.ObjectDefinition;
|
||||||
|
import org.apollo.game.model.Direction;
|
||||||
|
import org.apollo.game.model.Position;
|
||||||
|
import org.apollo.game.model.entity.obj.GameObject;
|
||||||
|
import org.apollo.game.model.entity.obj.ObjectType;
|
||||||
|
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A global update to the collision matrices.
|
||||||
|
*/
|
||||||
|
public final class CollisionUpdate {
|
||||||
|
/**
|
||||||
|
* The type of this update.
|
||||||
|
*/
|
||||||
|
private final CollisionUpdateType type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mapping of {@link Position}s to a set of their {@link DirectionFlag}s.
|
||||||
|
*/
|
||||||
|
private final Multimap<Position, DirectionFlag> flags;
|
||||||
|
|
||||||
|
public CollisionUpdate(CollisionUpdateType type, Multimap<Position, DirectionFlag> flags) {
|
||||||
|
this.type = type;
|
||||||
|
this.flags = flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the type of this update (ADDING, or REMOVING).
|
||||||
|
*
|
||||||
|
* @return The type of this update.
|
||||||
|
*/
|
||||||
|
public CollisionUpdateType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the mapping of tiles -> flags contained in this update.
|
||||||
|
*
|
||||||
|
* @return The flags contained in this update.
|
||||||
|
*/
|
||||||
|
public Multimap<Position, DirectionFlag> getFlags() {
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A directional flag in a {@code CollisionUpdate}. Consists of a {@code direction} and a flag indicating whether
|
||||||
|
* that tile is impenetrable as well as untraversable.
|
||||||
|
*/
|
||||||
|
public static final class DirectionFlag {
|
||||||
|
private final boolean impenetrable;
|
||||||
|
private final Direction direction;
|
||||||
|
|
||||||
|
public DirectionFlag(boolean impenetrable, Direction direction) {
|
||||||
|
this.impenetrable = impenetrable;
|
||||||
|
this.direction = direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
DirectionFlag that = (DirectionFlag) o;
|
||||||
|
|
||||||
|
if (impenetrable != that.impenetrable) return false;
|
||||||
|
return direction == that.direction;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = (impenetrable ? 1 : 0);
|
||||||
|
result = 31 * result + direction.hashCode();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this flag represents an impenetrable direction.
|
||||||
|
*
|
||||||
|
* @return {@code true} if this flag represents an impenetrable direction, {@code false} otherwise.
|
||||||
|
*/
|
||||||
|
public boolean isImpenetrable() {
|
||||||
|
return impenetrable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the direction this flag represents.
|
||||||
|
*
|
||||||
|
* @return The direction this flag represents.
|
||||||
|
*/
|
||||||
|
public Direction getDirection() {
|
||||||
|
return direction;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private final Multimap<Position, DirectionFlag> flags;
|
||||||
|
private CollisionUpdateType type;
|
||||||
|
|
||||||
|
public Builder() {
|
||||||
|
this.flags = MultimapBuilder.hashKeys().hashSetValues().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the type of the {@link CollisionUpdate}. Can only be called once.
|
||||||
|
*
|
||||||
|
* @param type The type of collision update to use.
|
||||||
|
*/
|
||||||
|
public void type(CollisionUpdateType type) {
|
||||||
|
Preconditions.checkState(type != null, "update type has already been set");
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the tile at the given {@code position} as untraversable in the given directions.
|
||||||
|
*
|
||||||
|
* @param position The world position of the tile.
|
||||||
|
* @param directions The directions that are untraversable from this tile.
|
||||||
|
*/
|
||||||
|
public void tile(Position position, boolean impenetrable, Direction... directions) {
|
||||||
|
if (directions.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream.of(directions).forEach(direction -> flags.put(position, new DirectionFlag(impenetrable, direction)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag a wall in the CollisionUpdate. When constructing a CollisionMatrix, the flags for a wall are represented
|
||||||
|
* as the tile the wall exists on and the tile one step in the facing direction. So for a wall facing south,
|
||||||
|
* the tile one step to the south be flagged as untraversable from the north.
|
||||||
|
*
|
||||||
|
* @param position The position of the wall.
|
||||||
|
* @param impenetrable If projectiles can pass through this wall.
|
||||||
|
* @param orientation The facing direction of this wall.
|
||||||
|
*/
|
||||||
|
public void wall(Position position, boolean impenetrable, Direction orientation) {
|
||||||
|
tile(position, impenetrable, orientation);
|
||||||
|
tile(position.step(1, orientation), impenetrable, orientation.opposite());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag a larger corner wall in the CollisionUpdate. A corner is represented by the 2 directions that it faces,
|
||||||
|
* and the 2 tiles in both directions. For example, when a tile is facing north its facing directions
|
||||||
|
* are north and east, so the position of the object will be untraversable from those directions. Additionally,
|
||||||
|
* the tile 1 step to the north, and 1 step to the east will be untraversable from the opposite directions of
|
||||||
|
* north and east respectively.
|
||||||
|
* <p>
|
||||||
|
* todo: "large corner wall", is that really what this is?
|
||||||
|
*
|
||||||
|
* @param position The position of the corner wall.
|
||||||
|
* @param impenetrable If projectiles can pass through this corner wall.
|
||||||
|
* @param orientation The direction of this corner wall
|
||||||
|
*/
|
||||||
|
public void largeCornerWall(Position position, boolean impenetrable, Direction orientation) {
|
||||||
|
Direction[] directions = Direction.diagonalComponents(orientation);
|
||||||
|
|
||||||
|
tile(position, impenetrable, directions);
|
||||||
|
|
||||||
|
for (Direction direction : directions) {
|
||||||
|
tile(position.step(1, direction), impenetrable, direction.opposite());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag a collision update for the given {@link GameObject}.
|
||||||
|
*
|
||||||
|
* @param object The object to update collision flags for.
|
||||||
|
*/
|
||||||
|
public void object(GameObject object) {
|
||||||
|
ObjectDefinition definition = object.getDefinition();
|
||||||
|
Position position = object.getPosition();
|
||||||
|
int type = object.getType();
|
||||||
|
|
||||||
|
if (!unwalkable(definition, type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x = position.getX(), y = position.getY(), height = position.getHeight();
|
||||||
|
int width = definition.getWidth(), length = definition.getLength();
|
||||||
|
boolean impenetrable = definition.isImpenetrable();
|
||||||
|
int orientation = object.getOrientation();
|
||||||
|
|
||||||
|
// north / south for walls, north east / south west for corners
|
||||||
|
if (orientation == 1 || orientation == 3) {
|
||||||
|
width = definition.getLength();
|
||||||
|
length = definition.getWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == ObjectType.FLOOR_DECORATION.getValue()) {
|
||||||
|
if (definition.isInteractive() && definition.isSolid()) {
|
||||||
|
tile(new Position(x, y, height), impenetrable, Direction.NESW);
|
||||||
|
}
|
||||||
|
} else if (type >= ObjectType.DIAGONAL_WALL.getValue() && type < ObjectType.FLOOR_DECORATION.getValue()) {
|
||||||
|
for (int dx = 0; dx < width; dx++) {
|
||||||
|
for (int dy = 0; dy < length; dy++) {
|
||||||
|
tile(new Position(x + dx, y + dy, height), impenetrable, Direction.NESW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (type == ObjectType.LENGTHWISE_WALL.getValue()) {
|
||||||
|
wall(position, impenetrable, Direction.WNES[orientation]);
|
||||||
|
} else if (type == ObjectType.TRIANGULAR_CORNER.getValue()
|
||||||
|
|| type == ObjectType.RECTANGULAR_CORNER.getValue()) {
|
||||||
|
wall(position, impenetrable, Direction.WNES_DIAGONAL[orientation]);
|
||||||
|
} else if (type == ObjectType.WALL_CORNER.getValue()) {
|
||||||
|
largeCornerWall(position, impenetrable, Direction.WNES_DIAGONAL[orientation]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link CollisionUpdate}.
|
||||||
|
*
|
||||||
|
* @return A new CollisionUpdate with the flags in this builder.
|
||||||
|
*/
|
||||||
|
public CollisionUpdate build() {
|
||||||
|
Preconditions.checkNotNull(type, "update type must not be null");
|
||||||
|
return new CollisionUpdate(type, Multimaps.unmodifiableMultimap(flags));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not an object with the specified {@link ObjectDefinition} and {@code type} should result in
|
||||||
|
* the tile(s) it is located on being blocked.
|
||||||
|
*
|
||||||
|
* @param definition The {@link ObjectDefinition} of the object.
|
||||||
|
* @param type The type of the object.
|
||||||
|
* @return {@code true} iff the tile(s) the object is on should be blocked.
|
||||||
|
*/
|
||||||
|
private static boolean unwalkable(ObjectDefinition definition, int type) {
|
||||||
|
boolean isSolidFloorDecoration = type == ObjectType.FLOOR_DECORATION.getValue() && definition.isInteractive();
|
||||||
|
|
||||||
|
boolean isWall = type >= ObjectType.LENGTHWISE_WALL.getValue()
|
||||||
|
&& type <= ObjectType.RECTANGULAR_CORNER.getValue() || type == ObjectType.DIAGONAL_WALL.getValue();
|
||||||
|
|
||||||
|
boolean isRoof = type > ObjectType.DIAGONAL_INTERACTABLE.getValue()
|
||||||
|
&& type < ObjectType.FLOOR_DECORATION.getValue();
|
||||||
|
|
||||||
|
boolean isSolidInteractable = (type == ObjectType.DIAGONAL_INTERACTABLE.getValue()
|
||||||
|
|| type == ObjectType.INTERACTABLE.getValue()) && definition.isSolid();
|
||||||
|
|
||||||
|
return isWall || isRoof || isSolidInteractable || isSolidFloorDecoration;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package org.apollo.game.model.area.collision;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An enum which represents the type of a {@link CollisionUpdate}.
|
||||||
|
*/
|
||||||
|
public enum CollisionUpdateType {
|
||||||
|
/**
|
||||||
|
* Indicates that a {@link CollisionUpdate} will be adding new flags to collision matrices.
|
||||||
|
*/
|
||||||
|
ADDING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that a {@link CollisionUpdate} will be clearing existing flags from collision matrices.
|
||||||
|
*/
|
||||||
|
REMOVING
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
package org.apollo.game.model.area.collision;
|
||||||
|
|
||||||
|
import org.apollo.game.model.area.EntityUpdateType;
|
||||||
|
import org.apollo.game.model.area.Region;
|
||||||
|
import org.apollo.game.model.area.RegionListener;
|
||||||
|
import org.apollo.game.model.entity.Entity;
|
||||||
|
import org.apollo.game.model.entity.EntityType;
|
||||||
|
import org.apollo.game.model.entity.obj.GameObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link RegionListener} which listens on object addition / removal events and applies
|
||||||
|
* the respective {@link CollisionUpdate}.
|
||||||
|
*/
|
||||||
|
public final class GameObjectCollisionUpdateListener implements RegionListener {
|
||||||
|
/**
|
||||||
|
* The {@link CollisionManager} to apply updates to.
|
||||||
|
*/
|
||||||
|
private CollisionManager collisionManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link GameObjectCollisionUpdateListener}.
|
||||||
|
*
|
||||||
|
* @param collisionManager The {@link CollisionManager} that collision updates will be applied to.
|
||||||
|
*/
|
||||||
|
public GameObjectCollisionUpdateListener(CollisionManager collisionManager) {
|
||||||
|
this.collisionManager = collisionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(Region region, Entity entity, EntityUpdateType type) {
|
||||||
|
EntityType entityType = entity.getEntityType();
|
||||||
|
|
||||||
|
if (entityType != EntityType.STATIC_OBJECT && entityType != EntityType.DYNAMIC_OBJECT) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CollisionUpdate.Builder objectUpdateBuilder = new CollisionUpdate.Builder();
|
||||||
|
if (type == EntityUpdateType.ADD) {
|
||||||
|
objectUpdateBuilder.type(CollisionUpdateType.ADDING);
|
||||||
|
} else {
|
||||||
|
objectUpdateBuilder.type(CollisionUpdateType.REMOVING);
|
||||||
|
}
|
||||||
|
|
||||||
|
objectUpdateBuilder.object((GameObject) entity);
|
||||||
|
|
||||||
|
CollisionUpdate objectUpdate = objectUpdateBuilder.build();
|
||||||
|
collisionManager.apply(objectUpdate);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
package org.apollo.game.model.entity;
|
package org.apollo.game.model.entity;
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.Deque;
|
|
||||||
import java.util.Queue;
|
|
||||||
|
|
||||||
import org.apollo.game.model.Direction;
|
import org.apollo.game.model.Direction;
|
||||||
import org.apollo.game.model.Position;
|
import org.apollo.game.model.Position;
|
||||||
|
import org.apollo.game.model.World;
|
||||||
import org.apollo.game.model.area.Region;
|
import org.apollo.game.model.area.Region;
|
||||||
import org.apollo.game.model.area.RegionRepository;
|
import org.apollo.game.model.area.RegionRepository;
|
||||||
|
import org.apollo.game.model.area.collision.CollisionFlag;
|
||||||
|
import org.apollo.game.model.area.collision.CollisionManager;
|
||||||
|
import org.apollo.game.model.area.collision.CollisionMatrix;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A queue of {@link Direction}s which a {@link Mob} will follow.
|
* A queue of {@link Direction}s which a {@link Mob} will follow.
|
||||||
@@ -126,19 +133,33 @@ public final class WalkingQueue {
|
|||||||
|
|
||||||
Direction firstDirection = Direction.NONE;
|
Direction firstDirection = Direction.NONE;
|
||||||
Direction secondDirection = Direction.NONE;
|
Direction secondDirection = Direction.NONE;
|
||||||
|
World world = mob.getWorld();
|
||||||
|
CollisionManager collisionManager = world.getCollisionManager();
|
||||||
|
|
||||||
Position next = points.poll();
|
Position next = points.poll();
|
||||||
if (next != null) {
|
if (next != null) {
|
||||||
previousPoints.add(next);
|
|
||||||
firstDirection = Direction.between(position, next);
|
firstDirection = Direction.between(position, next);
|
||||||
position = new Position(next.getX(), next.getY(), height);
|
|
||||||
|
|
||||||
if (running) {
|
if (!collisionManager.traversable(position, EntityType.NPC, firstDirection)) {
|
||||||
next = points.poll();
|
clear();
|
||||||
if (next != null) {
|
firstDirection = Direction.NONE;
|
||||||
previousPoints.add(next);
|
} else {
|
||||||
secondDirection = Direction.between(position, next);
|
previousPoints.add(next);
|
||||||
position = new Position(next.getX(), next.getY(), height);
|
position = new Position(next.getX(), next.getY(), height);
|
||||||
|
|
||||||
|
if (running) {
|
||||||
|
next = points.poll();
|
||||||
|
if (next != null) {
|
||||||
|
secondDirection = Direction.between(position, next);
|
||||||
|
|
||||||
|
if (!collisionManager.traversable(position, EntityType.NPC, secondDirection)) {
|
||||||
|
clear();
|
||||||
|
secondDirection = Direction.NONE;
|
||||||
|
} else {
|
||||||
|
previousPoints.add(next);
|
||||||
|
position = new Position(next.getX(), next.getY(), height);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,142 +1,145 @@
|
|||||||
package org.apollo.game.model.entity.path;
|
package org.apollo.game.model.entity.path;
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.PriorityQueue;
|
import java.util.PriorityQueue;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.apollo.game.model.Direction;
|
import org.apollo.game.model.Direction;
|
||||||
import org.apollo.game.model.Position;
|
import org.apollo.game.model.Position;
|
||||||
import org.apollo.game.model.area.RegionRepository;
|
import org.apollo.game.model.World;
|
||||||
|
import org.apollo.game.model.area.RegionRepository;
|
||||||
/**
|
import org.apollo.game.model.area.collision.CollisionManager;
|
||||||
* A {@link PathfindingAlgorithm} that utilises the A* algorithm to find a solution.
|
|
||||||
* <p>
|
/**
|
||||||
* This implementation utilises a {@link PriorityQueue} of open {@link Node}s, in addition to the usual {@link HashSet}.
|
* A {@link PathfindingAlgorithm} that utilises the A* algorithm to find a solution.
|
||||||
* This allows for logarithmic-time finding of the cheapest element (as opposed to the linear time associated with
|
* <p>
|
||||||
* iterating over the set), whilst still maintaining the constant time contains and remove of the set.
|
* This implementation utilises a {@link PriorityQueue} of open {@link Node}s, in addition to the usual {@link HashSet}.
|
||||||
* <p>
|
* This allows for logarithmic-time finding of the cheapest element (as opposed to the linear time associated with
|
||||||
* This implementation also avoids the linear-time removal from the queue by polling until the first open node is found
|
* iterating over the set), whilst still maintaining the constant time contains and remove of the set.
|
||||||
* when identifying the cheapest node.
|
* <p>
|
||||||
*
|
* This implementation also avoids the linear-time removal from the queue by polling until the first open node is found
|
||||||
* @author Major
|
* when identifying the cheapest node.
|
||||||
*/
|
*
|
||||||
public final class AStarPathfindingAlgorithm extends PathfindingAlgorithm {
|
* @author Major
|
||||||
|
*/
|
||||||
/**
|
public final class AStarPathfindingAlgorithm extends PathfindingAlgorithm {
|
||||||
* The Heuristic used by this PathfindingAlgorithm.
|
|
||||||
*/
|
/**
|
||||||
private final Heuristic heuristic;
|
* The Heuristic used by this PathfindingAlgorithm.
|
||||||
|
*/
|
||||||
/**
|
private final Heuristic heuristic;
|
||||||
* Creates the A* pathfinding algorithm with the specified {@link Heuristic}.
|
|
||||||
*
|
/**
|
||||||
* @param repository The {@link RegionRepository}.
|
* Creates the A* pathfinding algorithm with the specified {@link Heuristic}.
|
||||||
* @param heuristic The Heuristic.
|
*
|
||||||
*/
|
* @param collisionManager The {@link CollisionManager} used to check if there is a collision
|
||||||
public AStarPathfindingAlgorithm(RegionRepository repository, Heuristic heuristic) {
|
* between two {@link Position}s in a path.
|
||||||
super(repository);
|
* @param heuristic The Heuristic.
|
||||||
this.heuristic = heuristic;
|
*/
|
||||||
}
|
public AStarPathfindingAlgorithm(CollisionManager collisionManager, Heuristic heuristic) {
|
||||||
|
super(collisionManager);
|
||||||
@Override
|
this.heuristic = heuristic;
|
||||||
public Deque<Position> find(Position origin, Position target) {
|
}
|
||||||
Map<Position, Node> nodes = new HashMap<>();
|
|
||||||
Node start = new Node(origin), end = new Node(target);
|
@Override
|
||||||
nodes.put(origin, start);
|
public Deque<Position> find(Position origin, Position target) {
|
||||||
nodes.put(target, end);
|
Map<Position, Node> nodes = new HashMap<>();
|
||||||
|
Node start = new Node(origin), end = new Node(target);
|
||||||
Set<Node> open = new HashSet<>();
|
nodes.put(origin, start);
|
||||||
Queue<Node> sorted = new PriorityQueue<>();
|
nodes.put(target, end);
|
||||||
open.add(start);
|
|
||||||
sorted.add(start);
|
Set<Node> open = new HashSet<>();
|
||||||
|
Queue<Node> sorted = new PriorityQueue<>();
|
||||||
do {
|
open.add(start);
|
||||||
Node active = getCheapest(sorted);
|
sorted.add(start);
|
||||||
Position position = active.getPosition();
|
|
||||||
|
do {
|
||||||
if (position.equals(target)) {
|
Node active = getCheapest(sorted);
|
||||||
break;
|
Position position = active.getPosition();
|
||||||
}
|
|
||||||
|
if (position.equals(target)) {
|
||||||
open.remove(active);
|
break;
|
||||||
active.close();
|
}
|
||||||
|
|
||||||
int x = position.getX(), y = position.getY();
|
open.remove(active);
|
||||||
for (int nextX = x - 1; nextX <= x + 1; nextX++) {
|
active.close();
|
||||||
for (int nextY = y - 1; nextY <= y + 1; nextY++) {
|
|
||||||
if (nextX == x && nextY == y) {
|
int x = position.getX(), y = position.getY();
|
||||||
continue;
|
for (int nextX = x - 1; nextX <= x + 1; nextX++) {
|
||||||
}
|
for (int nextY = y - 1; nextY <= y + 1; nextY++) {
|
||||||
|
if (nextX == x && nextY == y) {
|
||||||
Position adjacent = new Position(nextX, nextY);
|
continue;
|
||||||
Direction direction = Direction.between(adjacent, position);
|
}
|
||||||
if (traversable(adjacent, direction)) {
|
|
||||||
Node node = nodes.computeIfAbsent(adjacent, Node::new);
|
Position adjacent = new Position(nextX, nextY);
|
||||||
compare(active, node, open, sorted, heuristic);
|
Direction direction = Direction.between(adjacent, position);
|
||||||
}
|
if (traversable(adjacent, direction)) {
|
||||||
}
|
Node node = nodes.computeIfAbsent(adjacent, Node::new);
|
||||||
}
|
compare(active, node, open, sorted, heuristic);
|
||||||
} while (!open.isEmpty());
|
}
|
||||||
|
}
|
||||||
Deque<Position> shortest = new ArrayDeque<>();
|
}
|
||||||
Node active = end;
|
} while (!open.isEmpty());
|
||||||
|
|
||||||
if (active.hasParent()) {
|
Deque<Position> shortest = new ArrayDeque<>();
|
||||||
Position position = active.getPosition();
|
Node active = end;
|
||||||
|
|
||||||
while (!origin.equals(position)) {
|
if (active.hasParent()) {
|
||||||
shortest.addFirst(position);
|
Position position = active.getPosition();
|
||||||
active = active.getParent(); // If the target has a parent then all of the others will.
|
|
||||||
position = active.getPosition();
|
while (!origin.equals(position)) {
|
||||||
}
|
shortest.addFirst(position);
|
||||||
}
|
active = active.getParent(); // If the target has a parent then all of the others will.
|
||||||
|
position = active.getPosition();
|
||||||
return shortest;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return shortest;
|
||||||
* Compares the two specified {@link Node}s, adding the other node to the open {@link Set} if the estimation is
|
}
|
||||||
* cheaper than the current cost.
|
|
||||||
*
|
/**
|
||||||
* @param active The active node.
|
* Compares the two specified {@link Node}s, adding the other node to the open {@link Set} if the estimation is
|
||||||
* @param other The node to compare the active node against.
|
* cheaper than the current cost.
|
||||||
* @param open The set of open nodes.
|
*
|
||||||
* @param sorted The sorted {@link Queue} of nodes.
|
* @param active The active node.
|
||||||
* @param heuristic The {@link Heuristic} used to estimate the cost of the node.
|
* @param other The node to compare the active node against.
|
||||||
*/
|
* @param open The set of open nodes.
|
||||||
private void compare(Node active, Node other, Set<Node> open, Queue<Node> sorted, Heuristic heuristic) {
|
* @param sorted The sorted {@link Queue} of nodes.
|
||||||
int cost = active.getCost() + heuristic.estimate(active.getPosition(), other.getPosition());
|
* @param heuristic The {@link Heuristic} used to estimate the cost of the node.
|
||||||
|
*/
|
||||||
if (other.getCost() > cost) {
|
private void compare(Node active, Node other, Set<Node> open, Queue<Node> sorted, Heuristic heuristic) {
|
||||||
open.remove(other);
|
int cost = active.getCost() + heuristic.estimate(active.getPosition(), other.getPosition());
|
||||||
other.close();
|
|
||||||
} else if (other.isOpen() && !open.contains(other)) {
|
if (other.getCost() > cost) {
|
||||||
other.setCost(cost);
|
open.remove(other);
|
||||||
other.setParent(active);
|
other.close();
|
||||||
open.add(other);
|
} else if (other.isOpen() && !open.contains(other)) {
|
||||||
sorted.add(other);
|
other.setCost(cost);
|
||||||
}
|
other.setParent(active);
|
||||||
}
|
open.add(other);
|
||||||
|
sorted.add(other);
|
||||||
/**
|
}
|
||||||
* Gets the cheapest open {@link Node} from the {@link Queue}.
|
}
|
||||||
*
|
|
||||||
* @param nodes The queue of nodes.
|
/**
|
||||||
* @return The cheapest node.
|
* Gets the cheapest open {@link Node} from the {@link Queue}.
|
||||||
*/
|
*
|
||||||
private Node getCheapest(Queue<Node> nodes) {
|
* @param nodes The queue of nodes.
|
||||||
Node node = nodes.peek();
|
* @return The cheapest node.
|
||||||
while (!node.isOpen()) {
|
*/
|
||||||
nodes.poll();
|
private Node getCheapest(Queue<Node> nodes) {
|
||||||
node = nodes.peek();
|
Node node = nodes.peek();
|
||||||
}
|
while (!node.isOpen()) {
|
||||||
|
nodes.poll();
|
||||||
return node;
|
node = nodes.peek();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -5,8 +5,10 @@ import java.util.Optional;
|
|||||||
|
|
||||||
import org.apollo.game.model.Direction;
|
import org.apollo.game.model.Direction;
|
||||||
import org.apollo.game.model.Position;
|
import org.apollo.game.model.Position;
|
||||||
|
import org.apollo.game.model.World;
|
||||||
import org.apollo.game.model.area.Region;
|
import org.apollo.game.model.area.Region;
|
||||||
import org.apollo.game.model.area.RegionRepository;
|
import org.apollo.game.model.area.RegionRepository;
|
||||||
|
import org.apollo.game.model.area.collision.CollisionManager;
|
||||||
import org.apollo.game.model.entity.EntityType;
|
import org.apollo.game.model.entity.EntityType;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
@@ -18,18 +20,16 @@ import com.google.common.base.Preconditions;
|
|||||||
*/
|
*/
|
||||||
abstract class PathfindingAlgorithm {
|
abstract class PathfindingAlgorithm {
|
||||||
|
|
||||||
/**
|
private final CollisionManager collisionManager;
|
||||||
* The RegionRepository.
|
|
||||||
*/
|
|
||||||
private final RegionRepository repository;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the PathfindingAlgorithm.
|
* Creates the PathfindingAlgorithm.
|
||||||
*
|
*
|
||||||
* @param repository The {@link RegionRepository}.
|
* @param collisionManager The {@link CollisionManager} used to check if there is a collision
|
||||||
|
* between two {@link Position}s in a path.
|
||||||
*/
|
*/
|
||||||
public PathfindingAlgorithm(RegionRepository repository) {
|
public PathfindingAlgorithm(CollisionManager collisionManager) {
|
||||||
this.repository = repository;
|
this.collisionManager = collisionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,9 +84,7 @@ abstract class PathfindingAlgorithm {
|
|||||||
x--;
|
x--;
|
||||||
}
|
}
|
||||||
|
|
||||||
Position next = new Position(x, y, height);
|
if (collisionManager.traversable(current, EntityType.NPC, direction)) {
|
||||||
Region region = repository.get(next.getRegionCoordinates());
|
|
||||||
if (region.traversable(next, EntityType.NPC, direction) && (positions.length == 0 || inside(next, positions))) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,143 +1,146 @@
|
|||||||
package org.apollo.game.model.entity.path;
|
package org.apollo.game.model.entity.path;
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.apollo.game.model.Direction;
|
import org.apollo.game.model.Direction;
|
||||||
import org.apollo.game.model.Position;
|
import org.apollo.game.model.Position;
|
||||||
import org.apollo.game.model.area.RegionRepository;
|
import org.apollo.game.model.World;
|
||||||
|
import org.apollo.game.model.area.RegionRepository;
|
||||||
/**
|
import org.apollo.game.model.area.collision.CollisionManager;
|
||||||
* A very simple pathfinding algorithm that simply walks in the direction of the target until it either reaches it or is
|
|
||||||
* blocked.
|
/**
|
||||||
*
|
* A very simple pathfinding algorithm that simply walks in the direction of the target until it either reaches it or is
|
||||||
* @author Major
|
* blocked.
|
||||||
*/
|
*
|
||||||
public final class SimplePathfindingAlgorithm extends PathfindingAlgorithm {
|
* @author Major
|
||||||
|
*/
|
||||||
/**
|
public final class SimplePathfindingAlgorithm extends PathfindingAlgorithm {
|
||||||
* Creates the SimplePathfindingAlgorithm.
|
|
||||||
*
|
/**
|
||||||
* @param repository The {@link RegionRepository}.
|
* Creates the SimplePathfindingAlgorithm.
|
||||||
*/
|
*
|
||||||
public SimplePathfindingAlgorithm(RegionRepository repository) {
|
* @param collisionManager The {@link CollisionManager} used to check if there is a collision
|
||||||
super(repository);
|
* between two {@link Position}s in a path.
|
||||||
}
|
*/
|
||||||
|
public SimplePathfindingAlgorithm(CollisionManager collisionManager) {
|
||||||
/**
|
super(collisionManager);
|
||||||
* The Optional containing the boundary Positions.
|
}
|
||||||
*/
|
|
||||||
private Optional<Position[]> boundaries = Optional.empty();
|
/**
|
||||||
|
* The Optional containing the boundary Positions.
|
||||||
@Override
|
*/
|
||||||
public Deque<Position> find(Position origin, Position target) {
|
private Optional<Position[]> boundaries = Optional.empty();
|
||||||
int approximation = (int) (origin.getLongestDelta(target) * 1.5);
|
|
||||||
Deque<Position> positions = new ArrayDeque<>(approximation);
|
@Override
|
||||||
|
public Deque<Position> find(Position origin, Position target) {
|
||||||
return addHorizontal(origin, target, positions);
|
int approximation = (int) (origin.getLongestDelta(target) * 1.5);
|
||||||
}
|
Deque<Position> positions = new ArrayDeque<>(approximation);
|
||||||
|
|
||||||
/**
|
return addHorizontal(origin, target, positions);
|
||||||
* Finds a valid path from the origin {@link Position} to the target one.
|
}
|
||||||
*
|
|
||||||
* @param origin The origin Position.
|
/**
|
||||||
* @param target The target Position.
|
* Finds a valid path from the origin {@link Position} to the target one.
|
||||||
* @param boundaries The boundary Positions, which are marking as untraversable.
|
*
|
||||||
* @return The {@link Deque} containing the Positions to go through.
|
* @param origin The origin Position.
|
||||||
*/
|
* @param target The target Position.
|
||||||
public Deque<Position> find(Position origin, Position target, Position[] boundaries) {
|
* @param boundaries The boundary Positions, which are marking as untraversable.
|
||||||
this.boundaries = Optional.of(boundaries);
|
* @return The {@link Deque} containing the Positions to go through.
|
||||||
return find(origin, target);
|
*/
|
||||||
}
|
public Deque<Position> find(Position origin, Position target, Position[] boundaries) {
|
||||||
|
this.boundaries = Optional.of(boundaries);
|
||||||
/**
|
return find(origin, target);
|
||||||
* Adds the necessary and possible horizontal {@link Position}s to the existing {@link Deque}.
|
}
|
||||||
* <p>
|
|
||||||
* This method:
|
/**
|
||||||
* <ul>
|
* Adds the necessary and possible horizontal {@link Position}s to the existing {@link Deque}.
|
||||||
* <li>Adds positions horizontally until we are either horizontally aligned with the target, or the next step is not
|
* <p/>
|
||||||
* traversable.
|
* This method:
|
||||||
* <li>Checks if we are not at the target, and that either of the horizontally-adjacent positions are traversable:
|
* <ul>
|
||||||
* if so, we traverse horizontally (see {@link #addHorizontal}); if not, return the current path.
|
* <li>Adds positions horizontally until we are either horizontally aligned with the target, or the next step is not
|
||||||
* </ul>
|
* traversable.
|
||||||
*
|
* <li>Checks if we are not at the target, and that either of the horizontally-adjacent positions are traversable:
|
||||||
* @param start The current position.
|
* if so, we traverse horizontally (see {@link #addHorizontal}); if not, return the current path.
|
||||||
* @param target The target position.
|
* </ul>
|
||||||
* @param positions The deque of positions.
|
*
|
||||||
* @return The deque of positions containing the path.
|
* @param start The current position.
|
||||||
*/
|
* @param target The target position.
|
||||||
private Deque<Position> addHorizontal(Position start, Position target, Deque<Position> positions) {
|
* @param positions The deque of positions.
|
||||||
int x = start.getX(), y = start.getY(), height = start.getHeight();
|
* @return The deque of positions containing the path.
|
||||||
int dx = x - target.getX(), dy = y - target.getY();
|
*/
|
||||||
|
private Deque<Position> addHorizontal(Position start, Position target, Deque<Position> positions) {
|
||||||
if (dx > 0) {
|
int x = start.getX(), y = start.getY(), height = start.getHeight();
|
||||||
Position current = start;
|
int dx = x - target.getX(), dy = y - target.getY();
|
||||||
|
|
||||||
while (traversable(current, boundaries, Direction.WEST) && dx-- > 0) {
|
if (dx > 0) {
|
||||||
current = new Position(--x, y, height);
|
Position current = start;
|
||||||
positions.addLast(current);
|
|
||||||
}
|
while (traversable(current, boundaries, Direction.WEST) && dx-- > 0) {
|
||||||
} else if (dx < 0) {
|
current = new Position(--x, y, height);
|
||||||
Position current = start;
|
positions.addLast(current);
|
||||||
|
}
|
||||||
while (traversable(current, boundaries, Direction.EAST) && dx++ < 0) {
|
} else if (dx < 0) {
|
||||||
current = new Position(++x, y, height);
|
Position current = start;
|
||||||
positions.addLast(current);
|
|
||||||
}
|
while (traversable(current, boundaries, Direction.EAST) && dx++ < 0) {
|
||||||
}
|
current = new Position(++x, y, height);
|
||||||
|
positions.addLast(current);
|
||||||
Position last = new Position(x, y, height);
|
}
|
||||||
if (!start.equals(last) && dy != 0 && traversable(last, boundaries, dy > 0 ? Direction.SOUTH : Direction.NORTH)) {
|
}
|
||||||
return addVertical(last, target, positions);
|
|
||||||
}
|
Position last = new Position(x, y, height);
|
||||||
|
if (!start.equals(last) && dy != 0 && traversable(last, boundaries, dy > 0 ? Direction.SOUTH : Direction.NORTH)) {
|
||||||
return positions;
|
return addVertical(last, target, positions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return positions;
|
||||||
* Adds the necessary and possible vertical {@link Position}s to the existing {@link Deque}.
|
}
|
||||||
* <p>
|
|
||||||
* This method:
|
/**
|
||||||
* <ul>
|
* Adds the necessary and possible vertical {@link Position}s to the existing {@link Deque}.
|
||||||
* <li>Adds positions vertically until we are either vertically aligned with the target, or the next step is not
|
* <p/>
|
||||||
* traversable.
|
* This method:
|
||||||
* <li>Checks if we are not at the target, and that either of the horizontally-adjacent positions are traversable:
|
* <ul>
|
||||||
* if so, we traverse horizontally (see {@link #addHorizontal}); if not, return the current path.
|
* <li>Adds positions vertically until we are either vertically aligned with the target, or the next step is not
|
||||||
* </ul>
|
* traversable.
|
||||||
*
|
* <li>Checks if we are not at the target, and that either of the horizontally-adjacent positions are traversable:
|
||||||
* @param start The current position.
|
* if so, we traverse horizontally (see {@link #addHorizontal}); if not, return the current path.
|
||||||
* @param target The target position.
|
* </ul>
|
||||||
* @param positions The deque of positions.
|
*
|
||||||
* @return The deque of positions containing the path.
|
* @param start The current position.
|
||||||
*/
|
* @param target The target position.
|
||||||
private Deque<Position> addVertical(Position start, Position target, Deque<Position> positions) {
|
* @param positions The deque of positions.
|
||||||
int x = start.getX(), y = start.getY(), height = start.getHeight();
|
* @return The deque of positions containing the path.
|
||||||
int dy = y - target.getY(), dx = x - target.getX();
|
*/
|
||||||
|
private Deque<Position> addVertical(Position start, Position target, Deque<Position> positions) {
|
||||||
if (dy > 0) {
|
int x = start.getX(), y = start.getY(), height = start.getHeight();
|
||||||
Position current = start;
|
int dy = y - target.getY(), dx = x - target.getX();
|
||||||
|
|
||||||
while (traversable(current, boundaries, Direction.SOUTH) && dy-- > 0) {
|
if (dy > 0) {
|
||||||
current = new Position(x, --y, height);
|
Position current = start;
|
||||||
positions.addLast(current);
|
|
||||||
}
|
while (traversable(current, boundaries, Direction.SOUTH) && dy-- > 0) {
|
||||||
} else if (dy < 0) {
|
current = new Position(x, --y, height);
|
||||||
Position current = start;
|
positions.addLast(current);
|
||||||
|
}
|
||||||
while (traversable(current, boundaries, Direction.NORTH) && dy++ < 0) {
|
} else if (dy < 0) {
|
||||||
current = new Position(x, ++y, height);
|
Position current = start;
|
||||||
positions.addLast(current);
|
|
||||||
}
|
while (traversable(current, boundaries, Direction.NORTH) && dy++ < 0) {
|
||||||
}
|
current = new Position(x, ++y, height);
|
||||||
|
positions.addLast(current);
|
||||||
Position last = new Position(x, y, height);
|
}
|
||||||
if (!last.equals(target) && dx != 0
|
}
|
||||||
&& traversable(last, boundaries, dx > 0 ? Direction.WEST : Direction.EAST)) {
|
|
||||||
return addHorizontal(last, target, positions);
|
Position last = new Position(x, y, height);
|
||||||
}
|
if (!last.equals(target) && dx != 0
|
||||||
|
&& traversable(last, boundaries, dx > 0 ? Direction.WEST : Direction.EAST)) {
|
||||||
return positions;
|
return addHorizontal(last, target, positions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return positions;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,9 @@ import java.util.Queue;
|
|||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
import org.apollo.game.model.Position;
|
import org.apollo.game.model.Position;
|
||||||
|
import org.apollo.game.model.World;
|
||||||
import org.apollo.game.model.area.RegionRepository;
|
import org.apollo.game.model.area.RegionRepository;
|
||||||
|
import org.apollo.game.model.area.collision.CollisionManager;
|
||||||
import org.apollo.game.model.entity.Npc;
|
import org.apollo.game.model.entity.Npc;
|
||||||
import org.apollo.game.model.entity.WalkingQueue;
|
import org.apollo.game.model.entity.WalkingQueue;
|
||||||
import org.apollo.game.model.entity.path.SimplePathfindingAlgorithm;
|
import org.apollo.game.model.entity.path.SimplePathfindingAlgorithm;
|
||||||
@@ -50,11 +52,11 @@ public final class NpcMovementTask extends ScheduledTask {
|
|||||||
/**
|
/**
|
||||||
* Creates the NpcMovementTask.
|
* Creates the NpcMovementTask.
|
||||||
*
|
*
|
||||||
* @param repository The {@link RegionRepository}.
|
* @param collisionManager The {@link CollisionManager} used to check if an {@link Npc} movement is valid.
|
||||||
*/
|
*/
|
||||||
public NpcMovementTask(RegionRepository repository) {
|
public NpcMovementTask(CollisionManager collisionManager) {
|
||||||
super(DELAY, false);
|
super(DELAY, false);
|
||||||
algorithm = new SimplePathfindingAlgorithm(repository);
|
algorithm = new SimplePathfindingAlgorithm(collisionManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user