Add support for projectiles

* Adds support for short-lived entities, which are never added to the
  entity sets of the region they belong in.

* Adds support for checking the number of tiles a Mob occupies.

* Adds a ProjectileBuilder class to simplify creating projectiles.
This commit is contained in:
Gary Tierney
2016-03-02 20:05:24 +00:00
parent 761af6d97b
commit f2aced1bca
8 changed files with 614 additions and 9 deletions
@@ -0,0 +1,74 @@
package org.apollo.game.message.impl;
import org.apollo.game.model.entity.Projectile;
import org.apollo.net.message.Message;
/**
* A {@link Message} sent to the client to display a projectile in the world.
*/
public final class SendProjectileMessage extends RegionUpdateMessage {
/**
* The {@link Projectile} to be sent to the client.
*/
private final Projectile projectile;
/**
* The position offset.
*/
private final int positionOffset;
/**
* Creates the {@code SendProjectileMessage}.
*
* @param projectile The projectile to be sent to the client. Must not be {@code null}.
* @param positionOffset The position offset.
*/
public SendProjectileMessage(Projectile projectile, int positionOffset) {
this.projectile = projectile;
this.positionOffset = positionOffset;
}
/**
* Gets the projectile.
*
* @return The projectile to be sent to the client.
*/
public Projectile getProjectile() {
return projectile;
}
/**
* Gets the position offset.
*
* @return The position offset.
*/
public int getPositionOffset() {
return positionOffset;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof SendProjectileMessage) {
SendProjectileMessage other = (SendProjectileMessage) obj;
return projectile.equals(other.projectile) && positionOffset == other.positionOffset;
}
return false;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + positionOffset;
result = prime * result + projectile.hashCode();
return result;
}
@Override
public int priority() {
return LOW_PRIORITY;
}
}
@@ -136,11 +136,14 @@ public final class Region {
* @throws IllegalArgumentException If the Entity does not belong in this Region.
*/
public void addEntity(Entity entity, boolean notify) {
EntityType type = entity.getEntityType();
Position position = entity.getPosition();
checkPosition(position);
Set<Entity> local = entities.computeIfAbsent(position, key -> new HashSet<>(DEFAULT_LIST_SIZE));
local.add(entity);
if (!type.isTransient()) {
Set<Entity> local = entities.computeIfAbsent(position, key -> new HashSet<>(DEFAULT_LIST_SIZE));
local.add(entity);
}
if (notify) {
notifyListeners(entity, EntityUpdateType.ADD);
@@ -325,6 +328,12 @@ public final class Region {
* @throws IllegalArgumentException If the Entity does not belong in this Region, or if it was never added.
*/
public void removeEntity(Entity entity) {
EntityType type = entity.getEntityType();
if (type.isTransient()) {
throw new IllegalArgumentException("Tried to remove a transient Entity (" + entity + ") from " +
"(" + this + ").");
}
Position position = entity.getPosition();
checkPosition(position);
@@ -389,14 +398,13 @@ public final class Region {
if (update == EntityUpdateType.REMOVE) {
removedObjects.get(height).add(message);
} else { // TODO should this really be possible?
removedObjects.get(height).remove(inverse);
removedObjects.get(height).remove(operation.inverse());
}
updates.add(message);
} else {
updates.add(message);
updates.remove(inverse);
} else if (update == EntityUpdateType.REMOVE && !type.isTransient()) {
updates.remove(operation.inverse());
}
updates.add(message);
}
}
@@ -0,0 +1,35 @@
package org.apollo.game.model.area.update;
import org.apollo.game.message.impl.RegionUpdateMessage;
import org.apollo.game.message.impl.SendProjectileMessage;
import org.apollo.game.model.area.EntityUpdateType;
import org.apollo.game.model.area.Region;
import org.apollo.game.model.entity.Projectile;
/**
* An {@link UpdateOperation} for addition of {@link Projectile}s.
*/
public final class ProjectileUpdateOperation extends UpdateOperation<Projectile> {
/**
* Creates the ProjectileUpdateOperation.
*
* @param region The region in which the UpdateOperation occurred. Must not be {@code null}.
* @param type The type of {@link EntityUpdateType}. Must not be {@code null}.
* @param entity The {@link Entity} being added or removed. Must not be {@code null}.
*/
public ProjectileUpdateOperation(Region region, EntityUpdateType type, Projectile entity) {
super(region, type, entity);
}
@Override
protected RegionUpdateMessage add(int offset) {
return new SendProjectileMessage(entity, offset);
}
@Override
protected RegionUpdateMessage remove(int offset) {
throw new IllegalStateException("Projectiles cannot be removed.");
}
}
@@ -2,7 +2,7 @@ package org.apollo.game.model.entity;
/**
* Represents a type of {@link Entity}.
*
*
* @author Major
*/
public enum EntityType {
@@ -46,4 +46,13 @@ public enum EntityType {
return this == PLAYER || this == NPC;
}
/**
* Returns whether or not this EntityType should be short-lived (i.e. not added to its {@link Region}s
* local objects).
*
* @return {@code true} if this EntityType is short-lived.
*/
public boolean isTransient() {
return this == PROJECTILE;
}
}
@@ -466,6 +466,15 @@ public abstract class Mob extends Entity {
blockSet.add(SynchronizationBlock.createForceChatBlock(message));
}
/**
* Gets the number of tiles this mob occupies.
*
* @return The number of tiles this mob occupies.
*/
public int size() {
return definition.map(NpcDefinition::getSize).orElse(1);
}
/**
* Starts a new action, stopping the current one if it exists.
*
@@ -0,0 +1,431 @@
package org.apollo.game.model.entity;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import org.apollo.game.model.Position;
import org.apollo.game.model.World;
import org.apollo.game.model.area.EntityUpdateType;
import org.apollo.game.model.area.Region;
import org.apollo.game.model.area.update.GroupableEntity;
import org.apollo.game.model.area.update.ProjectileUpdateOperation;
/**
* A projectile fired through the game world, such as an arrow.
*
* @author Major
*/
public final class Projectile extends Entity implements GroupableEntity {
/**
* A builder for {@link Projectile}s.
*/
public static final class ProjectileBuilder {
/**
* The time, in ticks, before the projectile is fired.
*/
private int delay;
/**
* The ending {@link Position} of the Projectile.
*/
private Position destination;
/**
* The ending height of the projectile. Defaults to the most commonly used ending height (for arrows and combat
* magic).
*/
private int endHeight = 40;
/**
* The id of the graphic played as the Projectile proceeds.
*/
private int graphic;
/**
* The time, in ticks, that the Projectile is present.
*/
private int lifetime;
/**
* The pitch of the Projectile as it climbs. Defaults to the most commonly used pitch (for arrows and combat
* magic).
*/
private int pitch = 15;
/**
* The starting {@link Position} of the Projectile.
*/
private Position source;
/**
* The starting height of the projectile, relative to the height of the tile the Projectile starts on. Defaults
* to the most commonly used starting height (for arrows and combat magic).
*/
private int startHeight = 50;
/**
* The index of the target Mob (which must be 0 if the Projectile does not target a Mob).
*/
private int target;
/**
* The offset from the center of the {@code source} tile in world units. Defaults to 1/2 of
* a tile (or a {@link Mob} with a size of 1).
*/
private int offset = 64;
/**
* The {@link World} the projectile will be built in.
*/
private final World world;
/**
* Creates a new ProjectileBuilder.
*
* @param world The {@link World} the projectile will be built in.
*/
public ProjectileBuilder(World world) {
this.world = world;
}
/**
* Builds this ProjectileBuilder into a {@link Projectile}.
*
* @return The built {@link Projectile}. Will never be {@code null}.
* @throws IllegalArgumentException If {@code lifetime} is not positive.
* @throws NullPointerException If the source or destination {@link Position}s are {@code null}.
*/
public Projectile build() {
Preconditions.checkNotNull(source, "Projectile must have a source.");
Preconditions.checkNotNull(destination, "Projectile must have a destination.");
Preconditions.checkArgument(lifetime > 0, "Lifetime must be positive.");
return new Projectile(world, source, delay, destination, endHeight, graphic, lifetime, pitch,
startHeight, target, offset);
}
/**
* Sets the delay before the Projectile is fired.
*
* @param delay The delay, in ticks. Must not be negative.
* @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}.
*/
public ProjectileBuilder delay(int delay) {
this.delay = delay;
return this;
}
/**
* Sets the destination {@link Position} of the {@link Projectile}.
*
* @param destination The destination {@link Position}. Must not be {@code null}.
* @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}.
*/
public ProjectileBuilder destination(Position destination) {
this.destination = destination;
return this;
}
/**
* Sets the end height of the {@link Projectile}. Note that this is the height in 'world units',
* <strong>not</strong> the height as described in {@link Position}s.
*
* @param height The end height of the {@link Projectile}. Must be {@code 0 <= height <= 255}.
* @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}.
*/
public ProjectileBuilder endHeight(int height) {
this.endHeight = height;
return this;
}
/**
* Sets the id of the graphic played by the {@link Projectile}.
*
* @param graphic The graphic id. Must not be negative.
* @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}.
*/
public ProjectileBuilder graphic(int graphic) {
this.graphic = graphic;
return this;
}
/**
* Sets the amount of time, in ticks, that the {@link Projectile} lasts in the game world.
*
* @param lifetime The lifetime of the {@link Projectile}, in ticks. Must not be negative.
* @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}.
*/
public ProjectileBuilder lifetime(int lifetime) {
this.lifetime = lifetime;
return this;
}
/**
* Sets the pitch of the {@link Projectile}.
*
* @param pitch The pitch.
* @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}.
*/
public ProjectileBuilder pitch(int pitch) {
this.pitch = pitch;
return this;
}
/**
* Sets the {@code offset} of the {@link Projectile} from the source tile. in 'world units'.
*
* @param offset The offset from the source tile in world units.
* @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}.
*/
public ProjectileBuilder offset(int offset) {
this.offset = offset;
return this;
}
/**
* Sets the source {@link Position} of the {@link Projectile}.
*
* @param source The source {@link Projectile}. Must not be {@code null}.
* @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}.
*/
public ProjectileBuilder source(Position source) {
this.source = source;
return this;
}
/**
* Sets the starting height of the {@link Projectile}, relative to the height of the origin tile (so a value
* of 0 fires the {@link Projectile} from the exact height of the origin tile). Note that this is the height
* in 'world units', <strong>not</strong> the height as described in {@link Position}s.
*
* @param height The start height of the {@link Projectile}. Must be {@code 0 <= height <= 255}.
* @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}.
*/
public ProjectileBuilder startHeight(int height) {
this.startHeight = height;
return this;
}
/**
* Sets the target of the {@link Projectile}.
*
* @param mob The {@link Mob} the {@link Projectile} is targeting. Must not be {@code null}.
* @return This {@link ProjectileBuilder}, for chaining. Will never be {@code null}.
*/
public ProjectileBuilder target(Mob mob) {
int mobIndex = mob.getIndex() + 1;
destination = mob.getPosition();
target = mob.getEntityType() == EntityType.NPC ? mobIndex : -mobIndex;
offset = mob.size() * 64;
return this;
}
}
/**
* Creates a new {@link ProjectileBuilder}.
*
* @param world The {@link World} the Projectile will be created in. Must not be {@code null}.
* @return The {@link ProjectileBuilder}. Will never be {@code null}.
*/
public static ProjectileBuilder builder(World world) {
return new ProjectileBuilder(world);
}
/**
* The time, in ticks, before the projectile is fired.
*/
private final int delay;
/**
* The ending {@link Position} of the Projectile
*/
private final Position destination;
/**
* The ending height of the projectile.
*/
private final int endHeight;
/**
* The id of the graphic played as the Projectile proceeds.
*/
private final int graphic;
/**
* The time, in ticks, that the Projectile is present.
*/
private final int lifetime;
/**
* The pitch of the Projectile as it climbs.
*/
private final int pitch;
/**
* The offset of the projectile from the origin in world units.
*/
private final int offset;
/**
* The starting height of the projectile, relative to the height of the tile the Projectile starts on.
*/
private final int startHeight;
/**
* The index of the target Mob (which must be 0 if the Projectile does not target a Mob).
*/
private final int target;
/**
* Creates the Projectile.
*
* @param delay The time, in ticks, before the projectile is fired. Must not be negative.
* @param destination The ending {@link Position} of the Projectile. Must not be {@code null}.
* @param endHeight The ending height of the projectile. Must be [0, 255].
* @param graphic The id of the graphic played as the Projectile proceeds. Must not be negative.
* @param lifetime The time, in ticks, that the Projectile is present. Must be positive.
* @param pitch The pitch of the Projectile as it climbs.
* @param source The starting {@link Position} of the Projectile. Must not be {@code null}.
* @param startHeight The starting height of the projectile, relative to the height of the {@code source} tile.
* @param target The index of the target Mob (or 0 if the Projectile is not targeting a Mob).
*/
public Projectile(World world, Position source, int delay, Position destination, int endHeight, int graphic, int lifetime,
int pitch, int startHeight, int target, int offset) {
super(world, source);
this.delay = delay;
this.destination = destination;
this.endHeight = endHeight;
this.graphic = graphic;
this.lifetime = lifetime;
this.pitch = pitch;
this.startHeight = startHeight;
this.target = target;
this.offset = offset;
}
/**
* Gets the delay before this Projectile is fired, in ticks.
*
* @return The delay.
*/
public int getDelay() {
return delay;
}
/**
* Gets the destination {@link Position} of this Projectile.
*
* @return The destination {@link Position}. Will never be {@code null}.
*/
public Position getDestination() {
return destination;
}
/**
* Gets the ending height of this Projectile.
*
* @return The ending height.
*/
public int getEndHeight() {
return endHeight;
}
/**
* Gets the id of the graphic played by this Projectile.
*
* @return The graphic id.
*/
public int getGraphic() {
return graphic;
}
/**
* Gets the time, in ticks, that this Projectile lasts in the game world.
*
* @return The lifetime of this Projectile.
*/
public int getLifetime() {
return lifetime;
}
/**
* Gets the offset of this Projectile.
*
* @return The offset of this Projectile.
*/
public int getOffset() {
return offset;
}
/**
* Gets the pitch of this Projectile.
*
* @return The pitch.
*/
public int getPitch() {
return pitch;
}
/**
* Gets the starting height of this Projectile, relative to the origin tile.
*
* @return The starting height.
*/
public int getStartHeight() {
return startHeight;
}
/**
* Gets the index of the {@link Mob} this Projectile is targeting, or 0 if this Projectile is not targeting a
* {@link Mob}.
*
* @return The index.
*/
public int getTarget() {
return target;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Projectile) {
Projectile other = (Projectile) obj;
return position.equals(other.position)
&& destination.equals(other.destination)
&& delay == other.delay
&& lifetime == other.lifetime
&& target == other.target
&& startHeight == other.startHeight
&& endHeight == other.endHeight
&& pitch == other.pitch
&& offset == other.offset
&& graphic == other.graphic;
}
return false;
}
@Override
public EntityType getEntityType() {
return EntityType.PROJECTILE;
}
@Override
public int hashCode() {
return Objects.hashCode(position, destination, delay, lifetime, target, startHeight,
endHeight, pitch, offset, graphic);
}
@Override
public ProjectileUpdateOperation toUpdateOperation(Region region, EntityUpdateType type) {
Preconditions.checkArgument(type == EntityUpdateType.ADD, "Projectiles cannot be removed from the client");
return new ProjectileUpdateOperation(region, type, this);
}
}
@@ -29,6 +29,7 @@ import org.apollo.game.message.impl.RemoveObjectMessage;
import org.apollo.game.message.impl.RemoveTileItemMessage;
import org.apollo.game.message.impl.SendFriendMessage;
import org.apollo.game.message.impl.SendObjectMessage;
import org.apollo.game.message.impl.SendProjectileMessage;
import org.apollo.game.message.impl.SendPublicTileItemMessage;
import org.apollo.game.message.impl.SendTileItemMessage;
import org.apollo.game.message.impl.ServerChatMessage;
@@ -207,6 +208,7 @@ public final class Release377 extends Release {
register(RemoveTileItemMessage.class, new RemoveTileItemMessageEncoder());
register(SendObjectMessage.class, new SendObjectMessageEncoder());
register(RemoveObjectMessage.class, new RemoveObjectMessageEncoder());
register(SendProjectileMessage.class, new SendProjectileMessageEncoder());
register(GroupedRegionUpdateMessage.class, new GroupedRegionUpdateMessageEncoder(this));
register(ClearRegionMessage.class, new ClearRegionMessageEncoder());
@@ -0,0 +1,37 @@
package org.apollo.game.release.r377;
import org.apollo.game.message.impl.SendProjectileMessage;
import org.apollo.game.model.Position;
import org.apollo.game.model.entity.Projectile;
import org.apollo.net.codec.game.DataType;
import org.apollo.net.codec.game.GamePacket;
import org.apollo.net.codec.game.GamePacketBuilder;
import org.apollo.net.release.MessageEncoder;
/**
* A {@link MessageEncoder} for the {@link SendProjectileMessage}.
*/
public final class SendProjectileMessageEncoder extends MessageEncoder<SendProjectileMessage> {
@Override
public GamePacket encode(SendProjectileMessage message) {
Projectile projectile = message.getProjectile();
Position source = projectile.getPosition();
Position destination = projectile.getDestination();
GamePacketBuilder builder = new GamePacketBuilder(181);
builder.put(DataType.BYTE, message.getPositionOffset());
builder.put(DataType.BYTE, destination.getX() - source.getX());
builder.put(DataType.BYTE, destination.getY() - source.getY());
builder.put(DataType.SHORT, projectile.getTarget());
builder.put(DataType.SHORT, projectile.getGraphic());
builder.put(DataType.BYTE, projectile.getStartHeight());
builder.put(DataType.BYTE, projectile.getEndHeight());
builder.put(DataType.SHORT, projectile.getDelay());
builder.put(DataType.SHORT, projectile.getLifetime());
builder.put(DataType.BYTE, projectile.getPitch());
builder.put(DataType.BYTE, projectile.getOffset());
return builder.toGamePacket();
}
}