mirror of
https://github.com/array-in-a-matrix/brainwine.git
synced 2025-04-02 11:11:58 -04:00
NPC & damage system phase 1
This commit is contained in:
parent
5ce226813f
commit
a1aff443b3
45 changed files with 3106 additions and 94 deletions
|
@ -7,11 +7,13 @@ import org.apache.logging.log4j.LogManager;
|
|||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import brainwine.gameserver.command.CommandManager;
|
||||
import brainwine.gameserver.entity.EntityRegistry;
|
||||
import brainwine.gameserver.entity.player.PlayerManager;
|
||||
import brainwine.gameserver.loot.LootManager;
|
||||
import brainwine.gameserver.prefab.PrefabManager;
|
||||
import brainwine.gameserver.server.NetworkRegistry;
|
||||
import brainwine.gameserver.server.Server;
|
||||
import brainwine.gameserver.zone.EntityManager;
|
||||
import brainwine.gameserver.zone.ZoneManager;
|
||||
import brainwine.gameserver.zone.gen.ZoneGenerator;
|
||||
|
||||
|
@ -38,6 +40,8 @@ public class GameServer {
|
|||
logger.info("Starting GameServer ...");
|
||||
CommandManager.init();
|
||||
GameConfiguration.init();
|
||||
EntityRegistry.init();
|
||||
EntityManager.loadEntitySpawns();
|
||||
lootManager = new LootManager();
|
||||
prefabManager = new PrefabManager();
|
||||
ZoneGenerator.init();
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
package brainwine.gameserver.behavior;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
|
||||
|
||||
import brainwine.gameserver.behavior.composed.CrawlerBehavior;
|
||||
import brainwine.gameserver.behavior.composed.FlyerBehavior;
|
||||
import brainwine.gameserver.behavior.composed.WalkerBehavior;
|
||||
import brainwine.gameserver.behavior.parts.ClimbBehavior;
|
||||
import brainwine.gameserver.behavior.parts.FallBehavior;
|
||||
import brainwine.gameserver.behavior.parts.FlyBehavior;
|
||||
import brainwine.gameserver.behavior.parts.FlyTowardBehavior;
|
||||
import brainwine.gameserver.behavior.parts.FollowBehavior;
|
||||
import brainwine.gameserver.behavior.parts.IdleBehavior;
|
||||
import brainwine.gameserver.behavior.parts.RandomlyTargetBehavior;
|
||||
import brainwine.gameserver.behavior.parts.ShielderBehavior;
|
||||
import brainwine.gameserver.behavior.parts.SpawnAttackBehavior;
|
||||
import brainwine.gameserver.behavior.parts.TurnBehavior;
|
||||
import brainwine.gameserver.behavior.parts.WalkBehavior;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
|
||||
/**
|
||||
* Heavily based on Deepworld's original "rubyhave" (ha ha very punny) behavior system.
|
||||
*
|
||||
* https://github.com/bytebin/deepworld-gameserver/tree/master/vendor/rubyhave
|
||||
* https://github.com/bytebin/deepworld-gameserver/tree/master/models/npcs/behavior
|
||||
*/
|
||||
@JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type")
|
||||
@JsonSubTypes({
|
||||
// Composed
|
||||
@Type(name = "walker", value = WalkerBehavior.class),
|
||||
@Type(name = "crawler", value = CrawlerBehavior.class),
|
||||
@Type(name = "flyer", value = FlyerBehavior.class),
|
||||
// Parts
|
||||
@Type(name = "idle", value = IdleBehavior.class),
|
||||
@Type(name = "walk", value = WalkBehavior.class),
|
||||
@Type(name = "fall", value = FallBehavior.class),
|
||||
@Type(name = "turn", value = TurnBehavior.class),
|
||||
@Type(name = "follow", value = FollowBehavior.class),
|
||||
@Type(name = "climb", value = ClimbBehavior.class),
|
||||
@Type(name = "fly", value = FlyBehavior.class),
|
||||
@Type(name = "fly_toward", value = FlyTowardBehavior.class),
|
||||
@Type(name = "shielder", value = ShielderBehavior.class),
|
||||
@Type(name = "spawn_attack", value = SpawnAttackBehavior.class),
|
||||
@Type(name = "randomly_target", value = RandomlyTargetBehavior.class)
|
||||
})
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public abstract class Behavior {
|
||||
|
||||
protected final Npc entity;
|
||||
|
||||
public Behavior(Npc entity) {
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
public abstract boolean behave();
|
||||
|
||||
public boolean canBehave() {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package brainwine.gameserver.behavior;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import com.fasterxml.jackson.databind.InjectableValues;
|
||||
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
import brainwine.shared.JsonHelper;
|
||||
|
||||
public abstract class CompositeBehavior extends Behavior {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
protected final List<Behavior> children = new ArrayList<>();
|
||||
|
||||
public CompositeBehavior(Npc entity, Map<String, Object> config) {
|
||||
super(entity);
|
||||
addChildren(config);
|
||||
}
|
||||
|
||||
public CompositeBehavior(Npc entity) {
|
||||
this(entity, Collections.emptyMap());
|
||||
}
|
||||
|
||||
protected void addChildren(Map<String, Object> config) {
|
||||
// Override
|
||||
}
|
||||
|
||||
public void addChild(Class<? extends Behavior> type, Map<String, Object> config) {
|
||||
try {
|
||||
addChild(JsonHelper.readValue(config, type, new InjectableValues.Std().addValue(Npc.class, entity)));
|
||||
} catch(IOException e) {
|
||||
logger.error("Could not add child behavior of type {}.", type.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void addChild(Behavior child) {
|
||||
if(children.contains(child)) {
|
||||
logger.warn("Duplicate child instance {} for behavior {}", child, this);
|
||||
return;
|
||||
}
|
||||
|
||||
children.add(child);
|
||||
}
|
||||
|
||||
public void removeChild(Behavior child) {
|
||||
children.remove(child);
|
||||
}
|
||||
|
||||
public Collection<Behavior> getChildren() {
|
||||
return Collections.unmodifiableCollection(children);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package brainwine.gameserver.behavior;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
|
||||
public class SelectorBehavior extends CompositeBehavior {
|
||||
|
||||
public SelectorBehavior(Npc entity, Map<String, Object> config) {
|
||||
super(entity, config);
|
||||
}
|
||||
|
||||
public SelectorBehavior(Npc entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean behave() {
|
||||
for(Behavior child : children) {
|
||||
if(child.canBehave() && child.behave()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package brainwine.gameserver.behavior;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import com.fasterxml.jackson.databind.InjectableValues;
|
||||
import com.fasterxml.jackson.databind.exc.InvalidTypeIdException;
|
||||
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
import brainwine.gameserver.util.MapHelper;
|
||||
import brainwine.shared.JsonHelper;
|
||||
|
||||
public class SequenceBehavior extends CompositeBehavior {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
private static final List<String> loggedInvalidTypes = new ArrayList<>();
|
||||
|
||||
public SequenceBehavior(Npc entity, Map<String, Object> config) {
|
||||
super(entity, config);
|
||||
}
|
||||
|
||||
public SequenceBehavior(Npc entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
public static SequenceBehavior createBehaviorTree(Npc npc, List<Map<String, Object>> behavior) {
|
||||
SequenceBehavior root = new SequenceBehavior(npc);
|
||||
|
||||
for(Map<String, Object> config : behavior) {
|
||||
try {
|
||||
root.addChild(JsonHelper.readValue(config, Behavior.class, new InjectableValues.Std().addValue(Npc.class, npc)));
|
||||
} catch(InvalidTypeIdException e) {
|
||||
String type = e.getTypeId();
|
||||
|
||||
// TODO get rid of this once we add the remaining behaviors
|
||||
if(!loggedInvalidTypes.contains(type)) {
|
||||
logger.warn("No implementation exists for behavior type '{}'", type);
|
||||
loggedInvalidTypes.add(type);
|
||||
}
|
||||
} catch(IOException e) {
|
||||
logger.error("Could not add behavior type '{}' to behavior tree for entity with type '{}'",
|
||||
MapHelper.getString(config, "type", "unknown"), npc.getType(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean behave() {
|
||||
for(Behavior child : children) {
|
||||
if(!child.canBehave() || !child.behave()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package brainwine.gameserver.behavior.composed;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
import brainwine.gameserver.behavior.SelectorBehavior;
|
||||
import brainwine.gameserver.behavior.parts.ClimbBehavior;
|
||||
import brainwine.gameserver.behavior.parts.FallBehavior;
|
||||
import brainwine.gameserver.behavior.parts.IdleBehavior;
|
||||
import brainwine.gameserver.behavior.parts.TurnBehavior;
|
||||
import brainwine.gameserver.behavior.parts.WalkBehavior;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
import brainwine.gameserver.util.MapHelper;
|
||||
|
||||
public class CrawlerBehavior extends SelectorBehavior {
|
||||
|
||||
@JsonCreator
|
||||
private CrawlerBehavior(@JacksonInject Npc entity,
|
||||
Map<String, Object> config) {
|
||||
super(entity, config);
|
||||
}
|
||||
|
||||
public CrawlerBehavior(Npc entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChildren(Map<String, Object> config) {
|
||||
if(config.containsKey("idle")) {
|
||||
addChild(IdleBehavior.class, MapHelper.getMap(config, "idle"));
|
||||
}
|
||||
|
||||
addChild(new WalkBehavior(entity));
|
||||
addChild(new ClimbBehavior(entity));
|
||||
addChild(new TurnBehavior(entity));
|
||||
addChild(new ClimbBehavior(entity));
|
||||
addChild(new FallBehavior(entity));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package brainwine.gameserver.behavior.composed;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
import brainwine.gameserver.behavior.SelectorBehavior;
|
||||
import brainwine.gameserver.behavior.parts.FlyBehavior;
|
||||
import brainwine.gameserver.behavior.parts.FlyTowardBehavior;
|
||||
import brainwine.gameserver.behavior.parts.IdleBehavior;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
import brainwine.gameserver.util.MapHelper;
|
||||
|
||||
public class FlyerBehavior extends SelectorBehavior {
|
||||
|
||||
@JsonCreator
|
||||
private FlyerBehavior(@JacksonInject Npc entity,
|
||||
Map<String, Object> config) {
|
||||
super(entity, config);
|
||||
}
|
||||
|
||||
public FlyerBehavior(Npc entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChildren(Map<String, Object> config) {
|
||||
if(config.containsKey("idle")) {
|
||||
addChild(IdleBehavior.class, MapHelper.getMap(config, "idle"));
|
||||
}
|
||||
|
||||
addChild(FlyTowardBehavior.class, config);
|
||||
addChild(FlyBehavior.class, config);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package brainwine.gameserver.behavior.composed;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
import brainwine.gameserver.behavior.SelectorBehavior;
|
||||
import brainwine.gameserver.behavior.parts.FallBehavior;
|
||||
import brainwine.gameserver.behavior.parts.IdleBehavior;
|
||||
import brainwine.gameserver.behavior.parts.TurnBehavior;
|
||||
import brainwine.gameserver.behavior.parts.WalkBehavior;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
import brainwine.gameserver.util.MapHelper;
|
||||
|
||||
public class WalkerBehavior extends SelectorBehavior {
|
||||
|
||||
@JsonCreator
|
||||
private WalkerBehavior(@JacksonInject Npc entity,
|
||||
Map<String, Object> config) {
|
||||
super(entity, config);
|
||||
}
|
||||
|
||||
public WalkerBehavior(Npc entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addChildren(Map<String, Object> config) {
|
||||
if(config.containsKey("idle")) {
|
||||
addChild(IdleBehavior.class, MapHelper.getMap(config, "idle"));
|
||||
}
|
||||
|
||||
addChild(new WalkBehavior(entity));
|
||||
addChild(new FallBehavior(entity));
|
||||
addChild(new TurnBehavior(entity));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package brainwine.gameserver.behavior.parts;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
import brainwine.gameserver.behavior.Behavior;
|
||||
import brainwine.gameserver.entity.FacingDirection;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
|
||||
public class ClimbBehavior extends Behavior {
|
||||
|
||||
protected int lastClimbSide = 1;
|
||||
|
||||
@JsonCreator
|
||||
public ClimbBehavior(@JacksonInject Npc entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean behave() {
|
||||
if(climb(lastClimbSide)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return climb(lastClimbSide * -1);
|
||||
}
|
||||
|
||||
protected boolean climb(int side) {
|
||||
FacingDirection direction = entity.getDirection();
|
||||
int y = side * direction.getId() * -1;
|
||||
|
||||
if((entity.isBlocked(side, 0) || entity.isBlocked(side, y)) && !entity.isBlocked(0, y)) {
|
||||
lastClimbSide = side;
|
||||
entity.move(0, y, entity.getBaseSpeed() * 0.75F, side == -1 ? "climb-left" : "climb-right");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package brainwine.gameserver.behavior.parts;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
import brainwine.gameserver.behavior.Behavior;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
|
||||
public class FallBehavior extends Behavior {
|
||||
|
||||
@JsonCreator
|
||||
public FallBehavior(@JacksonInject Npc entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean behave() {
|
||||
entity.move(0, 1, entity.getSpeed() + 0.5F, "fall");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canBehave() {
|
||||
return !entity.isOnGround();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package brainwine.gameserver.behavior.parts;
|
||||
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
import brainwine.gameserver.behavior.Behavior;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
import brainwine.gameserver.util.Vector2i;
|
||||
|
||||
public class FlyBehavior extends Behavior {
|
||||
|
||||
protected boolean blockable = true;
|
||||
protected String animation = "fly";
|
||||
protected Vector2i targetPoint;
|
||||
|
||||
@JsonCreator
|
||||
public FlyBehavior(@JacksonInject Npc entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean behave() {
|
||||
if((targetPoint = getTargetPoint()) != null) {
|
||||
Vector2i next = entity.getZone().raynext((int)entity.getX(), (int)entity.getY(), targetPoint.getX(), targetPoint.getY());
|
||||
|
||||
if(next != null) {
|
||||
int moveX = (int)(next.getX() - entity.getX());
|
||||
int moveY = (int)(next.getY() - entity.getY());
|
||||
|
||||
if(!blockable || !entity.isBlocked(moveX, moveY)) {
|
||||
entity.move(moveX, moveY, entity.getBaseSpeed() * getSpeedMultiplier(), animation);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
targetPoint = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected float getSpeedMultiplier() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
protected Vector2i getTargetPoint() {
|
||||
return targetPoint == null ? getRandomPoint(10, 30) : targetPoint;
|
||||
}
|
||||
|
||||
protected Vector2i getRandomPoint(int minDistance, int maxDistance) {
|
||||
int distance = ThreadLocalRandom.current().nextInt(minDistance, maxDistance);
|
||||
double theta = Math.random() * 2 * Math.PI;
|
||||
int x = (int)(entity.getX() + distance * Math.cos(theta));
|
||||
int y = (int)(entity.getY() + distance * Math.sin(theta));
|
||||
return new Vector2i(x, y);
|
||||
}
|
||||
|
||||
public void setBlockable(boolean blockable) {
|
||||
this.blockable = blockable;
|
||||
}
|
||||
|
||||
public void setAnimation(String animation) {
|
||||
this.animation = animation;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package brainwine.gameserver.behavior.parts;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
import brainwine.gameserver.entity.Entity;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
import brainwine.gameserver.util.Vector2i;
|
||||
|
||||
public class FlyTowardBehavior extends FlyBehavior {
|
||||
|
||||
protected long lastNearbyAt;
|
||||
|
||||
@JsonCreator
|
||||
public FlyTowardBehavior(@JacksonInject Npc entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canBehave() {
|
||||
return entity.hasTarget() && !shouldBackOff();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float getSpeedMultiplier() {
|
||||
return 1.25F;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Vector2i getTargetPoint() {
|
||||
return new Vector2i((int)entity.getTarget().getX(), (int)entity.getTarget().getY());
|
||||
}
|
||||
|
||||
protected boolean shouldBackOff() {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if(now < lastNearbyAt + 2000) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Entity target = entity.getTarget();
|
||||
|
||||
if(target != null && entity.inRange(target, 2)) {
|
||||
lastNearbyAt = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package brainwine.gameserver.behavior.parts;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
import brainwine.gameserver.behavior.Behavior;
|
||||
import brainwine.gameserver.entity.FacingDirection;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
|
||||
public class FollowBehavior extends Behavior {
|
||||
|
||||
@JsonCreator
|
||||
public FollowBehavior(@JacksonInject Npc entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean behave() {
|
||||
if(entity.hasTarget()) {
|
||||
entity.setDirection(entity.getTarget().getX() - entity.getX() > 0 ? FacingDirection.EAST : FacingDirection.WEST);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package brainwine.gameserver.behavior.parts;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonSetter;
|
||||
|
||||
import brainwine.gameserver.behavior.Behavior;
|
||||
import brainwine.gameserver.entity.FacingDirection;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
|
||||
public class IdleBehavior extends Behavior {
|
||||
|
||||
protected int delay = 60;
|
||||
protected int duration = 60;
|
||||
protected double random = 0.5;
|
||||
protected String[] animations = new String[] {"idle"};
|
||||
protected long until;
|
||||
protected boolean idle;
|
||||
protected String currentAnimation;
|
||||
protected long animationSetUntil = System.currentTimeMillis();
|
||||
|
||||
@JsonCreator
|
||||
public IdleBehavior(@JacksonInject Npc entity) {
|
||||
super(entity);
|
||||
until = getNextUntil();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean behave() {
|
||||
Random random = ThreadLocalRandom.current();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if(entity.isOnGround()) {
|
||||
if(now > until) {
|
||||
idle = !idle;
|
||||
until = getNextUntil();
|
||||
}
|
||||
} else {
|
||||
idle = false;
|
||||
}
|
||||
|
||||
if(idle) {
|
||||
if(now > animationSetUntil) {
|
||||
int length = animations.length;
|
||||
currentAnimation = length == 0 ? "idle" : animations[random.nextInt(length)];
|
||||
animationSetUntil = now + (random.nextInt(2) + 1) * 1000;
|
||||
}
|
||||
|
||||
entity.move(0, 0, currentAnimation);
|
||||
|
||||
if(Math.random() < 0.01) {
|
||||
entity.setDirection(entity.getDirection() == FacingDirection.WEST ? FacingDirection.EAST : FacingDirection.WEST);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected long getNextUntil() {
|
||||
int currentDuration = (idle ? duration : delay) * 1000;
|
||||
return (long)(System.currentTimeMillis() + currentDuration + currentDuration * (Math.random() * random));
|
||||
}
|
||||
|
||||
public void setDelay(int delay) {
|
||||
this.delay = delay;
|
||||
}
|
||||
|
||||
public void setDuration(int duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public void setRandom(double random) {
|
||||
this.random = random;
|
||||
}
|
||||
|
||||
@JsonSetter("animation")
|
||||
public void setAnimations(String... animations) {
|
||||
this.animations = animations;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package brainwine.gameserver.behavior.parts;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
import brainwine.gameserver.behavior.Behavior;
|
||||
import brainwine.gameserver.entity.Entity;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
|
||||
public class RandomlyTargetBehavior extends Behavior {
|
||||
|
||||
protected int range = 20;
|
||||
protected boolean friendlyFire;
|
||||
protected boolean blockable = true;
|
||||
protected String animation;
|
||||
protected long targetLockedAt;
|
||||
|
||||
@JsonCreator
|
||||
public RandomlyTargetBehavior(@JacksonInject Npc entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean behave() {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if(now > targetLockedAt + 5000) {
|
||||
entity.setTarget(null);
|
||||
}
|
||||
|
||||
if(!entity.hasTarget()) {
|
||||
Entity target = entity.getZone().getRandomPlayerInRange(entity.getX(), entity.getY(), range);
|
||||
|
||||
if(target != null && !target.isDead() && entity.canSee(target)) {
|
||||
entity.setTarget(target);
|
||||
targetLockedAt = now;
|
||||
}
|
||||
}
|
||||
|
||||
if(animation != null && entity.hasTarget()) {
|
||||
entity.setAnimation(animation);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setRange(int range) {
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
public void setFriendlyFire(boolean friendlyFire) {
|
||||
this.friendlyFire = friendlyFire;
|
||||
}
|
||||
|
||||
public void setBlockable(boolean blockable) {
|
||||
this.blockable = blockable;
|
||||
}
|
||||
|
||||
public void setAnimation(String animation) {
|
||||
this.animation = animation;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package brainwine.gameserver.behavior.parts;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
import brainwine.gameserver.behavior.Behavior;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
import brainwine.gameserver.item.DamageType;
|
||||
import brainwine.gameserver.item.Item;
|
||||
import brainwine.gameserver.util.Pair;
|
||||
|
||||
public class ShielderBehavior extends Behavior {
|
||||
|
||||
private final Set<DamageType> defenses = new HashSet<>();
|
||||
private int duration = 5;
|
||||
private int recharge = 5;
|
||||
private long shieldStart;
|
||||
private long lastAttackedAt;
|
||||
private DamageType currentShield;
|
||||
|
||||
@JsonCreator
|
||||
public ShielderBehavior(@JacksonInject Npc entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean behave() {
|
||||
long now = System.currentTimeMillis();
|
||||
Collection<Pair<Item, Long>> recentAttacks = entity.getRecentAttacks();
|
||||
|
||||
if(!recentAttacks.isEmpty()) {
|
||||
lastAttackedAt = now;
|
||||
DamageType type = recentAttacks.stream().findFirst().get().getFirst().getDamageType();
|
||||
if(currentShield == null && now >= shieldStart + (recharge * 1000)) {
|
||||
if(defenses.contains(type)) {
|
||||
setShield(type);
|
||||
shieldStart = now;
|
||||
}
|
||||
} else if(currentShield != null) {
|
||||
if(now >= shieldStart + (duration * 1000)) {
|
||||
setShield(null);
|
||||
shieldStart = now;
|
||||
} else if(currentShield != type && defenses.contains(type)) {
|
||||
setShield(type);
|
||||
}
|
||||
}
|
||||
} else if(now >= lastAttackedAt + 2000) {
|
||||
setShield(null);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void setShield(DamageType type) {
|
||||
entity.setProperty("s", type);
|
||||
entity.setDefense(currentShield, 0);
|
||||
currentShield = type;
|
||||
|
||||
if(type != null) {
|
||||
entity.setDefense(type, 1 - entity.getBaseDefense(type));
|
||||
}
|
||||
}
|
||||
|
||||
public void setDefenses(String... defenses) {
|
||||
this.defenses.clear();
|
||||
|
||||
for(String defense : defenses) {
|
||||
switch(defense) {
|
||||
case "all":
|
||||
this.defenses.addAll(Arrays.asList(DamageType.values()));
|
||||
break;
|
||||
case "elemental":
|
||||
DamageType[] elementalDamageTypes = DamageType.getElementalDamageTypes();
|
||||
this.defenses.add(elementalDamageTypes[ThreadLocalRandom.current().nextInt(elementalDamageTypes.length)]);
|
||||
break;
|
||||
default:
|
||||
DamageType type = DamageType.fromName(defense);
|
||||
|
||||
if(type != DamageType.NONE) {
|
||||
this.defenses.add(type);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setDuration(int duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
public void setRecharge(int recharge) {
|
||||
this.recharge = recharge;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package brainwine.gameserver.behavior.parts;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonSetter;
|
||||
|
||||
import brainwine.gameserver.behavior.Behavior;
|
||||
import brainwine.gameserver.entity.EntityConfig;
|
||||
import brainwine.gameserver.entity.EntityStatus;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
import brainwine.gameserver.server.messages.EntityStatusMessage;
|
||||
|
||||
public class SpawnAttackBehavior extends Behavior {
|
||||
|
||||
protected EntityConfig bulletConfig;
|
||||
protected float speed = 8;
|
||||
protected float frequency = 1;
|
||||
protected float range = 15;
|
||||
protected Object burst;
|
||||
protected long lastAttackAt;;
|
||||
|
||||
@JsonCreator
|
||||
public SpawnAttackBehavior(@JacksonInject Npc entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean behave() {
|
||||
// TODO much more to be done here
|
||||
lastAttackAt = System.currentTimeMillis();
|
||||
|
||||
if(bulletConfig != null) {
|
||||
Npc bullet = new Npc(entity.getZone(), bulletConfig);
|
||||
bullet.setProperty("<", entity.getId());
|
||||
bullet.setProperty(">", entity.getTarget().getId());
|
||||
bullet.setProperty("*", true);
|
||||
bullet.setProperty("s", speed);
|
||||
|
||||
if(burst != null) {
|
||||
bullet.setProperty("#", burst);
|
||||
}
|
||||
|
||||
entity.getZone().sendMessage(new EntityStatusMessage(bullet, EntityStatus.ENTERING));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canBehave() {
|
||||
return System.currentTimeMillis() - lastAttackAt >= 1.0F / frequency * 1000
|
||||
&& entity.hasTarget() && entity.inRange(entity.getTarget(), 15);
|
||||
}
|
||||
|
||||
@JsonSetter("entity")
|
||||
public void setBulletConfig(EntityConfig bulletConfig) {
|
||||
this.bulletConfig = bulletConfig;
|
||||
}
|
||||
|
||||
public void setSpeed(float speed) {
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
public void setFrequency(float frequency) {
|
||||
this.frequency = frequency;
|
||||
}
|
||||
|
||||
public void setRange(float range) {
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
public void setBurst(Object burst) {
|
||||
this.burst = burst;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package brainwine.gameserver.behavior.parts;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
import brainwine.gameserver.behavior.Behavior;
|
||||
import brainwine.gameserver.entity.FacingDirection;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
|
||||
public class TurnBehavior extends Behavior {
|
||||
|
||||
@JsonCreator
|
||||
public TurnBehavior(@JacksonInject Npc entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean behave() {
|
||||
entity.setDirection(entity.getDirection() == FacingDirection.WEST ? FacingDirection.EAST : FacingDirection.WEST);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canBehave() {
|
||||
return entity.isBlocked(entity.getDirection().getId(), 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package brainwine.gameserver.behavior.parts;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
import brainwine.gameserver.behavior.Behavior;
|
||||
import brainwine.gameserver.entity.FacingDirection;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
|
||||
public class WalkBehavior extends Behavior {
|
||||
|
||||
@JsonCreator
|
||||
public WalkBehavior(@JacksonInject Npc entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean behave() {
|
||||
entity.move(entity.getDirection().getId(), 0, "walk");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canBehave() {
|
||||
FacingDirection direction = entity.getDirection();
|
||||
return entity.isOnGround(direction.getId()) && !entity.isBlocked(direction.getId(), 0);
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ import org.apache.logging.log4j.Logger;
|
|||
|
||||
import brainwine.gameserver.command.commands.AdminCommand;
|
||||
import brainwine.gameserver.command.commands.BroadcastCommand;
|
||||
import brainwine.gameserver.command.commands.EntityCommand;
|
||||
import brainwine.gameserver.command.commands.ExportCommand;
|
||||
import brainwine.gameserver.command.commands.GenerateZoneCommand;
|
||||
import brainwine.gameserver.command.commands.GiveCommand;
|
||||
|
@ -69,6 +70,7 @@ public class CommandManager {
|
|||
registerCommand(new ImportCommand());
|
||||
registerCommand(new PositionCommand());
|
||||
registerCommand(new RickrollCommand());
|
||||
registerCommand(new EntityCommand());
|
||||
}
|
||||
|
||||
public static void executeCommand(CommandExecutor executor, String commandLine) {
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
package brainwine.gameserver.command.commands;
|
||||
|
||||
import static brainwine.gameserver.entity.player.NotificationType.ALERT;
|
||||
|
||||
import brainwine.gameserver.command.Command;
|
||||
import brainwine.gameserver.command.CommandExecutor;
|
||||
import brainwine.gameserver.entity.EntityConfig;
|
||||
import brainwine.gameserver.entity.EntityRegistry;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
import brainwine.gameserver.entity.player.NotificationType;
|
||||
import brainwine.gameserver.entity.player.Player;
|
||||
import brainwine.gameserver.zone.Zone;
|
||||
|
||||
public class EntityCommand extends Command {
|
||||
|
||||
@Override
|
||||
public void execute(CommandExecutor executor, String[] args) {
|
||||
if(args.length == 0) {
|
||||
executor.notify(String.format("Usage: %s", getUsage(executor)), ALERT);
|
||||
return;
|
||||
}
|
||||
|
||||
Player player = (Player)executor;
|
||||
String name = args[0];
|
||||
EntityConfig config = EntityRegistry.getEntityConfig(name);
|
||||
|
||||
if(config == null) {
|
||||
executor.notify(String.format("Entity with name '%s' does not exist.", name), NotificationType.ALERT);
|
||||
return;
|
||||
}
|
||||
|
||||
Zone zone = player.getZone();
|
||||
zone.spawnEntity(new Npc(zone, config), (int)player.getX(), (int)player.getY(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "entity";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Spawns an entity at your current location.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage(CommandExecutor executor) {
|
||||
return "/entity <name>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canExecute(CommandExecutor executor) {
|
||||
return executor.isAdmin() && executor instanceof Player;
|
||||
}
|
||||
}
|
|
@ -1,45 +1,100 @@
|
|||
package brainwine.gameserver.entity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import brainwine.gameserver.entity.player.Player;
|
||||
import brainwine.gameserver.server.messages.EntityStatusMessage;
|
||||
import brainwine.gameserver.util.MathUtils;
|
||||
import brainwine.gameserver.zone.Zone;
|
||||
|
||||
public abstract class Entity {
|
||||
|
||||
public static final float POSITION_MODIFIER = 100F;
|
||||
public static final int VELOCITY_MODIFIER = (int)POSITION_MODIFIER;
|
||||
private static int discriminator;
|
||||
protected final List<Player> trackers = new ArrayList<>();
|
||||
protected int type;
|
||||
protected String name;
|
||||
protected float health;
|
||||
protected final int id;
|
||||
protected int id;
|
||||
protected Zone zone;
|
||||
protected float x;
|
||||
protected float y;
|
||||
protected int velocityX;
|
||||
protected int velocityY;
|
||||
protected float velocityX;
|
||||
protected float velocityY;
|
||||
protected int targetX;
|
||||
protected int targetY;
|
||||
protected FacingDirection direction = FacingDirection.WEST;
|
||||
protected int animation;
|
||||
|
||||
public Entity(Zone zone) {
|
||||
this.id = ++discriminator;
|
||||
this.zone = zone;
|
||||
health = 10; // TODO
|
||||
}
|
||||
|
||||
public abstract EntityType getType();
|
||||
public void tick(float deltaTime) {
|
||||
// Override
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
public void die(Player killer) {
|
||||
// Override
|
||||
}
|
||||
|
||||
public void damage(float amount) {
|
||||
damage(amount, null);
|
||||
}
|
||||
|
||||
public void damage(float amount, Player attacker) {
|
||||
setHealth(health - amount);
|
||||
|
||||
if(health <= 0) {
|
||||
die(attacker);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canSee(Entity other) {
|
||||
return canSee((int)other.getX(), (int)other.getY());
|
||||
}
|
||||
|
||||
public boolean canSee(int x, int y) {
|
||||
return zone.isPointVisibleFrom((int)this.x, (int)this.y, x, y) ||
|
||||
(y > 0 && zone.isPointVisibleFrom((int)this.x, (int)this.y, x, y - 1));
|
||||
}
|
||||
|
||||
public boolean inRange(Entity other, float range) {
|
||||
return inRange(other.getX(), other.getY(), range);
|
||||
}
|
||||
|
||||
public boolean inRange(float x, float y, float range) {
|
||||
return MathUtils.inRange(this.x, this.y, x, y, range);
|
||||
}
|
||||
|
||||
public void addTracker(Player tracker) {
|
||||
trackers.add(tracker);
|
||||
}
|
||||
|
||||
public void removeTracker(Player tracker) {
|
||||
trackers.remove(tracker);
|
||||
}
|
||||
|
||||
public List<Player> getTrackers() {
|
||||
return trackers;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
@ -53,7 +108,7 @@ public abstract class Entity {
|
|||
}
|
||||
|
||||
public void setHealth(float health) {
|
||||
this.health = health;
|
||||
this.health = health < 0 ? 0 : health;
|
||||
}
|
||||
|
||||
public float getHealth() {
|
||||
|
@ -73,16 +128,16 @@ public abstract class Entity {
|
|||
return y;
|
||||
}
|
||||
|
||||
public void setVelocity(int velocityX, int velocityY) {
|
||||
public void setVelocity(float velocityX, float velocityY) {
|
||||
this.velocityX = velocityX;
|
||||
this.velocityY = velocityY;
|
||||
}
|
||||
|
||||
public int getVelocityX() {
|
||||
public float getVelocityX() {
|
||||
return velocityX;
|
||||
}
|
||||
|
||||
public int getVelocityY() {
|
||||
public float getVelocityY() {
|
||||
return velocityY;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
package brainwine.gameserver.entity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import brainwine.gameserver.item.DamageType;
|
||||
import brainwine.gameserver.item.Item;
|
||||
import brainwine.gameserver.util.Vector2i;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class EntityConfig {
|
||||
|
||||
@JsonProperty("code")
|
||||
private int type;
|
||||
|
||||
@JacksonInject("name")
|
||||
private String name;
|
||||
|
||||
@JsonProperty("health")
|
||||
private float maxHealth = 5;
|
||||
|
||||
@JsonProperty("speed")
|
||||
private float baseSpeed = 3;
|
||||
|
||||
@JsonProperty("size")
|
||||
private Vector2i size = new Vector2i(1, 1);
|
||||
|
||||
@JsonProperty("loot")
|
||||
private List<EntityLoot> loot = new ArrayList<>();
|
||||
|
||||
@JsonProperty("loot_by_weapon")
|
||||
private Map<Item, List<EntityLoot>> lootByWeapon = new HashMap<>();
|
||||
|
||||
@JsonProperty("defense")
|
||||
private Map<DamageType, Float> resistances = new HashMap<>();
|
||||
|
||||
@JsonProperty("weakness")
|
||||
private Map<DamageType, Float> weaknesses = new HashMap<>();
|
||||
|
||||
@JsonProperty("components")
|
||||
private Map<String, String[]> components = Collections.emptyMap();
|
||||
|
||||
@JsonProperty("set_attachments")
|
||||
private Map<String, String> attachments = Collections.emptyMap();
|
||||
|
||||
@JsonProperty("behavior")
|
||||
private List<Map<String, Object>> behavior = Collections.emptyList();
|
||||
|
||||
@JsonProperty("animations")
|
||||
private List<Map<String, Object>> animations = Collections.emptyList();
|
||||
|
||||
@JsonProperty("slots")
|
||||
private List<String> slots = Collections.emptyList();
|
||||
|
||||
@JsonProperty("attachments")
|
||||
private List<String> possibleAttachments = Collections.emptyList();
|
||||
|
||||
@JsonCreator
|
||||
private EntityConfig() {}
|
||||
|
||||
@JsonCreator
|
||||
public static EntityConfig fromName(String name) {
|
||||
return EntityRegistry.getEntityConfig(name);
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public float getMaxHealth() {
|
||||
return maxHealth;
|
||||
}
|
||||
|
||||
public float getBaseSpeed() {
|
||||
return baseSpeed;
|
||||
}
|
||||
|
||||
public Vector2i getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public List<EntityLoot> getLoot() {
|
||||
return loot;
|
||||
}
|
||||
|
||||
public Map<Item, List<EntityLoot>> getLootByWeapon() {
|
||||
return lootByWeapon;
|
||||
}
|
||||
|
||||
public Map<DamageType, Float> getResistances() {
|
||||
return resistances;
|
||||
}
|
||||
|
||||
public Map<DamageType, Float> getWeaknesses() {
|
||||
return weaknesses;
|
||||
}
|
||||
|
||||
public Map<String, String[]> getComponents() {
|
||||
return components;
|
||||
}
|
||||
|
||||
public Map<String, String> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getBehavior() {
|
||||
return behavior;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getAnimations() {
|
||||
return animations;
|
||||
}
|
||||
|
||||
public List<String> getSlots() {
|
||||
return slots;
|
||||
}
|
||||
|
||||
public List<String> getPossibleAttachments() {
|
||||
return possibleAttachments;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package brainwine.gameserver.entity;
|
||||
|
||||
import java.beans.ConstructorProperties;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
|
||||
import brainwine.gameserver.item.Item;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class EntityLoot {
|
||||
|
||||
private final Item item;
|
||||
private final int quantity;
|
||||
private final int frequency;
|
||||
|
||||
@ConstructorProperties({"item", "quantity", "frequency"})
|
||||
public EntityLoot(Item item, int quantity, int frequency) {
|
||||
this.item = item;
|
||||
this.quantity = quantity < 1 ? 1 : quantity;
|
||||
this.frequency = frequency < 1 ? 1 : frequency;
|
||||
}
|
||||
|
||||
public Item getItem() {
|
||||
return item;
|
||||
}
|
||||
|
||||
public int getQuantity() {
|
||||
return quantity;
|
||||
}
|
||||
|
||||
public int getFrequency() {
|
||||
return frequency;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package brainwine.gameserver.entity;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import com.fasterxml.jackson.databind.InjectableValues;
|
||||
|
||||
import brainwine.gameserver.GameConfiguration;
|
||||
import brainwine.gameserver.util.MapHelper;
|
||||
import brainwine.shared.JsonHelper;
|
||||
|
||||
public class EntityRegistry {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
private static final Map<String, EntityConfig> entities = new HashMap<>();
|
||||
private static boolean initalized;
|
||||
|
||||
public static void init() {
|
||||
if(initalized) {
|
||||
logger.warn("Already initialized!");
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Map<String, Object>> entityConfigs = MapHelper.getMap(GameConfiguration.getBaseConfig(), "entities");
|
||||
|
||||
if(entityConfigs == null) {
|
||||
logger.warn("No entity configurations exist!");
|
||||
return;
|
||||
}
|
||||
|
||||
for(Entry<String, Map<String, Object>> entry : entityConfigs.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
Map<String, Object> config = entry.getValue();
|
||||
|
||||
if(!config.containsKey("code")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
registerEntityConfig(name, JsonHelper.readValue(entry.getValue(), EntityConfig.class,
|
||||
new InjectableValues.Std().addValue("name", name)));
|
||||
} catch(Exception e) {
|
||||
logger.error("Could not deserialize entity config for entity '{}'", name, e);
|
||||
}
|
||||
}
|
||||
|
||||
int entityCount = entities.size();
|
||||
logger.info("Successfully loaded {} entit{}", entityCount, entityCount == 1 ? "y" : "ies");
|
||||
initalized = true;
|
||||
}
|
||||
|
||||
public static void registerEntityConfig(String name, EntityConfig config) {
|
||||
if(entities.containsKey(name)) {
|
||||
logger.warn("Attempted to register entity with name '{}' twice", name);
|
||||
return;
|
||||
}
|
||||
|
||||
entities.put(name, config);
|
||||
}
|
||||
|
||||
public static EntityConfig getEntityConfig(String name) {
|
||||
return entities.get(name);
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package brainwine.gameserver.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
public enum EntityType {
|
||||
|
||||
PLAYER(0),
|
||||
GHOST(1),
|
||||
TERRAPUS_JUVENLIE(3),
|
||||
TERRAPUS_ADULT(4);
|
||||
|
||||
private final int id;
|
||||
|
||||
private EntityType(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,360 @@
|
|||
package brainwine.gameserver.entity.npc;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import brainwine.gameserver.behavior.SequenceBehavior;
|
||||
import brainwine.gameserver.entity.Entity;
|
||||
import brainwine.gameserver.entity.EntityConfig;
|
||||
import brainwine.gameserver.entity.EntityLoot;
|
||||
import brainwine.gameserver.entity.EntityRegistry;
|
||||
import brainwine.gameserver.entity.FacingDirection;
|
||||
import brainwine.gameserver.entity.player.Player;
|
||||
import brainwine.gameserver.item.DamageType;
|
||||
import brainwine.gameserver.item.Item;
|
||||
import brainwine.gameserver.server.messages.EntityChangeMessage;
|
||||
import brainwine.gameserver.util.MapHelper;
|
||||
import brainwine.gameserver.util.Pair;
|
||||
import brainwine.gameserver.util.Vector2i;
|
||||
import brainwine.gameserver.util.WeightedMap;
|
||||
import brainwine.gameserver.zone.MetaBlock;
|
||||
import brainwine.gameserver.zone.Zone;
|
||||
|
||||
public class Npc extends Entity {
|
||||
|
||||
public static final int ATTACK_RETENTION_TIME = 333;
|
||||
private final Map<String, Object> properties = new HashMap<>();
|
||||
private final Map<DamageType, Float> baseDefenses = new HashMap<>();
|
||||
private final Map<DamageType, Float> activeDefenses = new HashMap<>();
|
||||
private final Map<Player, Pair<Item, Long>> recentAttacks = new HashMap<>();
|
||||
private final WeightedMap<EntityLoot> loot = new WeightedMap<>();
|
||||
private final Map<Item, WeightedMap<EntityLoot>> lootByWeapon = new HashMap<>();
|
||||
private final List<String> animations;
|
||||
private final SequenceBehavior behaviorTree;
|
||||
private final Vector2i size;
|
||||
private final String typeName;
|
||||
private final float baseSpeed;
|
||||
private float speed;
|
||||
private int moveX;
|
||||
private int moveY;
|
||||
private int guardBlock = -1;
|
||||
private Entity target;
|
||||
private long lastBehavedAt = System.currentTimeMillis();
|
||||
private long lastTrackedAt = System.currentTimeMillis();
|
||||
|
||||
public Npc(Zone zone, EntityConfig config) {
|
||||
super(zone);
|
||||
|
||||
// Add components & merge relevant configurations if applicable
|
||||
List<Map<String, Object>> behavior = new ArrayList<>(config.getBehavior());
|
||||
Map<String, String> attachments = new HashMap<>(config.getAttachments());
|
||||
Map<String, String[]> components = config.getComponents();
|
||||
|
||||
// TODO to what extent should we merge the configurations with components?
|
||||
// Merging everything is kind of a pain and seems to be overdoing it anyway...
|
||||
|
||||
if(!components.isEmpty()) {
|
||||
List<Integer> selectedComponents = new ArrayList<>();
|
||||
|
||||
for(Entry<String, String[]> entry : components.entrySet()) {
|
||||
String[] pool = entry.getValue();
|
||||
EntityConfig componentConfig = pool.length > 0 ?
|
||||
EntityRegistry.getEntityConfig(pool[ThreadLocalRandom.current().nextInt(pool.length)]) : null;
|
||||
|
||||
if(componentConfig != null) {
|
||||
behavior.addAll(componentConfig.getBehavior());
|
||||
attachments.putAll(componentConfig.getAttachments());
|
||||
selectedComponents.add(componentConfig.getType());
|
||||
}
|
||||
}
|
||||
|
||||
properties.put("C", selectedComponents);
|
||||
}
|
||||
|
||||
// Set attachments if applicable
|
||||
if(!attachments.isEmpty()) {
|
||||
Map<Integer, Object> slots = new HashMap<>();
|
||||
|
||||
for(Entry<String, String> entry : attachments.entrySet()) {
|
||||
String slot = entry.getKey();
|
||||
String attachment = entry.getValue();
|
||||
|
||||
if(attachment != null) {
|
||||
slots.put(config.getSlots().indexOf(slot), config.getPossibleAttachments().indexOf(attachment));
|
||||
}
|
||||
}
|
||||
|
||||
properties.put("sl", slots);
|
||||
}
|
||||
|
||||
type = config.getType();
|
||||
typeName = config.getName();
|
||||
health = config.getMaxHealth();
|
||||
baseSpeed = config.getBaseSpeed();
|
||||
speed = baseSpeed;
|
||||
size = config.getSize();
|
||||
animations = config.getAnimations().stream().map(map -> MapHelper.getString(map, "name")).collect(Collectors.toList());
|
||||
behaviorTree = SequenceBehavior.createBehaviorTree(this, behavior);
|
||||
baseDefenses.putAll(config.getResistances());
|
||||
|
||||
config.getWeaknesses().forEach((type, multiplier) -> {
|
||||
baseDefenses.put(type, baseDefenses.getOrDefault(type, 0F) - multiplier);
|
||||
});
|
||||
|
||||
config.getLoot().forEach(loot -> this.loot.addEntry(loot, loot.getFrequency()));
|
||||
config.getLootByWeapon().forEach((weapon, loot) -> {
|
||||
WeightedMap<EntityLoot> lootMap = new WeightedMap<>();
|
||||
loot.forEach(entry -> lootMap.addEntry(entry, entry.getFrequency()));
|
||||
lootByWeapon.put(weapon, lootMap);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(float deltaTime) {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// Clear expired recent attacks
|
||||
recentAttacks.values().removeIf(attack -> now >= attack.getLast() + ATTACK_RETENTION_TIME);
|
||||
|
||||
// Tick behavior when it is ready
|
||||
if(now >= lastBehavedAt + (int)(1000 / speed)) {
|
||||
lastBehavedAt = now;
|
||||
behaviorTree.behave();
|
||||
|
||||
if(moveX != 0 || moveY != 0) {
|
||||
setPosition(x + moveX, y + moveY);
|
||||
setVelocity(moveX * speed, moveY * speed);
|
||||
moveX = 0;
|
||||
moveY = 0;
|
||||
}
|
||||
|
||||
setPosition(Math.round(x), Math.round(y));
|
||||
}
|
||||
|
||||
if(!trackers.isEmpty()) {
|
||||
lastTrackedAt = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void die(Player killer) {
|
||||
// Grant loot to killer
|
||||
if(killer != null) {
|
||||
EntityLoot loot = getRandomLoot(killer);
|
||||
|
||||
if(loot != null) {
|
||||
Item item = loot.getItem();
|
||||
|
||||
if(!item.isAir()) {
|
||||
killer.getInventory().addItem(item, loot.getQuantity());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove itself from the guard block metadata if it was guarding one
|
||||
if(isGuard()) {
|
||||
MetaBlock metaBlock = zone.getMetaBlock(guardBlock);
|
||||
|
||||
if(metaBlock != null) {
|
||||
MapHelper.getList(metaBlock.getMetadata(), "!", Collections.emptyList()).remove(typeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getStatusConfig() {
|
||||
Map<String, Object> config = super.getStatusConfig();
|
||||
config.putAll(properties);
|
||||
|
||||
if(isDead()) {
|
||||
config.put("!", "v");
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public void move(int x, int y) {
|
||||
move(x, y, baseSpeed);
|
||||
}
|
||||
|
||||
public void move(int x, int y, float speed) {
|
||||
move(x, y, speed, null);
|
||||
}
|
||||
|
||||
public void move(int x, int y, String animation) {
|
||||
move(x, y, baseSpeed, animation);
|
||||
}
|
||||
|
||||
public void move(int x, int y, float speed, String animation) {
|
||||
this.speed = speed;
|
||||
direction = x > 0 ? FacingDirection.EAST : x < 0 ? FacingDirection.WEST : direction;
|
||||
moveX = x;
|
||||
moveY = y;
|
||||
|
||||
if(animation != null) {
|
||||
setAnimation(animation);
|
||||
}
|
||||
}
|
||||
|
||||
public void attack(Player attacker, Item weapon) {
|
||||
Pair<Item, Long> recentAttack = recentAttacks.get(attacker);
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// Reject the attack if the player already attacked this entity recently
|
||||
if(recentAttack != null && now < recentAttack.getLast() + ATTACK_RETENTION_TIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
damage(calculateDamage(weapon.getDamage() / 4, weapon.getDamageType()), attacker); // TODO change weapon damage in config
|
||||
recentAttacks.put(attacker, new Pair<>(weapon, now));
|
||||
}
|
||||
|
||||
public float calculateDamage(float baseDamage, DamageType type) {
|
||||
return baseDamage * (1 - getDefense(type));
|
||||
}
|
||||
|
||||
@JsonIgnore // TODO Silly Jackson is drunk and errors trying to find a key deserializer for recentAttacks
|
||||
public Collection<Pair<Item, Long>> getRecentAttacks() {
|
||||
return Collections.unmodifiableCollection(recentAttacks.values());
|
||||
}
|
||||
|
||||
public void setDefense(DamageType type, float amount) {
|
||||
if(amount == 0) {
|
||||
activeDefenses.remove(type);
|
||||
} else {
|
||||
activeDefenses.put(type, amount);
|
||||
}
|
||||
}
|
||||
|
||||
public float getDefense(DamageType type) {
|
||||
return getDefense(type, true);
|
||||
}
|
||||
|
||||
public float getDefense(DamageType type, boolean includeBaseDefense) {
|
||||
return (includeBaseDefense ? getBaseDefense(type) : 0) + activeDefenses.getOrDefault(type, 0F);
|
||||
}
|
||||
|
||||
public boolean isClearable() {
|
||||
return !isGuard();
|
||||
}
|
||||
|
||||
public void setProperty(String key, Object value) {
|
||||
if(value == null) {
|
||||
properties.remove(key);
|
||||
} else {
|
||||
properties.put(key, value);
|
||||
}
|
||||
|
||||
for(Player tracker : trackers) {
|
||||
tracker.sendMessage(new EntityChangeMessage(id, MapHelper.map(key, value)));
|
||||
}
|
||||
}
|
||||
|
||||
public EntityLoot getRandomLoot(Player awardee) {
|
||||
Item weapon = awardee.getHeldItem();
|
||||
|
||||
if(lootByWeapon.containsKey(weapon)) {
|
||||
return lootByWeapon.get(weapon).next();
|
||||
} else {
|
||||
return loot.next();
|
||||
}
|
||||
}
|
||||
|
||||
public float getBaseDefense(DamageType type) {
|
||||
return baseDefenses.getOrDefault(type, 0F);
|
||||
}
|
||||
|
||||
public void setGuardBlock(int guardBlock) {
|
||||
this.guardBlock = guardBlock;
|
||||
}
|
||||
|
||||
public boolean isGuard() {
|
||||
return guardBlock >= 0;
|
||||
}
|
||||
|
||||
public int getGuardBlock() {
|
||||
return guardBlock;
|
||||
}
|
||||
|
||||
public void setTarget(Entity target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
public boolean hasTarget() {
|
||||
return target != null;
|
||||
}
|
||||
|
||||
public Entity getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
public void setSpeed(float speed) {
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
public float getSpeed() {
|
||||
return speed;
|
||||
}
|
||||
|
||||
public float getBaseSpeed() {
|
||||
return baseSpeed;
|
||||
}
|
||||
|
||||
public int getMoveX() {
|
||||
return moveX;
|
||||
}
|
||||
|
||||
public int getMoveY() {
|
||||
return moveY;
|
||||
}
|
||||
|
||||
public long getLastTrackedAt() {
|
||||
return lastTrackedAt;
|
||||
}
|
||||
|
||||
public boolean isBlocked(int oX, int oY) {
|
||||
int x = (int)this.x;
|
||||
int y = (int)this.y;
|
||||
int tX = x + oX;
|
||||
int tY = y + oY;
|
||||
boolean blocked = zone.isBlockSolid(tX, tY) || (oX != 0 && zone.isBlockSolid(tX, y)) || (oY != 0 && zone.isBlockSolid(x, tY));
|
||||
|
||||
if(size.getX() > 1) {
|
||||
int additionalWidth = size.getX() - 1;
|
||||
blocked = blocked || zone.isBlockSolid(tX + additionalWidth, tY)
|
||||
|| (oX != 0 && zone.isBlockSolid(tX + additionalWidth, y))
|
||||
|| (oY != 0 && zone.isBlockSolid(x + additionalWidth, tY));
|
||||
}
|
||||
|
||||
if(size.getY() > 1) {
|
||||
int additionalHeight = size.getY() - 1;
|
||||
blocked = blocked || zone.isBlockSolid(tX, tY - additionalHeight)
|
||||
|| (oX != 0 && zone.isBlockSolid(tX, y - additionalHeight))
|
||||
|| (oY != 0 && zone.isBlockSolid(x, tY - additionalHeight));
|
||||
}
|
||||
|
||||
return blocked;
|
||||
}
|
||||
|
||||
public boolean isOnGround() {
|
||||
return isOnGround(0);
|
||||
}
|
||||
|
||||
public boolean isOnGround(int diagonal) {
|
||||
return isBlocked(0, 1) || (diagonal != 0 && isBlocked(diagonal, 1));
|
||||
}
|
||||
|
||||
public void setAnimation(String name) {
|
||||
int index = animations.indexOf(name);
|
||||
setAnimation(index == -1 ? 0 : index);
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import java.util.Map;
|
|||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JacksonInject;
|
||||
import com.fasterxml.jackson.annotation.JsonIncludeProperties;
|
||||
|
@ -27,8 +28,8 @@ import brainwine.gameserver.dialog.DialogSection;
|
|||
import brainwine.gameserver.dialog.DialogType;
|
||||
import brainwine.gameserver.entity.Entity;
|
||||
import brainwine.gameserver.entity.EntityStatus;
|
||||
import brainwine.gameserver.entity.EntityType;
|
||||
import brainwine.gameserver.entity.FacingDirection;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
import brainwine.gameserver.item.Item;
|
||||
import brainwine.gameserver.item.ItemRegistry;
|
||||
import brainwine.gameserver.item.ItemUseType;
|
||||
|
@ -71,6 +72,8 @@ public class Player extends Entity implements CommandExecutor {
|
|||
public static final int MAX_SPEED_Y = 25;
|
||||
public static final int HEARTBEAT_TIMEOUT = 30000;
|
||||
public static final int MAX_AUTH_TOKENS = 3;
|
||||
public static final int TRACKED_ENTITY_UPDATE_INTERVAL = 100;
|
||||
public static final float ENTITY_VISIBILITY_RANGE = 40;
|
||||
private static int dialogDiscriminator;
|
||||
|
||||
@JacksonInject("documentId")
|
||||
|
@ -109,6 +112,7 @@ public class Player extends Entity implements CommandExecutor {
|
|||
private final Map<Skill, Integer> skills = new HashMap<>();
|
||||
private final Set<Integer> activeChunks = new HashSet<>();
|
||||
private final Map<Integer, Consumer<Object[]>> dialogs = new HashMap<>();
|
||||
private final List<Entity> trackedEntities = new ArrayList<>();
|
||||
private String clientVersion;
|
||||
private Placement lastPlacement;
|
||||
private Item heldItem = Item.AIR;
|
||||
|
@ -116,6 +120,7 @@ public class Player extends Entity implements CommandExecutor {
|
|||
private int teleportX;
|
||||
private int teleportY;
|
||||
private long lastHeartbeat;
|
||||
private long lastTrackedEntityUpdate;
|
||||
private Connection connection;
|
||||
|
||||
@ConstructorProperties({"documentId", "name", "current_zone"})
|
||||
|
@ -143,19 +148,29 @@ public class Player extends Entity implements CommandExecutor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public EntityType getType() {
|
||||
return EntityType.PLAYER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
public void tick(float deltaTime) {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if(lastHeartbeat != 0) {
|
||||
if(System.currentTimeMillis() - lastHeartbeat >= HEARTBEAT_TIMEOUT) {
|
||||
kick("Connection timed out.");
|
||||
}
|
||||
}
|
||||
|
||||
if(now - lastTrackedEntityUpdate >= TRACKED_ENTITY_UPDATE_INTERVAL) {
|
||||
updateTrackedEntities();
|
||||
|
||||
for(Entity entity : trackedEntities) {
|
||||
sendMessage(new EntityPositionMessage(entity));
|
||||
}
|
||||
|
||||
lastTrackedEntityUpdate = now;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void die(Player killer) {
|
||||
sendMessageToPeers(new EntityStatusMessage(this, EntityStatus.DEAD)); // TODO killer id
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -258,11 +273,17 @@ public class Player extends Entity implements CommandExecutor {
|
|||
clientVersion = null;
|
||||
|
||||
if(zone != null) {
|
||||
zone.removePlayer(this);
|
||||
zone.removeEntity(this);
|
||||
}
|
||||
|
||||
dialogs.clear();
|
||||
activeChunks.clear();
|
||||
|
||||
for(Entity entity : trackedEntities) {
|
||||
entity.removeTracker(this);
|
||||
}
|
||||
|
||||
trackedEntities.clear();
|
||||
GameServer.getInstance().getPlayerManager().onPlayerDisconnect(this);
|
||||
connection.setPlayer(null);
|
||||
connection = null;
|
||||
|
@ -310,7 +331,7 @@ public class Player extends Entity implements CommandExecutor {
|
|||
}
|
||||
|
||||
public void changeZone(Zone zone) {
|
||||
this.zone.removePlayer(this);
|
||||
this.zone.removeEntity(this);
|
||||
this.zone = zone;
|
||||
sendMessage(new EventMessage("playerWillChangeZone", null));
|
||||
kick("Teleporting...", true);
|
||||
|
@ -386,6 +407,10 @@ public class Player extends Entity implements CommandExecutor {
|
|||
}
|
||||
|
||||
public void respawn() {
|
||||
if(isDead()) {
|
||||
health = 10; // TODO max health
|
||||
}
|
||||
|
||||
int x = spawnPoint.getX();
|
||||
int y = spawnPoint.getY();
|
||||
sendMessage(new PlayerPositionMessage(x, y));
|
||||
|
@ -699,6 +724,42 @@ public class Player extends Entity implements CommandExecutor {
|
|||
return activeChunks.size();
|
||||
}
|
||||
|
||||
private void updateTrackedEntities() {
|
||||
List<Entity> entitiesInRange = zone.getEntitiesInRange(x, y, ENTITY_VISIBILITY_RANGE);
|
||||
entitiesInRange.remove(this);
|
||||
List<Entity> enteredEntities = entitiesInRange.stream().filter(entity -> !trackedEntities.contains(entity))
|
||||
.collect(Collectors.toList());
|
||||
List<Entity> departedEntities = trackedEntities.stream().filter(entity -> !entitiesInRange.contains(entity))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for(Entity entity : enteredEntities) {
|
||||
if(entity instanceof Npc) {
|
||||
sendMessage(new EntityStatusMessage(entity, EntityStatus.ENTERING));
|
||||
}
|
||||
|
||||
entity.addTracker(this);
|
||||
}
|
||||
|
||||
for(Entity entity : departedEntities) {
|
||||
if(entity instanceof Npc) {
|
||||
sendMessage(new EntityStatusMessage(entity, EntityStatus.EXITING));
|
||||
}
|
||||
|
||||
entity.removeTracker(this);
|
||||
}
|
||||
|
||||
trackedEntities.clear();
|
||||
trackedEntities.addAll(entitiesInRange);
|
||||
}
|
||||
|
||||
public boolean isTrackingEntity(Entity entity) {
|
||||
return trackedEntities.contains(entity);
|
||||
}
|
||||
|
||||
public List<Entity> getTrackedEntities() {
|
||||
return trackedEntities;
|
||||
}
|
||||
|
||||
public void setConnection(Connection connection) {
|
||||
if(isOnline()) {
|
||||
kick("You logged in from another location.");
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
package brainwine.gameserver.item;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
public enum DamageType {
|
||||
|
||||
ACID,
|
||||
BLUDGEONING,
|
||||
COLD,
|
||||
CRUSHING,
|
||||
ENERGY,
|
||||
FIRE,
|
||||
PIERCING,
|
||||
SLASHING,
|
||||
|
||||
@JsonEnumDefaultValue
|
||||
NONE;
|
||||
|
||||
public static DamageType[] getPhysicalDamageTypes() {
|
||||
return new DamageType[] { BLUDGEONING, CRUSHING, PIERCING, SLASHING };
|
||||
}
|
||||
|
||||
public static DamageType[] getElementalDamageTypes() {
|
||||
return new DamageType[] { ACID, COLD, ENERGY, FIRE };
|
||||
}
|
||||
|
||||
public static DamageType fromName(String id) {
|
||||
for(DamageType value : values()) {
|
||||
if(value.toString().equalsIgnoreCase(id)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return NONE;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getId() {
|
||||
return toString().toLowerCase();
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
|
||||
import brainwine.gameserver.dialog.DialogType;
|
||||
import brainwine.gameserver.util.Pair;
|
||||
import brainwine.gameserver.util.Vector2i;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
|
@ -66,6 +67,9 @@ public class Item {
|
|||
@JsonProperty("field")
|
||||
private int field;
|
||||
|
||||
@JsonProperty("guard")
|
||||
private int guardLevel;
|
||||
|
||||
@JsonProperty("diggable")
|
||||
private boolean diggable;
|
||||
|
||||
|
@ -84,6 +88,12 @@ public class Item {
|
|||
@JsonProperty("invulnerable")
|
||||
private boolean invulnerable;
|
||||
|
||||
@JsonProperty("solid")
|
||||
private boolean solid;
|
||||
|
||||
@JsonProperty("door")
|
||||
private boolean door;
|
||||
|
||||
@JsonProperty("inventory")
|
||||
private LazyItemGetter inventoryItem;
|
||||
|
||||
|
@ -96,6 +106,9 @@ public class Item {
|
|||
@JsonProperty("loot")
|
||||
private String[] lootCategories = {};
|
||||
|
||||
@JsonProperty("damage")
|
||||
private Pair<DamageType, Float> damageInfo;
|
||||
|
||||
@JsonProperty("ingredients")
|
||||
private List<CraftingIngredient> ingredients = new ArrayList<>();
|
||||
|
||||
|
@ -206,6 +219,10 @@ public class Item {
|
|||
return field;
|
||||
}
|
||||
|
||||
public int getGuardLevel() {
|
||||
return guardLevel;
|
||||
}
|
||||
|
||||
public boolean isDiggable() {
|
||||
return diggable;
|
||||
}
|
||||
|
@ -230,6 +247,18 @@ public class Item {
|
|||
return invulnerable || !isPlacable();
|
||||
}
|
||||
|
||||
public boolean isDoor() {
|
||||
return door;
|
||||
}
|
||||
|
||||
public boolean isSolid() {
|
||||
return solid;
|
||||
}
|
||||
|
||||
public boolean isWeapon() {
|
||||
return damageInfo != null;
|
||||
}
|
||||
|
||||
public Item getInventoryItem() {
|
||||
return inventoryItem == null ? this : inventoryItem.get();
|
||||
}
|
||||
|
@ -246,6 +275,14 @@ public class Item {
|
|||
return craftingQuantity;
|
||||
}
|
||||
|
||||
public DamageType getDamageType() {
|
||||
return isWeapon() ? damageInfo.getFirst() : DamageType.NONE;
|
||||
}
|
||||
|
||||
public float getDamage() {
|
||||
return isWeapon() ? damageInfo.getLast() : 0;
|
||||
}
|
||||
|
||||
public boolean isCraftable() {
|
||||
return !ingredients.isEmpty();
|
||||
}
|
||||
|
|
|
@ -22,12 +22,12 @@ public class EntityPositionMessage extends Message {
|
|||
this(entity.getId(), entity.getX(), entity.getY(), entity.getVelocityX(), entity.getVelocityY(), entity.getDirection(), entity.getTargetX(), entity.getTargetY(), entity.getAnimation());
|
||||
}
|
||||
|
||||
public EntityPositionMessage(int id, float x, float y, int velocityX, int velocityY, FacingDirection direction, int targetX, int targetY, int animation) {
|
||||
public EntityPositionMessage(int id, float x, float y, float velocityX, float velocityY, FacingDirection direction, int targetX, int targetY, int animation) {
|
||||
this.id = id;
|
||||
this.x = (int)(x * Entity.POSITION_MODIFIER);
|
||||
this.y = (int)(y * Entity.POSITION_MODIFIER);
|
||||
this.velocityX = velocityX * Entity.VELOCITY_MODIFIER;
|
||||
this.velocityY = velocityY * Entity.VELOCITY_MODIFIER;
|
||||
this.velocityX = (int)(velocityX * Entity.VELOCITY_MODIFIER);
|
||||
this.velocityY = (int)(velocityY * Entity.VELOCITY_MODIFIER);
|
||||
this.direction = direction;
|
||||
this.targetX = targetX * Entity.VELOCITY_MODIFIER;
|
||||
this.targetY = targetY * Entity.VELOCITY_MODIFIER;
|
||||
|
|
|
@ -5,14 +5,13 @@ import java.util.Map;
|
|||
import brainwine.gameserver.annotations.MessageInfo;
|
||||
import brainwine.gameserver.entity.Entity;
|
||||
import brainwine.gameserver.entity.EntityStatus;
|
||||
import brainwine.gameserver.entity.EntityType;
|
||||
import brainwine.gameserver.server.Message;
|
||||
|
||||
@MessageInfo(id = 7, collection = true)
|
||||
public class EntityStatusMessage extends Message {
|
||||
|
||||
public int id;
|
||||
public EntityType type;
|
||||
public int type;
|
||||
public String name;
|
||||
public EntityStatus status;
|
||||
public Map<String, Object> details;
|
||||
|
@ -21,7 +20,7 @@ public class EntityStatusMessage extends Message {
|
|||
this(entity.getId(), entity.getType(), entity.getName(), status, entity.getStatusConfig());
|
||||
}
|
||||
|
||||
public EntityStatusMessage(int id, EntityType type, String name, EntityStatus status, Map<String, Object> details) {
|
||||
public EntityStatusMessage(int id, int type, String name, EntityStatus status, Map<String, Object> details) {
|
||||
this.id = id;
|
||||
this.type = type;
|
||||
this.name = name;
|
||||
|
|
|
@ -53,7 +53,7 @@ public class AuthenticateRequest extends Request {
|
|||
return;
|
||||
}
|
||||
|
||||
zone.addPlayer(player);
|
||||
zone.addEntity(player);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package brainwine.gameserver.server.requests;
|
||||
|
||||
import brainwine.gameserver.annotations.RequestInfo;
|
||||
import brainwine.gameserver.entity.Entity;
|
||||
import brainwine.gameserver.entity.EntityStatus;
|
||||
import brainwine.gameserver.entity.player.Player;
|
||||
import brainwine.gameserver.server.PlayerRequest;
|
||||
import brainwine.gameserver.server.messages.EntityStatusMessage;
|
||||
|
||||
@RequestInfo(id = 51)
|
||||
public class EntitiesRequest extends PlayerRequest {
|
||||
|
@ -10,6 +13,14 @@ public class EntitiesRequest extends PlayerRequest {
|
|||
public int[] entityIds;
|
||||
|
||||
public void process(Player player) {
|
||||
// TODO
|
||||
int count = Math.min(entityIds.length, 10);
|
||||
|
||||
for(int i = 0; i < count; i++) {
|
||||
Entity entity = player.getZone().getEntity(entityIds[i]);
|
||||
|
||||
if(entity != null && player.isTrackingEntity(entity)) {
|
||||
player.sendMessage(new EntityStatusMessage(entity, EntityStatus.ENTERING));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ public class HealthRequest extends PlayerRequest {
|
|||
return;
|
||||
}
|
||||
|
||||
// TODO
|
||||
player.setHealth(10);
|
||||
player.damage(player.getHealth() - health, null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package brainwine.gameserver.server.requests;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import brainwine.gameserver.annotations.OptionalField;
|
||||
import brainwine.gameserver.annotations.RequestInfo;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
import brainwine.gameserver.entity.player.Player;
|
||||
import brainwine.gameserver.item.Item;
|
||||
import brainwine.gameserver.server.PlayerRequest;
|
||||
|
@ -10,6 +13,7 @@ import brainwine.gameserver.server.messages.EntityItemUseMessage;
|
|||
/**
|
||||
* TODO This request may be sent *before* a {@link CraftRequest} is sent.
|
||||
* So basically, we can't really perform any "has item" checks...
|
||||
* ... Let's do it anyway lol!
|
||||
*/
|
||||
@RequestInfo(id = 10)
|
||||
public class InventoryUseRequest extends PlayerRequest {
|
||||
|
@ -23,10 +27,34 @@ public class InventoryUseRequest extends PlayerRequest {
|
|||
|
||||
@Override
|
||||
public void process(Player player) {
|
||||
if(!player.getInventory().hasItem(item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(type == 0) {
|
||||
if(status != 2) {
|
||||
player.setHeldItem(item);
|
||||
}
|
||||
|
||||
// Lovely type ambiguity. Always nice.
|
||||
if(details instanceof Collection) {
|
||||
Collection<?> entityIds = (Collection<?>)details;
|
||||
int maxEntityAttackCount = 1; // TODO agility skill
|
||||
|
||||
for(Object id : entityIds) {
|
||||
if(id instanceof Integer) {
|
||||
Npc npc = player.getZone().getNpc((int)id);
|
||||
|
||||
if(npc != null && player.canSee(npc)) {
|
||||
npc.attack(player, item);
|
||||
}
|
||||
}
|
||||
|
||||
if(--maxEntityAttackCount <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
player.sendMessageToPeers(new EntityItemUseMessage(player.getId(), type, item, status));
|
||||
|
|
|
@ -46,7 +46,7 @@ public class MoveRequest extends PlayerRequest {
|
|||
player.setVelocity(velocityX, velocityY);
|
||||
player.setDirection(direction);
|
||||
player.setTarget(targetX, targetY);
|
||||
player.setAnimation(animation);
|
||||
player.setAnimation(player.isDead() ? 55 : animation); // TODO why doesn't death animation sync normally?
|
||||
player.sendMessageToPeers(new EntityPositionMessage(player));
|
||||
zone.exploreArea((int)fX, (int)fY); // TODO xp reward
|
||||
}
|
||||
|
|
|
@ -28,6 +28,12 @@ public class MapHelper {
|
|||
return new HashMap<>();
|
||||
}
|
||||
|
||||
public static <K, V> Map<K, V> map(K key, V value) {
|
||||
Map<K, V> map = new HashMap<>();
|
||||
map.put(key, value);
|
||||
return map;
|
||||
}
|
||||
|
||||
public static void put(Map<?, ?> map, String path, Object value) {
|
||||
String[] segments = path.split("\\.");
|
||||
Map<Object, Object> current = (Map<Object, Object>)map;
|
||||
|
|
26
gameserver/src/main/java/brainwine/gameserver/util/Pair.java
Normal file
26
gameserver/src/main/java/brainwine/gameserver/util/Pair.java
Normal file
|
@ -0,0 +1,26 @@
|
|||
package brainwine.gameserver.util;
|
||||
|
||||
import java.beans.ConstructorProperties;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
|
||||
@JsonFormat(shape = JsonFormat.Shape.ARRAY)
|
||||
public class Pair<K, V> {
|
||||
|
||||
private final K first;
|
||||
private final V last;
|
||||
|
||||
@ConstructorProperties({"first", "last"})
|
||||
public Pair(K first, V last) {
|
||||
this.first = first;
|
||||
this.last = last;
|
||||
}
|
||||
|
||||
public K getFirst() {
|
||||
return first;
|
||||
}
|
||||
|
||||
public V getLast() {
|
||||
return last;
|
||||
}
|
||||
}
|
|
@ -11,8 +11,10 @@ import java.util.ArrayList;
|
|||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
@ -160,6 +162,33 @@ public class ChunkManager {
|
|||
return null;
|
||||
}
|
||||
|
||||
public List<Chunk> getVisibleChunks() {
|
||||
List<Chunk> visibleChunks = new ArrayList<>();
|
||||
Set<Integer> chunkIndices = new HashSet<>();
|
||||
int chunkWidth = zone.getChunkWidth();
|
||||
int chunkHeight = zone.getChunkHeight();
|
||||
|
||||
for(Player player : zone.getPlayers()) {
|
||||
int x = (int)player.getX();
|
||||
int y = (int)player.getY();
|
||||
|
||||
// TODO take screen size & perception skill into account
|
||||
for(int i = -40; i <= 40; i += chunkWidth) {
|
||||
for(int j = -20; j <= 20; j += chunkHeight) {
|
||||
chunkIndices.add(getChunkIndex(x + i, y + j));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(int chunkIndex : chunkIndices) {
|
||||
if(isChunkLoaded(chunkIndex)) {
|
||||
visibleChunks.add(chunks.get(chunkIndex));
|
||||
}
|
||||
}
|
||||
|
||||
return visibleChunks;
|
||||
}
|
||||
|
||||
public void putChunk(int index, Chunk chunk) {
|
||||
if(!chunks.containsKey(index) && isChunkIndexInBounds(index)) {
|
||||
chunk.setModified(true);
|
||||
|
|
|
@ -0,0 +1,287 @@
|
|||
package brainwine.gameserver.zone;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
|
||||
import brainwine.gameserver.entity.Entity;
|
||||
import brainwine.gameserver.entity.EntityConfig;
|
||||
import brainwine.gameserver.entity.EntityStatus;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
import brainwine.gameserver.entity.player.Player;
|
||||
import brainwine.gameserver.item.Item;
|
||||
import brainwine.gameserver.item.Layer;
|
||||
import brainwine.gameserver.item.ModType;
|
||||
import brainwine.gameserver.server.messages.EffectMessage;
|
||||
import brainwine.gameserver.server.messages.EntityPositionMessage;
|
||||
import brainwine.gameserver.server.messages.EntityStatusMessage;
|
||||
import brainwine.gameserver.util.ResourceUtils;
|
||||
import brainwine.gameserver.util.Vector2i;
|
||||
import brainwine.gameserver.util.WeightedMap;
|
||||
import brainwine.shared.JsonHelper;
|
||||
|
||||
public class EntityManager {
|
||||
|
||||
public static final long ENTITY_CLEAR_TIME = 10000;
|
||||
public static final long SPAWN_INTERVAL = 200;
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
private static final ThreadLocalRandom random = ThreadLocalRandom.current();
|
||||
private static final Map<Biome, List<EntitySpawn>> spawns = new HashMap<>();
|
||||
private final Map<Integer, Entity> entities = new HashMap<>();
|
||||
private final Map<Integer, Npc> npcs = new HashMap<>();
|
||||
private final Map<Integer, Player> players = new HashMap<>();
|
||||
private final Map<String, Player> playersByName = new HashMap<>();
|
||||
private final Zone zone;
|
||||
private int entityDiscriminator;
|
||||
private long lastSpawnAt = System.currentTimeMillis();
|
||||
|
||||
public EntityManager(Zone zone) {
|
||||
this.zone = zone;
|
||||
}
|
||||
|
||||
public static void loadEntitySpawns() {
|
||||
spawns.clear();
|
||||
logger.info("Loading entity spawns ...");
|
||||
File file = new File("spawning.json");
|
||||
ResourceUtils.copyDefaults("spawning.json");
|
||||
|
||||
if(file.isFile()) {
|
||||
try {
|
||||
Map<Biome, List<EntitySpawn>> loot = JsonHelper.readValue(file, new TypeReference<Map<Biome, List<EntitySpawn>>>(){});
|
||||
spawns.putAll(loot);
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to load entity spawns", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static EntityConfig getRandomEligibleEntity(Biome biome, String locale, double depth, Item baseItem) {
|
||||
WeightedMap<EntityConfig> eligibleEntities = new WeightedMap<>();
|
||||
|
||||
if(spawns.containsKey(biome)) {
|
||||
spawns.get(biome).stream().filter(spawn -> locale.equalsIgnoreCase(spawn.getLocale())
|
||||
&& depth >= spawn.getMinDepth() && depth <= spawn.getMaxDepth()
|
||||
&& ((baseItem.getId() != 5 && baseItem.getId() != 6) || spawn.getOrifice() == baseItem))
|
||||
.forEach(spawn -> eligibleEntities.addEntry(spawn.getEntity(), spawn.getFrequency()));
|
||||
}
|
||||
|
||||
return eligibleEntities.next();
|
||||
}
|
||||
|
||||
public void tick(float deltaTime) {
|
||||
clearEntities();
|
||||
|
||||
for(Entity entity : getEntities()) {
|
||||
entity.tick(deltaTime);
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if(now > lastSpawnAt + SPAWN_INTERVAL &&
|
||||
!players.isEmpty() && getTransientNpcCount() < Math.min(64, players.size() * 8)) {
|
||||
spawnRandomEntity();
|
||||
lastSpawnAt = now;
|
||||
}
|
||||
}
|
||||
|
||||
private void spawnRandomEntity() {
|
||||
boolean immediate = random.nextDouble() < 0.75;
|
||||
List<Chunk> visibleChunks = zone.getVisibleChunks();
|
||||
List<Chunk> chunks = immediate ? visibleChunks : zone.getLoadedChunks().stream()
|
||||
.filter(chunk -> !visibleChunks.contains(chunk)).collect(Collectors.toList());
|
||||
|
||||
if(!chunks.isEmpty()) {
|
||||
List<Vector2i> eligiblePositions = new ArrayList<>();
|
||||
Chunk chunk = chunks.get(random.nextInt(chunks.size()));
|
||||
|
||||
for(int x = chunk.getX(); x < chunk.getX() + chunk.getWidth(); x++) {
|
||||
for(int y = chunk.getY(); y < chunk.getY() + chunk.getHeight(); y++) {
|
||||
Block block = chunk.getBlock(x, y);
|
||||
int base = block.getBaseItem().getId();
|
||||
|
||||
if((immediate && base == 5 || base == 6) ||
|
||||
(!immediate && block.getBackItem().isAir() && block.getFrontItem().isAir())) {
|
||||
eligiblePositions.add(new Vector2i(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!eligiblePositions.isEmpty()) {
|
||||
Vector2i position = eligiblePositions.get(random.nextInt(eligiblePositions.size()));
|
||||
int x = position.getX();
|
||||
int y = position.getY();
|
||||
Block block = chunk.getBlock(x, y);
|
||||
String locale = block.getBaseItem().isAir() ? "sky" : "cave";
|
||||
EntityConfig entity = getRandomEligibleEntity(zone.getBiome(), locale, y / (double)zone.getHeight(), block.getBaseItem());
|
||||
|
||||
if(immediate) {
|
||||
if(tryBustOrifice(x, y, Layer.BACK) || tryBustOrifice(x, y, Layer.FRONT)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(entity != null) {
|
||||
spawnEntity(new Npc(zone, entity), x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean tryBustOrifice(int x, int y, Layer layer) {
|
||||
if(zone.isBlockProtected(x, y, null)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Block block = zone.getBlock(x, y);
|
||||
Item item = block.getItem(layer);
|
||||
int mod = block.getMod(layer);
|
||||
|
||||
if(!item.isAir()) {
|
||||
if(random.nextBoolean()) {
|
||||
item = item.getMod() == ModType.DECAY && mod < 5 ? item : Item.AIR;
|
||||
mod = item.isAir() ? 0 : Math.min(5, mod + random.nextInt(1, 3));
|
||||
zone.updateBlock(x, y, layer, item, mod);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void clearEntities() {
|
||||
List<Npc> clearableEntities = new ArrayList<>();
|
||||
|
||||
for(Npc npc : npcs.values()) {
|
||||
if(npc.isDead() || !zone.isChunkLoaded((int)npc.getX(), (int)npc.getY()) ||
|
||||
(!npc.isGuard() && System.currentTimeMillis() > npc.getLastTrackedAt() + ENTITY_CLEAR_TIME)) {
|
||||
clearableEntities.add(npc);
|
||||
}
|
||||
}
|
||||
|
||||
for(Npc npc : clearableEntities) {
|
||||
removeEntity(npc);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Entity> getEntitiesInRange(float x, float y, float range) {
|
||||
return getEntities().stream().filter(entity -> entity.inRange(x, y, range)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public Player getRandomPlayerInRange(float x, float y, float range) {
|
||||
List<Player> players = getPlayersInRange(x, y, range);
|
||||
return players.isEmpty() ? null : players.get(random.nextInt(players.size()));
|
||||
}
|
||||
|
||||
public List<Player> getPlayersInRange(float x, float y, float range) {
|
||||
return getPlayers().stream().filter(player -> player.inRange(x, y, range)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public void spawnEntity(Entity entity, int x, int y) {
|
||||
spawnEntity(entity, x, y, false);
|
||||
}
|
||||
|
||||
public void spawnEntity(Entity entity, int x, int y, boolean effect) {
|
||||
if(zone.isChunkLoaded(x, y)) {
|
||||
addEntity(entity);
|
||||
entity.setPosition(x, y);
|
||||
|
||||
if(effect) {
|
||||
zone.sendMessageToChunk(new EffectMessage(x + 0.5F, y + 0.5F, "bomb-teleport", 4), zone.getChunk(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addEntity(Entity entity) {
|
||||
if(entities.containsValue(entity)) {
|
||||
removeEntity(entity);
|
||||
}
|
||||
|
||||
int entityId = ++entityDiscriminator;
|
||||
entity.setZone(zone);
|
||||
entity.setId(entityId);
|
||||
|
||||
if(entity instanceof Player) {
|
||||
Player player = (Player)entity;
|
||||
player.onZoneChanged();
|
||||
players.put(entityId, player);
|
||||
playersByName.put(player.getName(), player);
|
||||
player.sendMessageToPeers(new EntityStatusMessage(player, EntityStatus.ENTERING));
|
||||
player.sendMessageToPeers(new EntityPositionMessage(player));
|
||||
} else if(entity instanceof Npc) {
|
||||
npcs.put(entityId, (Npc)entity);
|
||||
}
|
||||
|
||||
entities.put(entityId, entity);
|
||||
}
|
||||
|
||||
public void removeEntity(Entity entity) {
|
||||
int entityId = entity.getId();
|
||||
|
||||
if(entity instanceof Player) {
|
||||
players.remove(entityId);
|
||||
playersByName.remove(entity.getName());
|
||||
zone.sendMessage(new EntityStatusMessage(entity, EntityStatus.EXITING));
|
||||
} else {
|
||||
npcs.remove(entityId);
|
||||
}
|
||||
|
||||
entities.remove(entityId);
|
||||
}
|
||||
|
||||
public Entity getEntity(int entityId) {
|
||||
return entities.get(entityId);
|
||||
}
|
||||
|
||||
public int getEntityCount() {
|
||||
return entities.size();
|
||||
}
|
||||
|
||||
public Collection<Entity> getEntities() {
|
||||
return Collections.unmodifiableCollection(entities.values());
|
||||
}
|
||||
|
||||
public Npc getNpc(int entityId) {
|
||||
return npcs.get(entityId);
|
||||
}
|
||||
|
||||
public int getNpcCount() {
|
||||
return npcs.size();
|
||||
}
|
||||
|
||||
public int getTransientNpcCount() {
|
||||
return (int)(getNpcCount() - getNpcs().stream().filter(npc -> npc.isGuard()).count());
|
||||
}
|
||||
|
||||
public Collection<Npc> getNpcs() {
|
||||
return Collections.unmodifiableCollection(npcs.values());
|
||||
}
|
||||
|
||||
public Player getPlayer(int entityId) {
|
||||
return players.get(entityId);
|
||||
}
|
||||
|
||||
public Player getPlayer(String name) {
|
||||
return playersByName.get(name);
|
||||
}
|
||||
|
||||
public int getPlayerCount() {
|
||||
return players.size();
|
||||
}
|
||||
|
||||
public Collection<Player> getPlayers() {
|
||||
return Collections.unmodifiableCollection(players.values());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package brainwine.gameserver.zone;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import brainwine.gameserver.entity.EntityConfig;
|
||||
import brainwine.gameserver.item.Item;
|
||||
|
||||
// TODO groups
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class EntitySpawn {
|
||||
|
||||
@JsonProperty("entity")
|
||||
private EntityConfig entity;
|
||||
|
||||
@JsonProperty("locale")
|
||||
private String locale;
|
||||
|
||||
@JsonProperty("min_depth")
|
||||
private double minDepth;
|
||||
|
||||
@JsonProperty("max_depth")
|
||||
private double maxDepth = 1;
|
||||
|
||||
@JsonProperty("orifice")
|
||||
private Item orifice;
|
||||
|
||||
@JsonProperty("frequency")
|
||||
private double frequency = 1;
|
||||
|
||||
public EntityConfig getEntity() {
|
||||
return entity;
|
||||
}
|
||||
|
||||
public String getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
public double getMinDepth() {
|
||||
return minDepth;
|
||||
}
|
||||
|
||||
public double getMaxDepth() {
|
||||
return maxDepth;
|
||||
}
|
||||
|
||||
public Item getOrifice() {
|
||||
return orifice;
|
||||
}
|
||||
|
||||
public double getFrequency() {
|
||||
return frequency;
|
||||
}
|
||||
}
|
|
@ -20,7 +20,9 @@ import com.fasterxml.jackson.annotation.JsonCreator;
|
|||
|
||||
import brainwine.gameserver.GameServer;
|
||||
import brainwine.gameserver.entity.Entity;
|
||||
import brainwine.gameserver.entity.EntityStatus;
|
||||
import brainwine.gameserver.entity.EntityConfig;
|
||||
import brainwine.gameserver.entity.EntityRegistry;
|
||||
import brainwine.gameserver.entity.npc.Npc;
|
||||
import brainwine.gameserver.entity.player.ChatType;
|
||||
import brainwine.gameserver.entity.player.NotificationType;
|
||||
import brainwine.gameserver.entity.player.Player;
|
||||
|
@ -36,13 +38,12 @@ import brainwine.gameserver.server.messages.BlockChangeMessage;
|
|||
import brainwine.gameserver.server.messages.BlockMetaMessage;
|
||||
import brainwine.gameserver.server.messages.ChatMessage;
|
||||
import brainwine.gameserver.server.messages.ConfigurationMessage;
|
||||
import brainwine.gameserver.server.messages.EntityPositionMessage;
|
||||
import brainwine.gameserver.server.messages.EntityStatusMessage;
|
||||
import brainwine.gameserver.server.messages.LightMessage;
|
||||
import brainwine.gameserver.server.messages.ZoneExploredMessage;
|
||||
import brainwine.gameserver.server.messages.ZoneStatusMessage;
|
||||
import brainwine.gameserver.util.MapHelper;
|
||||
import brainwine.gameserver.util.MathUtils;
|
||||
import brainwine.gameserver.util.Vector2i;
|
||||
|
||||
public class Zone {
|
||||
|
||||
|
@ -65,10 +66,9 @@ public class Zone {
|
|||
private float acidity = 0;
|
||||
private final ChunkManager chunkManager;
|
||||
private final WeatherManager weatherManager = new WeatherManager();
|
||||
private final EntityManager entityManager = new EntityManager(this);
|
||||
private final Queue<DugBlock> digQueue = new ArrayDeque<>();
|
||||
private final Set<Integer> pendingSunlight = new HashSet<>();
|
||||
private final Map<Integer, Entity> entities = new HashMap<>();
|
||||
private final List<Player> players = new ArrayList<>();
|
||||
private final Map<String, Integer> dungeons = new HashMap<>();
|
||||
private final Map<Integer, MetaBlock> metaBlocks = new HashMap<>();
|
||||
private final Map<Integer, MetaBlock> globalMetaBlocks = new HashMap<>();
|
||||
|
@ -107,10 +107,7 @@ public class Zone {
|
|||
public void tick(float deltaTime) {
|
||||
long now = System.currentTimeMillis();
|
||||
weatherManager.tick(deltaTime);
|
||||
|
||||
for(Entity entity : getEntities()) {
|
||||
entity.tick();
|
||||
}
|
||||
entityManager.tick(deltaTime);
|
||||
|
||||
// One full cycle = 1200 seconds = 20 minutes
|
||||
time += deltaTime * (1.0F / 1200.0F);
|
||||
|
@ -119,7 +116,7 @@ public class Zone {
|
|||
time -= 1.0F;
|
||||
}
|
||||
|
||||
if(!players.isEmpty()) {
|
||||
if(!getPlayers().isEmpty()) {
|
||||
if(now >= lastStatusUpdate + 4000) {
|
||||
sendMessage(new ZoneStatusMessage(getStatusConfig()));
|
||||
lastStatusUpdate = now;
|
||||
|
@ -148,7 +145,7 @@ public class Zone {
|
|||
* @param message The message to send.
|
||||
*/
|
||||
public void sendMessage(Message message) {
|
||||
for(Player player : players) {
|
||||
for(Player player : getPlayers()) {
|
||||
player.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
@ -160,7 +157,7 @@ public class Zone {
|
|||
* @param chunk The chunk near which players must be.
|
||||
*/
|
||||
public void sendMessageToChunk(Message message, Chunk chunk) {
|
||||
for(Player player : players) {
|
||||
for(Player player : getPlayers()) {
|
||||
if(player.isChunkActive(chunk)) {
|
||||
player.sendMessage(message);
|
||||
}
|
||||
|
@ -182,6 +179,132 @@ public class Zone {
|
|||
sendMessage(new ChatMessage(sender.getId(), text, type));
|
||||
}
|
||||
|
||||
public boolean isPointVisibleFrom(int x1, int y1, int x2, int y2) {
|
||||
return raycast(x1, y1, x2, y2) == null;
|
||||
}
|
||||
|
||||
public Vector2i raycast(int x1, int y1, int x2, int y2) {
|
||||
List<Vector2i> path = raycast(x1, y1, x2, y2, false, false, false);
|
||||
|
||||
if(path != null && !path.isEmpty()) {
|
||||
return path.get(0);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Vector2i raynext(int x1, int y1, int x2, int y2) {
|
||||
List<Vector2i> path = raycast(x1, y1, x2, y2, true, true, true);
|
||||
|
||||
if(path != null && path.size() > 1) {
|
||||
return path.get(1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Shamelessly ported from the zone kernel (https://github.com/bytebin/deepworld-gameserver/blob/master/ext/lib/zone.c#L798)
|
||||
private List<Vector2i> raycast(int x1, int y1, int x2, int y2, boolean path, boolean all, boolean next) {
|
||||
List<Vector2i> coords = new ArrayList<>();
|
||||
int x = x1;
|
||||
int y = y1;
|
||||
int diffX = Math.abs(x2 - x1);
|
||||
int diffY = Math.abs(y2 - y1);
|
||||
int signX = (int)Math.signum(x2 - x1);
|
||||
int signY = (int)Math.signum(y2 - y1);
|
||||
boolean swap = false;
|
||||
|
||||
if(diffY > diffX) {
|
||||
int temp = diffX;
|
||||
diffX = diffY;
|
||||
diffY = temp;
|
||||
swap = true;
|
||||
}
|
||||
|
||||
int d = 2 * diffY - diffX;
|
||||
|
||||
for(int i = 0; i < diffX; i++) {
|
||||
if(!all && isBlockSolid(x, y)) {
|
||||
if(path) {
|
||||
return coords;
|
||||
}
|
||||
|
||||
coords.add(new Vector2i(x, y));
|
||||
return coords;
|
||||
}
|
||||
|
||||
if(path) {
|
||||
coords.add(new Vector2i(x, y));
|
||||
|
||||
if(next && i == 1) {
|
||||
return coords;
|
||||
}
|
||||
}
|
||||
|
||||
while(d >= 0) {
|
||||
d = d - 2 * diffX;
|
||||
|
||||
if(swap) {
|
||||
x += signX;
|
||||
} else {
|
||||
y += signY;
|
||||
}
|
||||
}
|
||||
|
||||
d = d + 2 * diffY;
|
||||
|
||||
if(swap) {
|
||||
y += signY;
|
||||
} else {
|
||||
x += signX;
|
||||
}
|
||||
}
|
||||
|
||||
return all ? coords : null;
|
||||
}
|
||||
|
||||
public boolean isBlockSolid(int x, int y) {
|
||||
return isBlockSolid(x, y, true);
|
||||
}
|
||||
|
||||
public boolean isBlockSolid(int x, int y, boolean checkAdjacents) {
|
||||
if(!areCoordinatesInBounds(x, y) || !isChunkLoaded(x, y)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Block block = getBlock(x, y);
|
||||
Item item = block.getItem(Layer.FRONT);
|
||||
|
||||
if(item.isDoor() && block.getFrontMod() % 2 == 0) {
|
||||
return true;
|
||||
} else if(!item.isDoor() && item.isSolid()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(checkAdjacents) {
|
||||
for(int i = -3; i <= 0; i++) {
|
||||
for(int j = 0; j <= 2; j++) {
|
||||
int x1 = x + i;
|
||||
int y1 = y + j;
|
||||
|
||||
if(!areCoordinatesInBounds(x1, y1) || !isChunkLoaded(x1, y1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
block = getBlock(x1, y1);
|
||||
item = block.getFrontItem();
|
||||
|
||||
if(item.getBlockWidth() > Math.abs(i) && item.getBlockHeight() > Math.abs(j)
|
||||
&& isBlockSolid(x1, y1, false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isBlockOccupied(int x, int y, Layer layer) {
|
||||
if(!areCoordinatesInBounds(x, y)) {
|
||||
return false;
|
||||
|
@ -193,6 +316,10 @@ public class Zone {
|
|||
return !item.isAir() && !item.canPlaceOver();
|
||||
}
|
||||
|
||||
public boolean isBlockProtected(int x, int y) {
|
||||
return isBlockProtected(x, y, null);
|
||||
}
|
||||
|
||||
public boolean isBlockProtected(int x, int y, Player from) {
|
||||
for(MetaBlock fieldBlock : fieldBlocks.values()) {
|
||||
Item item = fieldBlock.getItem();
|
||||
|
@ -200,13 +327,15 @@ public class Zone {
|
|||
int fY = fieldBlock.getY();
|
||||
int field = fieldBlock.getItem().getField();
|
||||
|
||||
if(item.isDish() && !ownsMetaBlock(fieldBlock, from)) {
|
||||
if(MathUtils.inRange(x, y, fX, fY, field)) {
|
||||
return true;
|
||||
}
|
||||
} else if(item.hasField() && !ownsMetaBlock(fieldBlock, from)) {
|
||||
if(x == fX && y == fY) {
|
||||
return true;
|
||||
if(from == null || !ownsMetaBlock(fieldBlock, from)) {
|
||||
if(item.isDish()) {
|
||||
if(MathUtils.inRange(x, y, fX, fY, field)) {
|
||||
return true;
|
||||
}
|
||||
} else if(item.hasField()) {
|
||||
if(x == fX && y == fY) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -351,6 +480,7 @@ public class Zone {
|
|||
metadata.put("@", dungeonId);
|
||||
|
||||
if(frontItem.hasUse(ItemUseType.GUARD)) {
|
||||
addGuardianEntities(metadata, frontItem.getGuardLevel());
|
||||
guardBlocks++;
|
||||
}
|
||||
}
|
||||
|
@ -406,9 +536,11 @@ public class Zone {
|
|||
List<MetaBlock> guardBlocks = getMetaBlocksWithUse(ItemUseType.GUARD);
|
||||
|
||||
for(MetaBlock metaBlock : guardBlocks) {
|
||||
String dungeonId = MapHelper.getString(metaBlock.getMetadata(), "@");
|
||||
Map<String, Object> metadata = metaBlock.getMetadata();
|
||||
String dungeonId = MapHelper.getString(metadata, "@");
|
||||
|
||||
if(dungeonId != null) {
|
||||
addGuardianEntities(metadata, metaBlock.getItem().getGuardLevel());
|
||||
int numGuardBlocks = dungeons.getOrDefault(dungeonId, 0);
|
||||
numGuardBlocks++;
|
||||
dungeons.put(dungeonId, numGuardBlocks);
|
||||
|
@ -416,6 +548,24 @@ public class Zone {
|
|||
}
|
||||
}
|
||||
|
||||
private void addGuardianEntities(Map<String, Object> metadata, int guardLevel) {
|
||||
List<String> guardians = MapHelper.getList(metadata, "!");
|
||||
|
||||
if(guardians == null) {
|
||||
guardians = new ArrayList<>();
|
||||
|
||||
if(guardLevel >= 5) {
|
||||
guardians.add("brains/large");
|
||||
} else if(guardLevel >= 3) {
|
||||
guardians.add("brains/medium");
|
||||
} else {
|
||||
guardians.add("brains/small");
|
||||
}
|
||||
|
||||
metadata.put("!", guardians);
|
||||
}
|
||||
}
|
||||
|
||||
public void destroyGuardBlock(String dungeonId, Player destroyer) {
|
||||
if(dungeons.containsKey(dungeonId)) {
|
||||
int guardBlocks = dungeons.get(dungeonId);
|
||||
|
@ -613,6 +763,10 @@ public class Zone {
|
|||
return metaBlocks.get(getBlockIndex(x, y));
|
||||
}
|
||||
|
||||
public MetaBlock getMetaBlock(int index) {
|
||||
return metaBlocks.get(index);
|
||||
}
|
||||
|
||||
public List<MetaBlock> getMetaBlocksWithUse(ItemUseType useType){
|
||||
return getMetaBlocksWhere(block -> block.getItem().hasUse(useType));
|
||||
}
|
||||
|
@ -640,35 +794,72 @@ public class Zone {
|
|||
return globalMetaBlocks.values();
|
||||
}
|
||||
|
||||
public void addPlayer(Player player) {
|
||||
addEntity(player);
|
||||
player.onZoneChanged();
|
||||
player.sendMessageToPeers(new EntityStatusMessage(player, EntityStatus.ENTERING));
|
||||
player.sendMessageToPeers(new EntityPositionMessage(player));
|
||||
players.add(player);
|
||||
public List<Entity> getEntitiesInRange(float x, float y, float range) {
|
||||
return entityManager.getEntitiesInRange(x, y, range);
|
||||
}
|
||||
|
||||
public void removePlayer(Player player) {
|
||||
players.remove(player);
|
||||
player.sendMessageToPeers(new EntityStatusMessage(player, EntityStatus.EXITING));
|
||||
removeEntity(player);
|
||||
public Player getRandomPlayerInRange(float x, float y, float range) {
|
||||
return entityManager.getRandomPlayerInRange(x, y, range);
|
||||
}
|
||||
|
||||
private void addEntity(Entity entity) {
|
||||
entity.setZone(this);
|
||||
entities.put(entity.getId(), entity);
|
||||
public List<Player> getPlayersInRange(float x, float y, float range) {
|
||||
return entityManager.getPlayersInRange(x, y, range);
|
||||
}
|
||||
|
||||
private void removeEntity(Entity entity) {
|
||||
entities.remove(entity.getId());
|
||||
public void spawnEntity(Entity entity, int x, int y) {
|
||||
entityManager.spawnEntity(entity, x, y);
|
||||
}
|
||||
|
||||
public void spawnEntity(Entity entity, int x, int y, boolean effect) {
|
||||
entityManager.spawnEntity(entity, x, y, effect);
|
||||
}
|
||||
|
||||
public void addEntity(Entity entity) {
|
||||
entityManager.addEntity(entity);
|
||||
}
|
||||
|
||||
public void removeEntity(Entity entity) {
|
||||
entityManager.removeEntity(entity);
|
||||
}
|
||||
|
||||
public Entity getEntity(int entityId) {
|
||||
return entityManager.getEntity(entityId);
|
||||
}
|
||||
|
||||
public int getEntityCount() {
|
||||
return entityManager.getEntityCount();
|
||||
}
|
||||
|
||||
public Collection<Entity> getEntities() {
|
||||
return entities.values();
|
||||
return entityManager.getEntities();
|
||||
}
|
||||
|
||||
public List<Player> getPlayers() {
|
||||
return players;
|
||||
public Npc getNpc(int entityId) {
|
||||
return entityManager.getNpc(entityId);
|
||||
}
|
||||
|
||||
public int getNpcCount() {
|
||||
return entityManager.getNpcCount();
|
||||
}
|
||||
|
||||
public Collection<Npc> getNpcs() {
|
||||
return entityManager.getNpcs();
|
||||
}
|
||||
|
||||
public Player getPlayer(int entityId) {
|
||||
return entityManager.getPlayer(entityId);
|
||||
}
|
||||
|
||||
public Player getPlayer(String name) {
|
||||
return entityManager.getPlayer(name);
|
||||
}
|
||||
|
||||
public int getPlayerCount() {
|
||||
return entityManager.getPlayerCount();
|
||||
}
|
||||
|
||||
public Collection<Player> getPlayers() {
|
||||
return entityManager.getPlayers();
|
||||
}
|
||||
|
||||
public boolean areCoordinatesInBounds(int x, int y) {
|
||||
|
@ -688,8 +879,30 @@ public class Zone {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO more chunk related thingies, such as surface calculations,
|
||||
// entity spawning and block indexing
|
||||
// Spawn guardian entities
|
||||
for(int x = 0; x < chunk.getWidth(); x++) {
|
||||
for(int y = 0; y < chunk.getHeight(); y++) {
|
||||
int x1 = chunk.getX() + x;
|
||||
int y1 = chunk.getY() + y;
|
||||
int index = getBlockIndex(x1, y1);
|
||||
MetaBlock metaBlock = getMetaBlock(index);
|
||||
|
||||
if(metaBlock != null) {
|
||||
List<String> guardians = MapHelper.getList(metaBlock.getMetadata(), "!", Collections.emptyList());
|
||||
|
||||
for(String guardian : guardians) {
|
||||
EntityConfig config = EntityRegistry.getEntityConfig(guardian);
|
||||
|
||||
if(config != null) {
|
||||
Npc entity = new Npc(this, config);
|
||||
entity.setPosition(x1, y1);
|
||||
entity.setGuardBlock(index);
|
||||
addEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void onChunkUnloaded(Chunk chunk) {
|
||||
|
@ -699,6 +912,10 @@ public class Zone {
|
|||
public void saveChunks() {
|
||||
chunkManager.saveChunks();
|
||||
}
|
||||
|
||||
public List<Chunk> getVisibleChunks() {
|
||||
return chunkManager.getVisibleChunks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Should only be called by zone gen.
|
||||
|
|
609
gameserver/src/main/resources/defaults/spawning.json
Normal file
609
gameserver/src/main/resources/defaults/spawning.json
Normal file
|
@ -0,0 +1,609 @@
|
|||
{
|
||||
"plain": [
|
||||
{
|
||||
"entity": "creatures/rat",
|
||||
"locale": "cave",
|
||||
"frequency": 10
|
||||
},
|
||||
{
|
||||
"entity": "creatures/skunk",
|
||||
"locale": "cave",
|
||||
"max_depth": 0.3,
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "creatures/crow",
|
||||
"locale": "sky",
|
||||
"frequency": 12
|
||||
},
|
||||
{
|
||||
"entity": "creatures/vulture",
|
||||
"locale": "sky",
|
||||
"frequency": 1
|
||||
},
|
||||
{
|
||||
"entity": "creatures/crow-auto",
|
||||
"locale": "sky",
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "creatures/bluejay",
|
||||
"locale": "sky",
|
||||
"purification": 1.0,
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "creatures/cardinal",
|
||||
"locale": "sky",
|
||||
"purification": 1.0,
|
||||
"frequency": 1
|
||||
},
|
||||
{
|
||||
"entity": "creatures/seagull",
|
||||
"locale": "sky",
|
||||
"purification": 1.0,
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "creatures/butterfly-monarch",
|
||||
"locale": "sky",
|
||||
"purification": 1.0,
|
||||
"frequency":23
|
||||
},
|
||||
{
|
||||
"entity": "creatures/papilio-ulysses",
|
||||
"locale": "sky",
|
||||
"purification": 1.0,
|
||||
"frequency": 0.5
|
||||
},
|
||||
{
|
||||
"entity": "creatures/butterfly-swallowtail",
|
||||
"locale": "sky",
|
||||
"purification": 1.0,
|
||||
"frequency": 1
|
||||
},
|
||||
{
|
||||
"entity": "creatures/butterfly-moth",
|
||||
"locale": "sky",
|
||||
"purification": 1.0,
|
||||
"frequency": 5
|
||||
},
|
||||
{
|
||||
"entity": "creatures/butterfly-owl",
|
||||
"locale": "sky",
|
||||
"purification": 1.0,
|
||||
"frequency": 0.1
|
||||
},
|
||||
{
|
||||
"entity": "creatures/butterfly-paper-kite",
|
||||
"locale": "sky",
|
||||
"purification": 1.0,
|
||||
"frequency": 0.01
|
||||
},
|
||||
{
|
||||
"entity": "creatures/bat",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"frequency": 9
|
||||
},
|
||||
{
|
||||
"entity": "creatures/bat-auto",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "creatures/scorpion",
|
||||
"locale": "cave",
|
||||
"frequency": 1
|
||||
},
|
||||
{
|
||||
"entity": "creatures/roach",
|
||||
"locale": "cave",
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "creatures/roach-large",
|
||||
"locale": "cave",
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "terrapus/child",
|
||||
"locale": "cave",
|
||||
"max_depth": 0.4,
|
||||
"orifice": "base/maw",
|
||||
"frequency": 20
|
||||
},
|
||||
{
|
||||
"entity": "terrapus/adult",
|
||||
"locale": "cave",
|
||||
"max_depth": 0.7,
|
||||
"orifice": "base/maw",
|
||||
"frequency": 20
|
||||
},
|
||||
{
|
||||
"entity": "terrapus/acid",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.4,
|
||||
"orifice": "base/maw",
|
||||
"frequency": 13
|
||||
},
|
||||
{
|
||||
"entity": "terrapus/fire",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.6,
|
||||
"orifice": "base/maw",
|
||||
"frequency": 8
|
||||
},
|
||||
{
|
||||
"entity": "automata/tiny",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 12
|
||||
},
|
||||
{
|
||||
"entity": "automata/small",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.35,
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 8
|
||||
},
|
||||
{
|
||||
"entity": "automata/medium",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.65,
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 6
|
||||
},
|
||||
{
|
||||
"entity": "brains/small",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.8,
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 1
|
||||
},
|
||||
{
|
||||
"entity": "brains/medium",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.9,
|
||||
"frequency": 1
|
||||
}
|
||||
],
|
||||
"arctic": [
|
||||
{
|
||||
"entity": "terrapus/adult",
|
||||
"locale": "cave",
|
||||
"max_depth": 0.7,
|
||||
"orifice": "base/maw",
|
||||
"frequency": 8
|
||||
},
|
||||
{
|
||||
"entity": "terrapus/frost",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.4,
|
||||
"orifice": "base/maw",
|
||||
"frequency": 9
|
||||
},
|
||||
{
|
||||
"entity": "creatures/bunny-ice",
|
||||
"locale": "cave",
|
||||
"max_depth": 0.5,
|
||||
"orifice": "base/maw",
|
||||
"frequency": 6
|
||||
},
|
||||
{
|
||||
"entity": "creatures/roach",
|
||||
"locale": "cave",
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "creatures/roach-large",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "automata/tiny",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.5,
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 5
|
||||
},
|
||||
{
|
||||
"entity": "automata/small",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.45,
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 4
|
||||
},
|
||||
{
|
||||
"entity": "automata/medium",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.7,
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "brains/small",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.6,
|
||||
"orifice": "base/maw",
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "brains/medium",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.7,
|
||||
"frequency": 2
|
||||
}
|
||||
],
|
||||
"hell": [
|
||||
{
|
||||
"entity": "creatures/butterfly-rumanzovia",
|
||||
"locale": "sky",
|
||||
"frequency": 0.05
|
||||
},
|
||||
{
|
||||
"entity": "terrapus/adult",
|
||||
"locale": "cave",
|
||||
"max_depth": 0.7,
|
||||
"orifice": "base/maw",
|
||||
"frequency": 4
|
||||
},
|
||||
{
|
||||
"entity": "terrapus/skeleton",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"frequency": 8
|
||||
},
|
||||
{
|
||||
"entity": "terrapus/fire",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.35,
|
||||
"orifice": "base/maw",
|
||||
"frequency": 5
|
||||
},
|
||||
{
|
||||
"entity": "ghost",
|
||||
"locale": "cave",
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "revenant",
|
||||
"locale": "cave",
|
||||
"frequency": 5
|
||||
},
|
||||
{
|
||||
"entity": "dire-revenant",
|
||||
"locale": "cave",
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "brains/small",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.5,
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 1
|
||||
},
|
||||
{
|
||||
"entity": "brains/medium",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.7,
|
||||
"frequency": 1
|
||||
}
|
||||
],
|
||||
"desert": [
|
||||
{
|
||||
"entity": "creatures/rat",
|
||||
"locale": "cave",
|
||||
"frequency": 4
|
||||
},
|
||||
{
|
||||
"entity": "creatures/armadillo",
|
||||
"locale": "cave",
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "creatures/crow",
|
||||
"locale": "sky",
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "creatures/vulture",
|
||||
"locale": "sky",
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "creatures/bat",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"frequency": 6
|
||||
},
|
||||
{
|
||||
"entity": "creatures/bat-auto",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "creatures/scorpion",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"frequency": 8
|
||||
},
|
||||
{
|
||||
"entity": "creatures/scorpion-large",
|
||||
"locale": "cave",
|
||||
"frequency": 5
|
||||
},
|
||||
{
|
||||
"entity": "creatures/roach",
|
||||
"locale": "cave",
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "creatures/roach-large",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "terrapus/adult",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"max_depth": 0.7,
|
||||
"frequency": 8
|
||||
},
|
||||
{
|
||||
"entity": "terrapus/fire",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"min_depth": 0.4,
|
||||
"frequency": 6
|
||||
},
|
||||
{
|
||||
"entity": "automata/tiny",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "automata/small",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"min_depth": 0.55,
|
||||
"frequency": 5
|
||||
},
|
||||
{
|
||||
"entity": "automata/medium",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"min_depth": 0.8,
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "brains/small",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"min_depth": 0.5,
|
||||
"frequency": 1
|
||||
},
|
||||
{
|
||||
"entity": "brains/medium",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.7,
|
||||
"frequency": 1
|
||||
}
|
||||
],
|
||||
"brain": [
|
||||
{
|
||||
"entity": "creatures/butterfly-green",
|
||||
"locale": "sky",
|
||||
"frequency": 0.25
|
||||
},
|
||||
{
|
||||
"entity": "terrapus/adult",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"max_depth": 0.7,
|
||||
"frequency": 1
|
||||
},
|
||||
{
|
||||
"entity": "terrapus/acid",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"frequency": 9
|
||||
},
|
||||
{
|
||||
"entity": "automata/tiny",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "automata/small",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "automata/medium",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "automata/large",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "brains/tiny-flyer",
|
||||
"locale": "sky",
|
||||
"frequency": 5
|
||||
},
|
||||
{
|
||||
"entity": "brains/small",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "brains/medium",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.4,
|
||||
"frequency": 1
|
||||
},
|
||||
{
|
||||
"entity": "brains/large",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.85,
|
||||
"frequency": 1
|
||||
}
|
||||
],
|
||||
"deep": [
|
||||
{
|
||||
"entity": "creatures/bat",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"frequency": 9
|
||||
},
|
||||
{
|
||||
"entity": "creatures/bat-auto",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "creatures/roach-large",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "terrapus/adult",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"max_depth": 0.4,
|
||||
"frequency": 14
|
||||
},
|
||||
{
|
||||
"entity": "terrapus/acid",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"frequency": 12
|
||||
},
|
||||
{
|
||||
"entity": "terrapus/fire",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"min_depth": 0.2,
|
||||
"frequency": 8
|
||||
},
|
||||
{
|
||||
"entity": "terrapus/queen",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.333,
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "automata/tiny",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 8
|
||||
},
|
||||
{
|
||||
"entity": "automata/small",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 10
|
||||
},
|
||||
{
|
||||
"entity": "automata/medium",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 7
|
||||
},
|
||||
{
|
||||
"entity": "automata/large",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"min_depth": 0.5,
|
||||
"frequency": 5
|
||||
},
|
||||
{
|
||||
"entity": "brains/small",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"min_depth": 0.4,
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "brains/medium",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.6,
|
||||
"frequency": 1
|
||||
}
|
||||
],
|
||||
"space": [
|
||||
{
|
||||
"entity": "automata/tiny",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "automata/small",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "automata/medium",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "automata/large",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "brains/tiny-flyer",
|
||||
"locale": "sky",
|
||||
"frequency": 5
|
||||
},
|
||||
{
|
||||
"entity": "brains/small",
|
||||
"locale": "cave",
|
||||
"orifice": "base/pipe",
|
||||
"frequency": 2
|
||||
},
|
||||
{
|
||||
"entity": "brains/medium",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.4,
|
||||
"frequency": 1
|
||||
},
|
||||
{
|
||||
"entity": "brains/large",
|
||||
"locale": "cave",
|
||||
"min_depth": 0.85,
|
||||
"frequency": 1
|
||||
},
|
||||
{
|
||||
"entity": "creatures/bat-auto",
|
||||
"locale": "cave",
|
||||
"orifice": "base/maw",
|
||||
"frequency": 3
|
||||
},
|
||||
{
|
||||
"entity": "creatures/crow-auto",
|
||||
"locale": "sky",
|
||||
"frequency": 3
|
||||
}
|
||||
]
|
||||
}
|
|
@ -22,6 +22,7 @@ public class JsonHelper {
|
|||
.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE, true)
|
||||
.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
|
||||
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true)
|
||||
.configure(MapperFeature.USE_BASE_TYPE_AS_DEFAULT_IMPL, true)
|
||||
.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
|
||||
private static final ObjectWriter writer = mapper.writer(CustomPrettyPrinter.INSTANCE);
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue