From a1aff443b3b91336fc4a43a25ea7425ecfd33855 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Fri, 22 Jul 2022 02:40:30 +0200 Subject: [PATCH] NPC & damage system phase 1 --- .../java/brainwine/gameserver/GameServer.java | 4 + .../gameserver/behavior/Behavior.java | 65 ++ .../behavior/CompositeBehavior.java | 60 ++ .../gameserver/behavior/SelectorBehavior.java | 27 + .../gameserver/behavior/SequenceBehavior.java | 64 ++ .../behavior/composed/CrawlerBehavior.java | 41 ++ .../behavior/composed/FlyerBehavior.java | 36 ++ .../behavior/composed/WalkerBehavior.java | 38 ++ .../behavior/parts/ClimbBehavior.java | 40 ++ .../behavior/parts/FallBehavior.java | 26 + .../behavior/parts/FlyBehavior.java | 66 ++ .../behavior/parts/FlyTowardBehavior.java | 50 ++ .../behavior/parts/FollowBehavior.java | 25 + .../behavior/parts/IdleBehavior.java | 85 +++ .../parts/RandomlyTargetBehavior.java | 62 ++ .../behavior/parts/ShielderBehavior.java | 101 +++ .../behavior/parts/SpawnAttackBehavior.java | 76 +++ .../behavior/parts/TurnBehavior.java | 27 + .../behavior/parts/WalkBehavior.java | 28 + .../gameserver/command/CommandManager.java | 2 + .../command/commands/EntityCommand.java | 55 ++ .../brainwine/gameserver/entity/Entity.java | 77 ++- .../gameserver/entity/EntityConfig.java | 133 ++++ .../gameserver/entity/EntityLoot.java | 34 + .../gameserver/entity/EntityRegistry.java | 68 ++ .../gameserver/entity/EntityType.java | 22 - .../brainwine/gameserver/entity/npc/Npc.java | 360 +++++++++++ .../gameserver/entity/player/Player.java | 81 ++- .../brainwine/gameserver/item/DamageType.java | 42 ++ .../java/brainwine/gameserver/item/Item.java | 37 ++ .../messages/EntityPositionMessage.java | 6 +- .../server/messages/EntityStatusMessage.java | 5 +- .../server/requests/AuthenticateRequest.java | 2 +- .../server/requests/EntitiesRequest.java | 13 +- .../server/requests/HealthRequest.java | 3 +- .../server/requests/InventoryUseRequest.java | 28 + .../server/requests/MoveRequest.java | 2 +- .../brainwine/gameserver/util/MapHelper.java | 6 + .../java/brainwine/gameserver/util/Pair.java | 26 + .../gameserver/zone/ChunkManager.java | 29 + .../gameserver/zone/EntityManager.java | 287 +++++++++ .../gameserver/zone/EntitySpawn.java | 54 ++ .../java/brainwine/gameserver/zone/Zone.java | 297 +++++++-- .../src/main/resources/defaults/spawning.json | 609 ++++++++++++++++++ .../java/brainwine/shared/JsonHelper.java | 1 + 45 files changed, 3106 insertions(+), 94 deletions(-) create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/Behavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/CompositeBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/SelectorBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/SequenceBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/composed/CrawlerBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/composed/FlyerBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/composed/WalkerBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/parts/ClimbBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/parts/FallBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyTowardBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/parts/FollowBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/parts/IdleBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/parts/RandomlyTargetBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/parts/ShielderBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/parts/SpawnAttackBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/parts/TurnBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/behavior/parts/WalkBehavior.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/command/commands/EntityCommand.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/EntityLoot.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/EntityRegistry.java delete mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/EntityType.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/item/DamageType.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/util/Pair.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/zone/EntitySpawn.java create mode 100644 gameserver/src/main/resources/defaults/spawning.json diff --git a/gameserver/src/main/java/brainwine/gameserver/GameServer.java b/gameserver/src/main/java/brainwine/gameserver/GameServer.java index 18c740f..1406920 100644 --- a/gameserver/src/main/java/brainwine/gameserver/GameServer.java +++ b/gameserver/src/main/java/brainwine/gameserver/GameServer.java @@ -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(); diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/Behavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/Behavior.java new file mode 100644 index 0000000..7808d71 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/Behavior.java @@ -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; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/CompositeBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/CompositeBehavior.java new file mode 100644 index 0000000..a643cf7 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/CompositeBehavior.java @@ -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 children = new ArrayList<>(); + + public CompositeBehavior(Npc entity, Map config) { + super(entity); + addChildren(config); + } + + public CompositeBehavior(Npc entity) { + this(entity, Collections.emptyMap()); + } + + protected void addChildren(Map config) { + // Override + } + + public void addChild(Class type, Map 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 getChildren() { + return Collections.unmodifiableCollection(children); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/SelectorBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/SelectorBehavior.java new file mode 100644 index 0000000..bf1025a --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/SelectorBehavior.java @@ -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 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; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/SequenceBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/SequenceBehavior.java new file mode 100644 index 0000000..595178b --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/SequenceBehavior.java @@ -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 loggedInvalidTypes = new ArrayList<>(); + + public SequenceBehavior(Npc entity, Map config) { + super(entity, config); + } + + public SequenceBehavior(Npc entity) { + super(entity); + } + + public static SequenceBehavior createBehaviorTree(Npc npc, List> behavior) { + SequenceBehavior root = new SequenceBehavior(npc); + + for(Map 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; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/CrawlerBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/composed/CrawlerBehavior.java new file mode 100644 index 0000000..55b723c --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/composed/CrawlerBehavior.java @@ -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 config) { + super(entity, config); + } + + public CrawlerBehavior(Npc entity) { + super(entity); + } + + @Override + public void addChildren(Map 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)); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/FlyerBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/composed/FlyerBehavior.java new file mode 100644 index 0000000..d511c6f --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/composed/FlyerBehavior.java @@ -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 config) { + super(entity, config); + } + + public FlyerBehavior(Npc entity) { + super(entity); + } + + @Override + public void addChildren(Map config) { + if(config.containsKey("idle")) { + addChild(IdleBehavior.class, MapHelper.getMap(config, "idle")); + } + + addChild(FlyTowardBehavior.class, config); + addChild(FlyBehavior.class, config); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/composed/WalkerBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/composed/WalkerBehavior.java new file mode 100644 index 0000000..f3cbc37 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/composed/WalkerBehavior.java @@ -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 config) { + super(entity, config); + } + + public WalkerBehavior(Npc entity) { + super(entity); + } + + @Override + protected void addChildren(Map config) { + if(config.containsKey("idle")) { + addChild(IdleBehavior.class, MapHelper.getMap(config, "idle")); + } + + addChild(new WalkBehavior(entity)); + addChild(new FallBehavior(entity)); + addChild(new TurnBehavior(entity)); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ClimbBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ClimbBehavior.java new file mode 100644 index 0000000..4c89bf2 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ClimbBehavior.java @@ -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; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FallBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FallBehavior.java new file mode 100644 index 0000000..018eb2b --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FallBehavior.java @@ -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(); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyBehavior.java new file mode 100644 index 0000000..2067687 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyBehavior.java @@ -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; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyTowardBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyTowardBehavior.java new file mode 100644 index 0000000..baa0a9d --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FlyTowardBehavior.java @@ -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; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FollowBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FollowBehavior.java new file mode 100644 index 0000000..087b004 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/FollowBehavior.java @@ -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; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/IdleBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/IdleBehavior.java new file mode 100644 index 0000000..64bb753 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/IdleBehavior.java @@ -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; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/RandomlyTargetBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/RandomlyTargetBehavior.java new file mode 100644 index 0000000..897c2f9 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/RandomlyTargetBehavior.java @@ -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; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ShielderBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ShielderBehavior.java new file mode 100644 index 0000000..1c8e94c --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/ShielderBehavior.java @@ -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 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> 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; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/SpawnAttackBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/SpawnAttackBehavior.java new file mode 100644 index 0000000..99010b2 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/SpawnAttackBehavior.java @@ -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; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/TurnBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/TurnBehavior.java new file mode 100644 index 0000000..8ae0134 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/TurnBehavior.java @@ -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); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/behavior/parts/WalkBehavior.java b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/WalkBehavior.java new file mode 100644 index 0000000..abebce6 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/behavior/parts/WalkBehavior.java @@ -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); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/command/CommandManager.java b/gameserver/src/main/java/brainwine/gameserver/command/CommandManager.java index b065179..1f3eaa1 100644 --- a/gameserver/src/main/java/brainwine/gameserver/command/CommandManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/command/CommandManager.java @@ -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) { diff --git a/gameserver/src/main/java/brainwine/gameserver/command/commands/EntityCommand.java b/gameserver/src/main/java/brainwine/gameserver/command/commands/EntityCommand.java new file mode 100644 index 0000000..aa5caab --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/command/commands/EntityCommand.java @@ -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 "; + } + + @Override + public boolean canExecute(CommandExecutor executor) { + return executor.isAdmin() && executor instanceof Player; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java index 5398306..0473e79 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/Entity.java @@ -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 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 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; } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java b/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java new file mode 100644 index 0000000..e103ec3 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java @@ -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 loot = new ArrayList<>(); + + @JsonProperty("loot_by_weapon") + private Map> lootByWeapon = new HashMap<>(); + + @JsonProperty("defense") + private Map resistances = new HashMap<>(); + + @JsonProperty("weakness") + private Map weaknesses = new HashMap<>(); + + @JsonProperty("components") + private Map components = Collections.emptyMap(); + + @JsonProperty("set_attachments") + private Map attachments = Collections.emptyMap(); + + @JsonProperty("behavior") + private List> behavior = Collections.emptyList(); + + @JsonProperty("animations") + private List> animations = Collections.emptyList(); + + @JsonProperty("slots") + private List slots = Collections.emptyList(); + + @JsonProperty("attachments") + private List 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 getLoot() { + return loot; + } + + public Map> getLootByWeapon() { + return lootByWeapon; + } + + public Map getResistances() { + return resistances; + } + + public Map getWeaknesses() { + return weaknesses; + } + + public Map getComponents() { + return components; + } + + public Map getAttachments() { + return attachments; + } + + public List> getBehavior() { + return behavior; + } + + public List> getAnimations() { + return animations; + } + + public List getSlots() { + return slots; + } + + public List getPossibleAttachments() { + return possibleAttachments; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/EntityLoot.java b/gameserver/src/main/java/brainwine/gameserver/entity/EntityLoot.java new file mode 100644 index 0000000..06f1d58 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/entity/EntityLoot.java @@ -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; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/EntityRegistry.java b/gameserver/src/main/java/brainwine/gameserver/entity/EntityRegistry.java new file mode 100644 index 0000000..4a2eab9 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/entity/EntityRegistry.java @@ -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 entities = new HashMap<>(); + private static boolean initalized; + + public static void init() { + if(initalized) { + logger.warn("Already initialized!"); + return; + } + + Map> entityConfigs = MapHelper.getMap(GameConfiguration.getBaseConfig(), "entities"); + + if(entityConfigs == null) { + logger.warn("No entity configurations exist!"); + return; + } + + for(Entry> entry : entityConfigs.entrySet()) { + String name = entry.getKey(); + Map 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); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/EntityType.java b/gameserver/src/main/java/brainwine/gameserver/entity/EntityType.java deleted file mode 100644 index fd3d359..0000000 --- a/gameserver/src/main/java/brainwine/gameserver/entity/EntityType.java +++ /dev/null @@ -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; - } -} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java new file mode 100644 index 0000000..3fc54e8 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java @@ -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 properties = new HashMap<>(); + private final Map baseDefenses = new HashMap<>(); + private final Map activeDefenses = new HashMap<>(); + private final Map> recentAttacks = new HashMap<>(); + private final WeightedMap loot = new WeightedMap<>(); + private final Map> lootByWeapon = new HashMap<>(); + private final List 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> behavior = new ArrayList<>(config.getBehavior()); + Map attachments = new HashMap<>(config.getAttachments()); + Map 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 selectedComponents = new ArrayList<>(); + + for(Entry 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 slots = new HashMap<>(); + + for(Entry 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 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 getStatusConfig() { + Map 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 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> 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); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java index 45f1d98..8b4c789 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java @@ -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 skills = new HashMap<>(); private final Set activeChunks = new HashSet<>(); private final Map> dialogs = new HashMap<>(); + private final List 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 entitiesInRange = zone.getEntitiesInRange(x, y, ENTITY_VISIBILITY_RANGE); + entitiesInRange.remove(this); + List enteredEntities = entitiesInRange.stream().filter(entity -> !trackedEntities.contains(entity)) + .collect(Collectors.toList()); + List 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 getTrackedEntities() { + return trackedEntities; + } + public void setConnection(Connection connection) { if(isOnline()) { kick("You logged in from another location."); diff --git a/gameserver/src/main/java/brainwine/gameserver/item/DamageType.java b/gameserver/src/main/java/brainwine/gameserver/item/DamageType.java new file mode 100644 index 0000000..b4dee83 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/item/DamageType.java @@ -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(); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/item/Item.java b/gameserver/src/main/java/brainwine/gameserver/item/Item.java index cb9db21..0cb32c0 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/Item.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/Item.java @@ -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 damageInfo; + @JsonProperty("ingredients") private List 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(); } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityPositionMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityPositionMessage.java index 1a18838..c7f8004 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityPositionMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityPositionMessage.java @@ -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; diff --git a/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityStatusMessage.java b/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityStatusMessage.java index 82fc375..c7ac9f0 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityStatusMessage.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/messages/EntityStatusMessage.java @@ -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 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 details) { + public EntityStatusMessage(int id, int type, String name, EntityStatus status, Map details) { this.id = id; this.type = type; this.name = name; diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/AuthenticateRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/AuthenticateRequest.java index 5bef5cb..b8151ec 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/AuthenticateRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/AuthenticateRequest.java @@ -53,7 +53,7 @@ public class AuthenticateRequest extends Request { return; } - zone.addPlayer(player); + zone.addEntity(player); }); }); } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/EntitiesRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/EntitiesRequest.java index 8eb2088..4429de3 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/EntitiesRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/EntitiesRequest.java @@ -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)); + } + } } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/HealthRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/HealthRequest.java index 459bebb..aa3a4ce 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/HealthRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/HealthRequest.java @@ -22,7 +22,6 @@ public class HealthRequest extends PlayerRequest { return; } - // TODO - player.setHealth(10); + player.damage(player.getHealth() - health, null); } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java index fbfaf77..3cc6e91 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/InventoryUseRequest.java @@ -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)); diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/MoveRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/MoveRequest.java index 3b55c61..3527982 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/MoveRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/MoveRequest.java @@ -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 } diff --git a/gameserver/src/main/java/brainwine/gameserver/util/MapHelper.java b/gameserver/src/main/java/brainwine/gameserver/util/MapHelper.java index f872e07..ce3b5e3 100644 --- a/gameserver/src/main/java/brainwine/gameserver/util/MapHelper.java +++ b/gameserver/src/main/java/brainwine/gameserver/util/MapHelper.java @@ -28,6 +28,12 @@ public class MapHelper { return new HashMap<>(); } + public static Map map(K key, V value) { + Map map = new HashMap<>(); + map.put(key, value); + return map; + } + public static void put(Map map, String path, Object value) { String[] segments = path.split("\\."); Map current = (Map)map; diff --git a/gameserver/src/main/java/brainwine/gameserver/util/Pair.java b/gameserver/src/main/java/brainwine/gameserver/util/Pair.java new file mode 100644 index 0000000..1984068 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/util/Pair.java @@ -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 { + + 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; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java index e8edadf..1c75772 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/ChunkManager.java @@ -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 getVisibleChunks() { + List visibleChunks = new ArrayList<>(); + Set 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); diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java b/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java new file mode 100644 index 0000000..2a5aac2 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/zone/EntityManager.java @@ -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> spawns = new HashMap<>(); + private final Map entities = new HashMap<>(); + private final Map npcs = new HashMap<>(); + private final Map players = new HashMap<>(); + private final Map 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> loot = JsonHelper.readValue(file, new TypeReference>>(){}); + 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 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 visibleChunks = zone.getVisibleChunks(); + List chunks = immediate ? visibleChunks : zone.getLoadedChunks().stream() + .filter(chunk -> !visibleChunks.contains(chunk)).collect(Collectors.toList()); + + if(!chunks.isEmpty()) { + List 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 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 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 players = getPlayersInRange(x, y, range); + return players.isEmpty() ? null : players.get(random.nextInt(players.size())); + } + + public List 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 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 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 getPlayers() { + return Collections.unmodifiableCollection(players.values()); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/EntitySpawn.java b/gameserver/src/main/java/brainwine/gameserver/zone/EntitySpawn.java new file mode 100644 index 0000000..0b2fe79 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/zone/EntitySpawn.java @@ -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; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java index 69019c2..9464a6f 100644 --- a/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java +++ b/gameserver/src/main/java/brainwine/gameserver/zone/Zone.java @@ -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 digQueue = new ArrayDeque<>(); private final Set pendingSunlight = new HashSet<>(); - private final Map entities = new HashMap<>(); - private final List players = new ArrayList<>(); private final Map dungeons = new HashMap<>(); private final Map metaBlocks = new HashMap<>(); private final Map 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 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 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 raycast(int x1, int y1, int x2, int y2, boolean path, boolean all, boolean next) { + List 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 guardBlocks = getMetaBlocksWithUse(ItemUseType.GUARD); for(MetaBlock metaBlock : guardBlocks) { - String dungeonId = MapHelper.getString(metaBlock.getMetadata(), "@"); + Map 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 metadata, int guardLevel) { + List 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 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 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 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 getEntities() { - return entities.values(); + return entityManager.getEntities(); } - public List getPlayers() { - return players; + public Npc getNpc(int entityId) { + return entityManager.getNpc(entityId); + } + + public int getNpcCount() { + return entityManager.getNpcCount(); + } + + public Collection 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 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 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 getVisibleChunks() { + return chunkManager.getVisibleChunks(); + } /** * Should only be called by zone gen. diff --git a/gameserver/src/main/resources/defaults/spawning.json b/gameserver/src/main/resources/defaults/spawning.json new file mode 100644 index 0000000..c4e45ac --- /dev/null +++ b/gameserver/src/main/resources/defaults/spawning.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/shared/src/main/java/brainwine/shared/JsonHelper.java b/shared/src/main/java/brainwine/shared/JsonHelper.java index 7a9f762..82cf55b 100644 --- a/shared/src/main/java/brainwine/shared/JsonHelper.java +++ b/shared/src/main/java/brainwine/shared/JsonHelper.java @@ -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);