Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom bounds implementation for game objects #408

Open
wants to merge 3 commits into
base: kotlin-experiments
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ private ObjectDefinition decode(int id, ByteBuffer data) {
} else if (opcode == 60 || opcode >= 65 && opcode <= 68) {
data.getShort();
} else if (opcode == 69) {
data.get();
definition.setInteractionMask(data.get());
} else if (opcode >= 70 && opcode <= 72) {
data.getShort();
} else if (opcode == 73) {
Expand Down
23 changes: 23 additions & 0 deletions cache/src/main/java/org/apollo/cache/def/ObjectDefinition.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ public static ObjectDefinition lookup(int id) {
*/
private boolean impenetrable = true;

/**
* A mask containing the set of directions from which this object can be interacted with.
*/
private int interactionMask;

/**
* Denotes whether this object has actions associated with it or not.
*/
Expand Down Expand Up @@ -137,6 +142,15 @@ public int getId() {
return id;
}

/**
* Gets the interaction mask for this object.
*
* @return The interaction mask.
*/
public int getInteractionMask() {
return interactionMask;
}

/**
* Gets the length of this object.
*
Expand Down Expand Up @@ -236,6 +250,15 @@ public void setInteractive(boolean interactive) {
this.interactive = interactive;
}

/**
* Sets the interaction mask for this object.
*
* @param interactionMask The interaction mask.
*/
public void setInteractionMask(int interactionMask) {
this.interactionMask = interactionMask;
}

/**
* Sets the length of this object.
*
Expand Down
90 changes: 89 additions & 1 deletion game/src/main/java/org/apollo/game/model/Direction.java
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,95 @@ public static Direction[] diagonalComponents(Direction direction) {
}

/**
* Gets the opposite direction of the this direction.
* Gets the direction that is clockwise of this direction.
*
* @return The clockwise direction.
*/
public Direction clockwise() {
switch (this) {
case NORTH:
return NORTH_EAST;
case SOUTH:
return SOUTH_WEST;
case EAST:
return SOUTH_EAST;
case WEST:
return NORTH_WEST;
case NORTH_WEST:
return NORTH;
case NORTH_EAST:
return EAST;
case SOUTH_EAST:
return SOUTH;
case SOUTH_WEST:
return WEST;
}

return NONE;
}

/**
* Gets a direction that is clockwise of this direction, given the number of times to rotate.
*
* @param count The number of times to apply the rotation.
* @return The rotated direction.
*/
public Direction clockwise(int count) {
Direction direction = this;

for (int index = 0; index < count; index++) {
direction = direction.clockwise();
}

return direction;
}

/**
* Gets the direction that is counter-clockwise of this direction.
*
* @return The counter-clockwise direction.
*/
public Direction counterClockwise() {
switch (this) {
case NORTH:
return NORTH_WEST;
case SOUTH:
return SOUTH_EAST;
case EAST:
return NORTH_EAST;
case WEST:
return SOUTH_WEST;
case NORTH_WEST:
return WEST;
case NORTH_EAST:
return NORTH;
case SOUTH_EAST:
return EAST;
case SOUTH_WEST:
return SOUTH;
}

return NONE;
}

/**
* Gets a direction that is counter-clockwise of this direction, given the number of times to rotate.
*
* @param count The number of times to apply the rotation.
* @return The rotated direction.
*/
public Direction counterClockwise(int count) {
Direction direction = this;

for (int index = 0; index < count; index++) {
direction = direction.counterClockwise();
}

return direction;
}

/**
* Gets the opposite direction of this direction.
*
* @return The opposite direction.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public abstract class Entity {
/**
* The EntityBounds for this Entity.
*/
private EntityBounds bounds;
protected EntityBounds bounds;

/**
* Creates the Entity.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class EntityBounds {
*
* @param entity The entity.
*/
EntityBounds(Entity entity) {
protected EntityBounds(Entity entity) {
this.entity = entity;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ public int getWidth() {
getDefinition().getLength() : getDefinition().getWidth();
}

@Override
public GameObjectBounds getBounds() {
if(bounds == null) {
bounds = new GameObjectBounds(this);
}

return (GameObjectBounds) bounds;
}

@Override
public int hashCode() {
return packed;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package org.apollo.game.model.entity.obj;

import com.google.common.collect.ImmutableMap;
import org.apollo.game.model.Direction;
import org.apollo.game.model.Position;
import org.apollo.game.model.entity.EntityBounds;

import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
* The bounds of a {@link GameObject}.
*
* @author Steve Soltys
*/
public class GameObjectBounds extends EntityBounds {

/**
* An {@link ImmutableMap} of Directions mapped to their mask bits for a {@link GameObject} interaction mask.
*/
private static final Map<Direction, Integer> INTERACTION_MASK_BITS = ImmutableMap.<Direction, Integer>builder()
.put(Direction.NORTH, 0x1)
.put(Direction.EAST, 0x8)
.put(Direction.SOUTH, 0x4)
.put(Direction.WEST, 0x2)
.build();

/**
* The GameObject.
*/
private final GameObject gameObject;

/**
* The set of positions where the GameObject can be interacted with.
*/
private Set<Position> interactionPositions;

/**
* Creates an EntityBounds.
*
* @param gameObject The GameObject.
*/
GameObjectBounds(GameObject gameObject) {
super(gameObject);

this.gameObject = gameObject;
}

/**
* Gets the set of positions where the GameObject can be interacted with.
*
* @return The set of positions.
*/
public Set<Position> getInteractionPositions() {
if (interactionPositions == null) {
interactionPositions = computeInteractionPositions();
}

return interactionPositions;
}

/**
* Computes the set of positions where the GameObject can be interacted with.
*
* @return The set of positions.
*/
private Set<Position> computeInteractionPositions() {

switch (ObjectType.valueOf(gameObject.getType())) {
case DIAGONAL_WALL:
return getDiagonalWallInteractionPositions();

case LENGTHWISE_WALL:
return getWallInteractionPositions();

case INTERACTABLE:
return getInteractablePositions();

// TODO: Figure out the rest.
}

return Collections.emptySet();
}

/**
* Gets the set of positions for a diagonal wall where the GameObject can be interacted with.
*
* @return The set of interaction positions.
*/
private Set<Position> getDiagonalWallInteractionPositions() {
Set<Position> positions = new HashSet<>();
Direction direction = Direction.WNES[gameObject.getOrientation()];
Position position = gameObject.getPosition();

// TODO: I think this either is missing one, or has one too many. Need to confirm these directions.
positions.add(position.step(1, direction));
positions.add(position.step(1, direction.counterClockwise()));
positions.add(position.step(1, direction.counterClockwise(2)));
positions.add(position.step(1, direction.clockwise()));
positions.add(position.step(1, direction.clockwise(2)));
positions.add(position.step(1, direction.opposite()));
positions.add(position.step(1, direction.opposite().counterClockwise()));
return positions;
}

/**
* Gets the set of Directions from which the GameObject can be interacted with.
*
* @return The interactable directions.
*/
private Set<Direction> getInteractableDirections() {
Set<Direction> interactableDirections = new HashSet<>();

int interactionMask = gameObject.getDefinition().getInteractionMask();
int rotatedInteractionMask = (interactionMask << gameObject.getOrientation() & 0xf) +
(interactionMask >> 4 - gameObject.getOrientation());

for (Direction direction : Direction.NESW) {
boolean interactive = (rotatedInteractionMask & INTERACTION_MASK_BITS.get(direction)) == 0;

if (interactive) {
interactableDirections.add(direction);
}
}

return interactableDirections;
}

/**
* Gets the set of positions where the GameObject can be interacted with.
*
* @return The set of interaction positions.
*/
private Set<Position> getInteractablePositions() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need an example of this in use. I'm not sure that creating this Set all the time is optimal. Maybe roll this up into your distanced action changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@garyttierney In the distanced action we provide an Entity and check the interaction positions of that entity to decide when to trigger the action for a Mob.

Here's an example: https://gist.github.com/stevesoltys/197e7a604f8bfe495ae070c2db0c89c1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that in my example, I've made the getInteractionPositions method abstract in EntityBounds.

Set<Direction> interactableDirections = getInteractableDirections();
Set<Position> interactablePositions = new HashSet<>();

Position position = gameObject.getPosition();
int width = gameObject.getWidth();
int length = gameObject.getLength();

for (int deltaX = position.getX(); deltaX < position.getX() + width; deltaX++) {
for (int deltaY = position.getY(); deltaY < position.getY() + length; deltaY++) {

for(Direction direction : interactableDirections) {
Position deltaPosition = new Position(deltaX, deltaY, position.getHeight()).step(1, direction);

if(!contains(deltaPosition)) {
interactablePositions.add(deltaPosition);
}
}
}
}

return interactablePositions;
}

/**
* Gets the set of positions for a wall where the GameObject can be interacted with.
*
* @return The set of interaction positions.
*/
private Set<Position> getWallInteractionPositions() {
Set<Position> positions = new HashSet<>();
Direction objectDirection = Direction.WNES[gameObject.getOrientation()];
Position position = gameObject.getPosition();

positions.add(position);
positions.add(position.step(1, objectDirection));
positions.add(position.step(1, objectDirection.clockwise(2)));
positions.add(position.step(1, objectDirection.counterClockwise(2)));
return positions;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.apollo.game.model.entity.obj;

import java.util.Arrays;

/**
* The type of an object, which affects specified behaviour (such as whether it displaces existing objects). TODO
* complete this...
Expand Down Expand Up @@ -70,6 +72,18 @@ public enum ObjectType {
this.group = group;
}

/**
* Gets the type with the specified value.
*
* @param value The value.
* @return The type.
*/
public static ObjectType valueOf(int value) {
return Arrays.stream(values())
.filter(type -> type.value == value)
.findAny().orElse(null);
}

/**
* Gets the {@link ObjectGroup} of this ObjectType.
*
Expand Down