NPC & damage system phase 1

This commit is contained in:
kuroppoi 2022-07-22 02:40:30 +02:00
parent 5ce226813f
commit a1aff443b3
45 changed files with 3106 additions and 94 deletions

View file

@ -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();

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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));
}
}

View file

@ -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);
}
}

View file

@ -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));
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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.");

View file

@ -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();
}
}

View file

@ -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();
}

View file

@ -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;

View file

@ -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;

View file

@ -53,7 +53,7 @@ public class AuthenticateRequest extends Request {
return;
}
zone.addPlayer(player);
zone.addEntity(player);
});
});
}

View file

@ -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));
}
}
}
}

View file

@ -22,7 +22,6 @@ public class HealthRequest extends PlayerRequest {
return;
}
// TODO
player.setHealth(10);
player.damage(player.getHealth() - health, null);
}
}

View file

@ -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));

View file

@ -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
}

View file

@ -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;

View 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;
}
}

View file

@ -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);

View file

@ -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());
}
}

View file

@ -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;
}
}

View file

@ -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.

View 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
}
]
}

View file

@ -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);