From 61c5599265ccc14f366d622254c3202a4fd98768 Mon Sep 17 00:00:00 2001 From: austin Date: Thu, 15 Aug 2024 09:52:32 -0400 Subject: [PATCH] true behavior trees --- assets/Data/creatures/human.json | 4 +- docs/src/progress/currenttarget.md | 4 +- docs/src/progress/renderertodo.md | 9 ++ .../java/electrosphere/engine/Globals.java | 2 +- .../entity/state/attack/ServerAttackTree.java | 21 ++++ .../groundmove/ServerGroundMovementTree.java | 12 ++ .../physicssync/ClientPhysicsSyncTree.java | 13 +++ .../types/camera/CameraEntityUtils.java | 16 +++ .../creature/type/ai/AttackerTreeData.java | 13 --- .../java/electrosphere/logger/Logger.java | 9 ++ .../electrosphere/logger/LoggerInterface.java | 2 + .../electrosphere/menu/debug/ImGuiAI.java | 7 ++ src/main/java/electrosphere/server/ai/AI.java | 44 ++++--- .../electrosphere/server/ai/AIManager.java | 44 ++++++- .../electrosphere/server/ai/AITreeNode.java | 18 --- .../ai/{ => blackboard}/Blackboard.java | 2 +- .../server/ai/blackboard/BlackboardKeys.java | 13 +++ .../server/ai/nodes/AITreeNode.java | 37 ++++++ .../ai/nodes/actions/BlockStartNode.java | 34 ++++++ .../ai/nodes/actions/BlockStopNode.java | 33 ++++++ .../nodes/actions/combat/AttackStartNode.java | 35 ++++++ .../actions/combat/MeleeRangeCheckNode.java | 80 +++++++++++++ .../actions/combat/MeleeTargetingNode.java | 95 +++++++++++++++ .../ai/nodes/actions/move/FaceTargetNode.java | 40 +++++++ .../ai/nodes/actions/move/WalkStartNode.java | 49 ++++++++ .../ai/nodes/actions/move/WalkStopNode.java | 33 ++++++ .../server/ai/nodes/checks/CanAttackNode.java | 25 ++++ .../server/ai/nodes/checks/IsMovingNode.java | 27 +++++ .../ai/nodes/checks/equip/HasWeaponNode.java | 29 +++++ .../server/ai/nodes/combat/BlockNode.java | 37 ------ .../ai/nodes/combat/MeleeTargetingNode.java | 60 ---------- .../server/ai/nodes/meta/PrintNode.java | 59 ++++++++++ .../server/ai/nodes/meta/StartTimerNode.java | 50 ++++++++ .../meta/collections/CollectionNode.java | 18 +++ .../meta/collections/RandomizerNode.java | 76 ++++++++++++ .../nodes/meta/collections/SelectorNode.java | 64 ++++++++++ .../nodes/meta/collections/SequenceNode.java | 64 ++++++++++ .../ai/nodes/meta/debug/DebuggerNode.java | 19 +++ .../nodes/meta/debug/PublishStatusNode.java | 32 +++++ .../nodes/meta/decorators/DecoratorNode.java | 16 +++ .../ai/nodes/meta/decorators/FailerNode.java | 38 ++++++ .../nodes/meta/decorators/InverterNode.java | 47 ++++++++ .../nodes/meta/decorators/RepeaterNode.java | 59 ++++++++++ .../ai/nodes/meta/decorators/RunnerNode.java | 38 ++++++ .../nodes/meta/decorators/SucceederNode.java | 38 ++++++ .../ai/nodes/meta/decorators/TimerNode.java | 70 +++++++++++ .../ai/nodes/meta/decorators/UntilNode.java | 50 ++++++++ .../conditional/ConditionalNode.java | 61 ++++++++++ .../decorators/conditional/OnFailureNode.java | 59 ++++++++++ .../decorators/conditional/OnSuccessNode.java | 59 ++++++++++ .../ai/nodes/movement/MeleeMovement.java | 84 -------------- .../server/ai/services/AIService.java | 13 +++ .../server/ai/services/TimerService.java | 97 ++++++++++++++++ .../ai/trees/creature/AttackerAITree.java | 12 +- .../ai/trees/creature/melee/MeleeAITree.java | 109 ++++++++++++++++++ .../server/ai/trees/test/BlockerAITree.java | 13 ++- 56 files changed, 1852 insertions(+), 240 deletions(-) delete mode 100644 src/main/java/electrosphere/server/ai/AITreeNode.java rename src/main/java/electrosphere/server/ai/{ => blackboard}/Blackboard.java (96%) create mode 100644 src/main/java/electrosphere/server/ai/blackboard/BlackboardKeys.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/AITreeNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/actions/BlockStartNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/actions/BlockStopNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/actions/combat/AttackStartNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/actions/combat/MeleeRangeCheckNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/actions/combat/MeleeTargetingNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/actions/move/FaceTargetNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/actions/move/WalkStartNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/actions/move/WalkStopNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/checks/CanAttackNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/checks/IsMovingNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/checks/equip/HasWeaponNode.java delete mode 100644 src/main/java/electrosphere/server/ai/nodes/combat/BlockNode.java delete mode 100644 src/main/java/electrosphere/server/ai/nodes/combat/MeleeTargetingNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/PrintNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/StartTimerNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/collections/CollectionNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/collections/RandomizerNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/collections/SelectorNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/collections/SequenceNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/debug/DebuggerNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/debug/PublishStatusNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/decorators/DecoratorNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/decorators/FailerNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/decorators/InverterNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/decorators/RepeaterNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/decorators/RunnerNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/decorators/SucceederNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/decorators/TimerNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/decorators/UntilNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/decorators/conditional/ConditionalNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/decorators/conditional/OnFailureNode.java create mode 100644 src/main/java/electrosphere/server/ai/nodes/meta/decorators/conditional/OnSuccessNode.java delete mode 100644 src/main/java/electrosphere/server/ai/nodes/movement/MeleeMovement.java create mode 100644 src/main/java/electrosphere/server/ai/services/AIService.java create mode 100644 src/main/java/electrosphere/server/ai/services/TimerService.java create mode 100644 src/main/java/electrosphere/server/ai/trees/creature/melee/MeleeAITree.java diff --git a/assets/Data/creatures/human.json b/assets/Data/creatures/human.json index 046527c6..77b5ec96 100644 --- a/assets/Data/creatures/human.json +++ b/assets/Data/creatures/human.json @@ -533,7 +533,9 @@ }, "aiTrees" : [ { - "name" : "Blocker" + "name" : "Attacker", + "aggroRange" : 10, + "attackRange" : 0.8 } ], "boneGroups" : [ diff --git a/docs/src/progress/currenttarget.md b/docs/src/progress/currenttarget.md index faa68b2c..e72efda2 100644 --- a/docs/src/progress/currenttarget.md +++ b/docs/src/progress/currenttarget.md @@ -5,7 +5,6 @@ + when you grab the sword, a tutorial popup appears to tell you how to use in + on clearing the tutorial, continue the game when the sword is equipped, create another popup to teach sword controls. it pauses the game + when popup is accepted, spawn an enemy with an effect - enemy ai review combat code (lifestate, damage calculation, etc) Maybe a fade-out before deleting entity on death? @@ -15,6 +14,9 @@ + fix the vibes Stability Movement penalty while swinging weapon + Only have ai strafe if its outside attack range + Ticketed randomizer node for BTs to more heavily weight attacking and waiting + Slow down strafe movement somehow + bug fixes Fix grass rendering distance diff --git a/docs/src/progress/renderertodo.md b/docs/src/progress/renderertodo.md index 84093d69..251529ec 100644 --- a/docs/src/progress/renderertodo.md +++ b/docs/src/progress/renderertodo.md @@ -566,6 +566,15 @@ Fix deleting entities on server not properly deleting the whole entity Windup and Cooldown animations on 2h block AI rearchitecture to actually do behavior trees now that I know what they are +(08/15/2024) +True behavior trees + - Decorators + - BT Timer service + - Meta nodes + - Colections + - Combat +Melee ai using BT framework + # TODO diff --git a/src/main/java/electrosphere/engine/Globals.java b/src/main/java/electrosphere/engine/Globals.java index aa6e5469..c84646ac 100644 --- a/src/main/java/electrosphere/engine/Globals.java +++ b/src/main/java/electrosphere/engine/Globals.java @@ -431,7 +431,7 @@ public class Globals { //script engine scriptEngine = new ScriptEngine(); //ai manager - aiManager = new AIManager(); + aiManager = new AIManager(0); //realm & data cell manager realmManager = new RealmManager(); entityDataCellMapper = new EntityDataCellMapper(); diff --git a/src/main/java/electrosphere/entity/state/attack/ServerAttackTree.java b/src/main/java/electrosphere/entity/state/attack/ServerAttackTree.java index f4d6352e..515d1ab6 100644 --- a/src/main/java/electrosphere/entity/state/attack/ServerAttackTree.java +++ b/src/main/java/electrosphere/entity/state/attack/ServerAttackTree.java @@ -511,6 +511,18 @@ public class ServerAttackTree implements BehaviorTree { return rVal; } + /** + * Checks if this tree can start an attack + * @return true if can attack, false otherwise + */ + public boolean canAttack(){ + String attackType = getAttackType(); + if(attackType == null){ + return false; + } + return this.canAttack(attackType); + } + /** * Gets the next attack move * @param moveset The moveset @@ -570,6 +582,15 @@ public class ServerAttackTree implements BehaviorTree { this.collidedEntities.add(target); } + /** + * Checks if the entity has a copy of this tree + * @param target The entity + * @return true if has a copy of this tree, false otherwise + */ + public static boolean hasAttackTree(Entity target){ + return target.containsKey(EntityDataStrings.TREE_SERVERATTACKTREE); + } + /** *

Automatically generated

*

diff --git a/src/main/java/electrosphere/entity/state/movement/groundmove/ServerGroundMovementTree.java b/src/main/java/electrosphere/entity/state/movement/groundmove/ServerGroundMovementTree.java index 2936a606..ef9e37d9 100644 --- a/src/main/java/electrosphere/entity/state/movement/groundmove/ServerGroundMovementTree.java +++ b/src/main/java/electrosphere/entity/state/movement/groundmove/ServerGroundMovementTree.java @@ -431,6 +431,18 @@ public class ServerGroundMovementTree implements BehaviorTree { networkMessageQueue.add(networkMessage); } + /** + * Checks if the tree is moving + * @return true if is moving, false otherwise + */ + public boolean isMoving(){ + return this.state != MovementTreeState.IDLE; + } + + /** + * Checks if the tree CAN start moving + * @return true if CAN start moving, false otherwise + */ public boolean canStartMoving(){ boolean rVal = true; return rVal; diff --git a/src/main/java/electrosphere/entity/state/physicssync/ClientPhysicsSyncTree.java b/src/main/java/electrosphere/entity/state/physicssync/ClientPhysicsSyncTree.java index 1d9d4147..e2fbaf85 100644 --- a/src/main/java/electrosphere/entity/state/physicssync/ClientPhysicsSyncTree.java +++ b/src/main/java/electrosphere/entity/state/physicssync/ClientPhysicsSyncTree.java @@ -11,6 +11,8 @@ import electrosphere.entity.Entity; import electrosphere.entity.EntityDataStrings; import electrosphere.entity.EntityUtils; import electrosphere.entity.btree.BehaviorTree; +import electrosphere.entity.types.camera.CameraEntityUtils; +import electrosphere.entity.types.creature.CreatureUtils; import electrosphere.net.parser.net.message.EntityMessage; /** @@ -40,6 +42,8 @@ public class ClientPhysicsSyncTree implements BehaviorTree { public void simulate(float deltaTime) { if(!hasPushesMessage && latestMessage != null){ this.hasPushesMessage = true; + // + //get values Vector3d position = new Vector3d(latestMessage.getpositionX(),latestMessage.getpositionY(),latestMessage.getpositionZ()); Quaterniond rotationFromServer = new Quaterniond(latestMessage.getrotationX(),latestMessage.getrotationY(),latestMessage.getrotationZ(),latestMessage.getrotationW()); Vector3d linearVelocity = new Vector3d(latestMessage.getlinVelX(), latestMessage.getlinVelY(), latestMessage.getlinVelZ()); @@ -47,9 +51,18 @@ public class ClientPhysicsSyncTree implements BehaviorTree { Vector3d linearForce = new Vector3d(latestMessage.getlinForceX(), latestMessage.getlinForceY(), latestMessage.getlinForceZ()); Vector3d angularForce = new Vector3d(); DBody body = PhysicsEntityUtils.getDBody(parent); + + // + //Synchronize data EntityUtils.getPosition(parent).set(position); EntityUtils.getRotation(parent).set(rotationFromServer); PhysicsUtils.synchronizeData(Globals.clientSceneWrapper.getCollisionEngine(), body, position, rotationFromServer, linearVelocity, angularVelocity, linearForce, angularForce); + + // + //update facing vector if relevant + if(CreatureUtils.getFacingVector(parent) != null){ + CreatureUtils.setFacingVector(parent, CameraEntityUtils.getFacingVec(rotationFromServer)); + } } } diff --git a/src/main/java/electrosphere/entity/types/camera/CameraEntityUtils.java b/src/main/java/electrosphere/entity/types/camera/CameraEntityUtils.java index c8689f92..7822bbc9 100644 --- a/src/main/java/electrosphere/entity/types/camera/CameraEntityUtils.java +++ b/src/main/java/electrosphere/entity/types/camera/CameraEntityUtils.java @@ -297,6 +297,22 @@ public class CameraEntityUtils { return new Vector3d(rotationVecRaw.x,0,rotationVecRaw.z); } + /** + * Gets the facing vector from the quaternion + * @param rotation The rotation to construct the facing vector with + * @return The facing vector + */ + public static Vector3d getFacingVec(Quaterniond rotation){ + //quaternion is multiplied by pi because we want to point away from the eye of the camera, NOT towards it + Matrix4d rotationMat = new Matrix4d().rotate(rotation); + Vector4d rotationVecRaw = MathUtils.getOriginVector4(); + rotationVecRaw = rotationMat.transform(rotationVecRaw); + if(rotationVecRaw.length() < 0.001){ + rotationVecRaw.set(MathUtils.getOriginVector4()); + } + return new Vector3d(rotationVecRaw.x,0,rotationVecRaw.z); + } + /** * Gets the rotation matrix from a yaw and pitch * @param yaw The yaw diff --git a/src/main/java/electrosphere/game/data/creature/type/ai/AttackerTreeData.java b/src/main/java/electrosphere/game/data/creature/type/ai/AttackerTreeData.java index 7bb7b9da..211585bd 100644 --- a/src/main/java/electrosphere/game/data/creature/type/ai/AttackerTreeData.java +++ b/src/main/java/electrosphere/game/data/creature/type/ai/AttackerTreeData.java @@ -15,11 +15,6 @@ public class AttackerTreeData implements AITreeData { */ float attackRange; - /** - * The number of frames to wait before changing between states - */ - int stateChangeTimeout; - /** * Gets the range at which this entity will locate enemies * @return The range @@ -36,14 +31,6 @@ public class AttackerTreeData implements AITreeData { return attackRange; } - /** - * Gets the number of frames to wait before changing between states - * @return The number of frames - */ - public int getStateChangeTimeout(){ - return stateChangeTimeout; - } - @Override public String getName() { return "Attacker"; diff --git a/src/main/java/electrosphere/logger/Logger.java b/src/main/java/electrosphere/logger/Logger.java index 3905c965..60f5f89f 100644 --- a/src/main/java/electrosphere/logger/Logger.java +++ b/src/main/java/electrosphere/logger/Logger.java @@ -112,4 +112,13 @@ public class Logger { } } + /** + * Prints a message at the specified logging level + * @param level The logging level + * @param message The message + */ + public void PRINT(LogLevel level, String message){ + + } + } diff --git a/src/main/java/electrosphere/logger/LoggerInterface.java b/src/main/java/electrosphere/logger/LoggerInterface.java index dd801b63..58fa0882 100644 --- a/src/main/java/electrosphere/logger/LoggerInterface.java +++ b/src/main/java/electrosphere/logger/LoggerInterface.java @@ -20,6 +20,7 @@ public class LoggerInterface { public static Logger loggerAudio; public static Logger loggerUI; public static Logger loggerScripts; + public static Logger loggerAI; /** * Initializes all logic objects @@ -36,6 +37,7 @@ public class LoggerInterface { loggerAudio = new Logger(LogLevel.WARNING); loggerUI = new Logger(LogLevel.WARNING); loggerScripts = new Logger(LogLevel.WARNING); + loggerAI = new Logger(LogLevel.LOOP_DEBUG); loggerStartup.INFO("Initialized loggers"); } } diff --git a/src/main/java/electrosphere/menu/debug/ImGuiAI.java b/src/main/java/electrosphere/menu/debug/ImGuiAI.java index 8d94a6ee..29999df2 100644 --- a/src/main/java/electrosphere/menu/debug/ImGuiAI.java +++ b/src/main/java/electrosphere/menu/debug/ImGuiAI.java @@ -3,6 +3,7 @@ package electrosphere.menu.debug; import electrosphere.engine.Globals; import electrosphere.renderer.ui.imgui.ImGuiWindow; import electrosphere.renderer.ui.imgui.ImGuiWindow.ImGuiWindowCallback; +import electrosphere.server.ai.AI; import imgui.ImGui; /** @@ -36,6 +37,12 @@ public class ImGuiAI { if(ImGui.button("Toggle AI Manager")){ Globals.aiManager.setActive(!Globals.aiManager.isActive()); } + + if(ImGui.collapsingHeader("Statuses")){ + for(AI ai : Globals.aiManager.getAIList()){ + ImGui.text(ai.getParent().getId() + " - " + ai.getStatus()); + } + } } }); diff --git a/src/main/java/electrosphere/server/ai/AI.java b/src/main/java/electrosphere/server/ai/AI.java index 6ec6abdf..0574a797 100644 --- a/src/main/java/electrosphere/server/ai/AI.java +++ b/src/main/java/electrosphere/server/ai/AI.java @@ -7,8 +7,12 @@ import electrosphere.entity.Entity; import electrosphere.entity.EntityDataStrings; import electrosphere.entity.types.creature.CreatureUtils; import electrosphere.game.data.creature.type.ai.AITreeData; +import electrosphere.game.data.creature.type.ai.AttackerTreeData; import electrosphere.game.data.creature.type.ai.BlockerTreeData; import electrosphere.logger.LoggerInterface; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; +import electrosphere.server.ai.trees.creature.AttackerAITree; import electrosphere.server.ai.trees.test.BlockerAITree; /** @@ -42,6 +46,11 @@ public class AI { */ List evaluatedNodes = new LinkedList(); + /** + * The status of the ai + */ + String status = "Idle"; + /** * Constructs an AI from a list of trees that should be present on the ai * @param treeData The list of data on trees to be provided @@ -56,6 +65,9 @@ public class AI { case BlockerAITree.TREE_NAME: { rVal.rootNode = BlockerAITree.create((BlockerTreeData) aiData); } break; + case AttackerAITree.TREE_NAME: { + rVal.rootNode = AttackerAITree.create((AttackerTreeData) aiData); + } break; default: { LoggerInterface.loggerEngine.ERROR(new IllegalArgumentException("Trying to construct ai tree with undefined data type! " + aiData.getName())); } break; @@ -77,21 +89,7 @@ public class AI { */ protected void simulate(){ if(this.shouldExecute()){ - AITreeNode currentNode = this.rootNode; - evaluatedNodes.clear(); - while(currentNode != null){ - // - //loop checking - if(evaluatedNodes.contains(currentNode)){ - LoggerInterface.loggerEngine.ERROR(new IllegalStateException("AI tree looped!")); - break; - } - - // - //evaluate node - evaluatedNodes.add(currentNode); - currentNode = currentNode.evaluate(this.parent, this.blackboard); - } + this.rootNode.evaluate(this.parent, this.blackboard); } } @@ -140,5 +138,21 @@ public class AI { private boolean shouldExecute(){ return !CreatureUtils.hasControllerPlayerId(this.parent); } + + /** + * Sets the status of the ai + * @param status The status + */ + public void setStatus(String status){ + this.status = status; + } + + /** + * Gets the status of the ai + * @return The status + */ + public String getStatus(){ + return this.status; + } } diff --git a/src/main/java/electrosphere/server/ai/AIManager.java b/src/main/java/electrosphere/server/ai/AIManager.java index fc28e521..3987b968 100644 --- a/src/main/java/electrosphere/server/ai/AIManager.java +++ b/src/main/java/electrosphere/server/ai/AIManager.java @@ -4,10 +4,12 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Random; import electrosphere.entity.Entity; import electrosphere.game.data.creature.type.ai.AITreeData; import electrosphere.logger.LoggerInterface; +import electrosphere.server.ai.services.TimerService; /** * Server manager for all entity AIs @@ -33,18 +35,32 @@ public class AIManager { * Controls whether the ai manager should simulate each frame or not */ boolean active = true; + + /** + * The timer service + */ + TimerService timerService = new TimerService(); + + /** + * The random of the ai + */ + Random random = null; /** * Constructor */ - public AIManager(){ - + public AIManager(long seed){ + this.random = new Random(seed); } /** * Simulates all AIs currently available */ public void simulate(){ + //exec the services + execServices(); + + //simulate each tree if(isActive()){ for(AI ai : aiList){ ai.simulate(); @@ -89,6 +105,7 @@ public class AIManager { aiList.add(ai); entityAIMap.put(entity,ai); aiEntityMap.put(ai,entity); + AI.setAI(entity, ai); } /** @@ -101,5 +118,28 @@ public class AIManager { aiEntityMap.remove(targetAI); entityAIMap.remove(entity); } + + /** + * Executes all the services + */ + private void execServices(){ + timerService.exec(); + } + + /** + * Gets the timer service + * @return The timer service + */ + public TimerService getTimerService(){ + return timerService; + } + + /** + * Gets the ai manager's random + * @return The random + */ + public Random getRandom(){ + return random; + } } diff --git a/src/main/java/electrosphere/server/ai/AITreeNode.java b/src/main/java/electrosphere/server/ai/AITreeNode.java deleted file mode 100644 index 0e3f189e..00000000 --- a/src/main/java/electrosphere/server/ai/AITreeNode.java +++ /dev/null @@ -1,18 +0,0 @@ -package electrosphere.server.ai; - -import electrosphere.entity.Entity; - -/** - * A node in a behavior tree - */ -public interface AITreeNode { - - /** - * Evaluates the node - * @param entity The entity to evaluate this node on - * @param blackboard The blackboard for the tree - * @return The next node to run, or null if no more nodes should be run - */ - public AITreeNode evaluate(Entity entity, Blackboard blackboard); - -} diff --git a/src/main/java/electrosphere/server/ai/Blackboard.java b/src/main/java/electrosphere/server/ai/blackboard/Blackboard.java similarity index 96% rename from src/main/java/electrosphere/server/ai/Blackboard.java rename to src/main/java/electrosphere/server/ai/blackboard/Blackboard.java index 178270f4..1e58f9cf 100644 --- a/src/main/java/electrosphere/server/ai/Blackboard.java +++ b/src/main/java/electrosphere/server/ai/blackboard/Blackboard.java @@ -1,4 +1,4 @@ -package electrosphere.server.ai; +package electrosphere.server.ai.blackboard; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/electrosphere/server/ai/blackboard/BlackboardKeys.java b/src/main/java/electrosphere/server/ai/blackboard/BlackboardKeys.java new file mode 100644 index 00000000..122d1e10 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/blackboard/BlackboardKeys.java @@ -0,0 +1,13 @@ +package electrosphere.server.ai.blackboard; + +/** + * Keys for the blackboard + */ +public class BlackboardKeys { + + /* + * Melee ai related + */ + public static final String MELEE_TARGET = "meleeTarget"; + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/AITreeNode.java b/src/main/java/electrosphere/server/ai/nodes/AITreeNode.java new file mode 100644 index 00000000..c9b9caea --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/AITreeNode.java @@ -0,0 +1,37 @@ +package electrosphere.server.ai.nodes; + +import electrosphere.entity.Entity; +import electrosphere.server.ai.blackboard.Blackboard; + +/** + * A node in a behavior tree + */ +public interface AITreeNode { + + /** + * Different results that a node can return + */ + public static enum AITreeNodeResult { + /** + * Successfully executed + */ + SUCCESS, + /** + * Still running + */ + RUNNING, + /** + * Failed + */ + FAILURE, + } + + /** + * Evaluates the node + * @param entity The entity to evaluate this node on + * @param blackboard The blackboard for the tree + * @return The result of evaluating the node + */ + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard); + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/actions/BlockStartNode.java b/src/main/java/electrosphere/server/ai/nodes/actions/BlockStartNode.java new file mode 100644 index 00000000..2194e8fa --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/actions/BlockStartNode.java @@ -0,0 +1,34 @@ +package electrosphere.server.ai.nodes.actions; + +import electrosphere.entity.Entity; +import electrosphere.entity.state.block.ServerBlockTree; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Attempts to block + */ +public class BlockStartNode implements AITreeNode { + + /** + * Constructor + */ + public BlockStartNode(){ + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + if(ServerBlockTree.getServerBlockTree(entity) != null){ + ServerBlockTree serverBlockTree = ServerBlockTree.getServerBlockTree(entity); + if(serverBlockTree.isIdle()){ + serverBlockTree.start(); + return AITreeNodeResult.SUCCESS; + } else { + return AITreeNodeResult.RUNNING; + } + } else { + return AITreeNodeResult.FAILURE; + } + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/actions/BlockStopNode.java b/src/main/java/electrosphere/server/ai/nodes/actions/BlockStopNode.java new file mode 100644 index 00000000..1fa2c2ea --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/actions/BlockStopNode.java @@ -0,0 +1,33 @@ +package electrosphere.server.ai.nodes.actions; + +import electrosphere.entity.Entity; +import electrosphere.entity.state.block.ServerBlockTree; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Tries to stop blocking + */ +public class BlockStopNode implements AITreeNode { + + /** + * Constructor + */ + public BlockStopNode(){ + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + if(ServerBlockTree.getServerBlockTree(entity) != null){ + ServerBlockTree serverBlockTree = ServerBlockTree.getServerBlockTree(entity); + if(!serverBlockTree.isIdle()){ + serverBlockTree.start(); + return AITreeNodeResult.SUCCESS; + } else { + return AITreeNodeResult.RUNNING; + } + } else { + return AITreeNodeResult.FAILURE; + } + } +} diff --git a/src/main/java/electrosphere/server/ai/nodes/actions/combat/AttackStartNode.java b/src/main/java/electrosphere/server/ai/nodes/actions/combat/AttackStartNode.java new file mode 100644 index 00000000..37a21583 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/actions/combat/AttackStartNode.java @@ -0,0 +1,35 @@ +package electrosphere.server.ai.nodes.actions.combat; + +import electrosphere.entity.Entity; +import electrosphere.entity.state.attack.ServerAttackTree; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Starts an attack + */ +public class AttackStartNode implements AITreeNode { + + /** + * Constructor + */ + public AttackStartNode(){ + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + if(!ServerAttackTree.hasAttackTree(entity)){ + return AITreeNodeResult.FAILURE; + } + ServerAttackTree serverAttackTree = ServerAttackTree.getServerAttackTree(entity); + if(!serverAttackTree.canAttack()){ + return AITreeNodeResult.FAILURE; + } + if(serverAttackTree.isAttacking()){ + return AITreeNodeResult.RUNNING; + } + serverAttackTree.start(); + return AITreeNodeResult.SUCCESS; + } + +} \ No newline at end of file diff --git a/src/main/java/electrosphere/server/ai/nodes/actions/combat/MeleeRangeCheckNode.java b/src/main/java/electrosphere/server/ai/nodes/actions/combat/MeleeRangeCheckNode.java new file mode 100644 index 00000000..bb3af858 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/actions/combat/MeleeRangeCheckNode.java @@ -0,0 +1,80 @@ +package electrosphere.server.ai.nodes.actions.combat; + +import org.joml.Vector3d; + +import electrosphere.entity.Entity; +import electrosphere.entity.EntityUtils; +import electrosphere.game.data.creature.type.ai.AttackerTreeData; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Checks if the target is in a range type + */ +public class MeleeRangeCheckNode implements AITreeNode { + + /** + * The type of check to run + */ + public static enum MeleeRangeCheckType { + /** + * Check if in aggro range + */ + AGGRO, + /** + * Check if in attack range + */ + ATTACK, + } + + /** + * The type of check to perform + */ + MeleeRangeCheckType checkType = null; + + /** + * The attacker tree data + */ + AttackerTreeData attackerTreeData = null; + + /** + * Constructor + * @param attackerTreeData The attacker tree data + * @param checkType The type of check to perform + */ + public MeleeRangeCheckNode(AttackerTreeData attackerTreeData, MeleeRangeCheckType checkType){ + if(attackerTreeData == null){ + throw new IllegalArgumentException("Trying to create melee range check node with null tree data!"); + } + if(checkType == null){ + throw new IllegalArgumentException("Trying to create melee range check node with null check type!"); + } + this.attackerTreeData = attackerTreeData; + this.checkType = checkType; + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + if(!MeleeTargetingNode.hasTarget(blackboard)){ + return AITreeNodeResult.FAILURE; + } + Entity target = MeleeTargetingNode.getTarget(blackboard); + Vector3d targetPos = EntityUtils.getPosition(target); + Vector3d parentPos = EntityUtils.getPosition(entity); + switch(this.checkType){ + case AGGRO: { + if(parentPos.distance(targetPos) < this.attackerTreeData.getAggroRange()){ + return AITreeNodeResult.SUCCESS; + } + } break; + case ATTACK: { + if(parentPos.distance(targetPos) < this.attackerTreeData.getAttackRange()){ + return AITreeNodeResult.SUCCESS; + } + } break; + } + return AITreeNodeResult.FAILURE; + } + + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/actions/combat/MeleeTargetingNode.java b/src/main/java/electrosphere/server/ai/nodes/actions/combat/MeleeTargetingNode.java new file mode 100644 index 00000000..ef3b415b --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/actions/combat/MeleeTargetingNode.java @@ -0,0 +1,95 @@ +package electrosphere.server.ai.nodes.actions.combat; + +import org.joml.Vector3d; + +import electrosphere.engine.Globals; +import electrosphere.entity.Entity; +import electrosphere.entity.EntityTags; +import electrosphere.entity.EntityUtils; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.blackboard.BlackboardKeys; +import electrosphere.server.ai.nodes.AITreeNode; +import electrosphere.server.datacell.Realm; +import electrosphere.server.datacell.utils.DataCellSearchUtils; + +/** + * BTree node to seek targets for melee attacks + */ +public class MeleeTargetingNode implements AITreeNode { + + /** + * The aggro range + */ + float aggroRange = 0.0f; + + /** + * Constructor + * @param aggroRange The range at which we will pick targets + * @param onSuccess The node to execute when a target is found + * @param onFailure (Optional) The next node to execute when a target is not found + */ + public MeleeTargetingNode(float aggroRange){ + this.aggroRange = aggroRange; + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard){ + if(MeleeTargetingNode.hasTarget(blackboard)){ + return AITreeNodeResult.SUCCESS; + } + + //search + Entity target = this.searchForTarget(entity, blackboard); + if(target == null){ + return AITreeNodeResult.FAILURE; + } else { + MeleeTargetingNode.setTarget(blackboard, target); + return AITreeNodeResult.SUCCESS; + } + } + + /** + * Searches for a valid target + */ + private Entity searchForTarget(Entity parent, Blackboard blackboard){ + Vector3d position = EntityUtils.getPosition(parent); + Realm realm = Globals.realmManager.getEntityRealm(parent); + for(Entity current : DataCellSearchUtils.getEntitiesWithTagAroundLocation(realm,position,EntityTags.LIFE_STATE)){ + if(current != parent){ + Vector3d potentialTargetPosition = EntityUtils.getPosition(current); + if(position.distance(potentialTargetPosition) < aggroRange){ + return current; + } + } + } + return null; + } + + /** + * checks if the blackboard contains a melee target + * @param blackboard The blackboard + * @return true if there is a target, false otherwise + */ + public static boolean hasTarget(Blackboard blackboard){ + return blackboard.has(BlackboardKeys.MELEE_TARGET); + } + + /** + * Gets the target stored in the blackboard + * @param blackboard The blackboard + * @return The target + */ + public static Entity getTarget(Blackboard blackboard){ + return (Entity)blackboard.get(BlackboardKeys.MELEE_TARGET); + } + + /** + * Sets the melee target stored in the blackboard + * @param blackboard The blackboard + * @param target The target entity + */ + public static void setTarget(Blackboard blackboard, Entity target){ + blackboard.put(BlackboardKeys.MELEE_TARGET, target); + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/actions/move/FaceTargetNode.java b/src/main/java/electrosphere/server/ai/nodes/actions/move/FaceTargetNode.java new file mode 100644 index 00000000..9d7dcb86 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/actions/move/FaceTargetNode.java @@ -0,0 +1,40 @@ +package electrosphere.server.ai.nodes.actions.move; + +import org.joml.Quaterniond; +import org.joml.Vector3d; + +import electrosphere.entity.Entity; +import electrosphere.entity.EntityUtils; +import electrosphere.entity.types.camera.CameraEntityUtils; +import electrosphere.entity.types.creature.CreatureUtils; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; +import electrosphere.server.ai.nodes.actions.combat.MeleeTargetingNode; +import electrosphere.util.math.MathUtils; + +/** + * Faces the target + */ +public class FaceTargetNode implements AITreeNode { + + /** + * Constructor + */ + public FaceTargetNode(){ + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + if(MeleeTargetingNode.hasTarget(blackboard)){ + Entity target = MeleeTargetingNode.getTarget(blackboard); + Vector3d parentPos = EntityUtils.getPosition(entity); + Vector3d targetPos = EntityUtils.getPosition(target); + Quaterniond rotation = MathUtils.calculateRotationFromPointToPoint(parentPos, targetPos); + EntityUtils.getRotation(entity).set(rotation); + CreatureUtils.setFacingVector(entity, CameraEntityUtils.getFacingVec(rotation)); + return AITreeNodeResult.SUCCESS; + } + return AITreeNodeResult.FAILURE; + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/actions/move/WalkStartNode.java b/src/main/java/electrosphere/server/ai/nodes/actions/move/WalkStartNode.java new file mode 100644 index 00000000..d57606bb --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/actions/move/WalkStartNode.java @@ -0,0 +1,49 @@ +package electrosphere.server.ai.nodes.actions.move; + +import electrosphere.entity.Entity; +import electrosphere.entity.state.movement.groundmove.ServerGroundMovementTree; +import electrosphere.entity.state.movement.groundmove.ClientGroundMovementTree.MovementRelativeFacing; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Starts walking + */ +public class WalkStartNode implements AITreeNode { + + /** + * The facing to move in + */ + MovementRelativeFacing facing = null; + + /** + * Constructor + */ + public WalkStartNode(MovementRelativeFacing facing){ + if(facing == null){ + throw new IllegalArgumentException("Trying to create walk start tree with null facing!"); + } + this.facing = facing; + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + if(ServerGroundMovementTree.getServerGroundMovementTree(entity) != null){ + ServerGroundMovementTree serverGroundMovementTree = ServerGroundMovementTree.getServerGroundMovementTree(entity); + if(serverGroundMovementTree.isMoving()){ + return AITreeNodeResult.RUNNING; + } else if(serverGroundMovementTree.canStartMoving()){ + serverGroundMovementTree.start(this.facing); + if(!serverGroundMovementTree.isMoving()){ + return AITreeNodeResult.FAILURE; + } + return AITreeNodeResult.SUCCESS; + } else { + return AITreeNodeResult.FAILURE; + } + } else { + return AITreeNodeResult.FAILURE; + } + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/actions/move/WalkStopNode.java b/src/main/java/electrosphere/server/ai/nodes/actions/move/WalkStopNode.java new file mode 100644 index 00000000..c05ec4a2 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/actions/move/WalkStopNode.java @@ -0,0 +1,33 @@ +package electrosphere.server.ai.nodes.actions.move; + +import electrosphere.entity.Entity; +import electrosphere.entity.state.movement.groundmove.ServerGroundMovementTree; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Stops walking + */ +public class WalkStopNode implements AITreeNode { + + /** + * Constructor + */ + public WalkStopNode(){ + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + if(ServerGroundMovementTree.getServerGroundMovementTree(entity) != null){ + ServerGroundMovementTree serverGroundMovementTree = ServerGroundMovementTree.getServerGroundMovementTree(entity); + if(serverGroundMovementTree.isMoving()){ + serverGroundMovementTree.slowdown(); + return AITreeNodeResult.RUNNING; + } else { + return AITreeNodeResult.SUCCESS; + } + } else { + return AITreeNodeResult.FAILURE; + } + } +} diff --git a/src/main/java/electrosphere/server/ai/nodes/checks/CanAttackNode.java b/src/main/java/electrosphere/server/ai/nodes/checks/CanAttackNode.java new file mode 100644 index 00000000..2990bb79 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/checks/CanAttackNode.java @@ -0,0 +1,25 @@ +package electrosphere.server.ai.nodes.checks; + +import electrosphere.entity.Entity; +import electrosphere.entity.state.attack.ServerAttackTree; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Checks if the entity can attack + */ +public class CanAttackNode implements AITreeNode { + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + if(!ServerAttackTree.hasAttackTree(entity)){ + return AITreeNodeResult.FAILURE; + } + ServerAttackTree attackTree = ServerAttackTree.getServerAttackTree(entity); + if(attackTree.canAttack()){ + return AITreeNodeResult.SUCCESS; + } else { + return AITreeNodeResult.FAILURE; + } + } +} diff --git a/src/main/java/electrosphere/server/ai/nodes/checks/IsMovingNode.java b/src/main/java/electrosphere/server/ai/nodes/checks/IsMovingNode.java new file mode 100644 index 00000000..676132bd --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/checks/IsMovingNode.java @@ -0,0 +1,27 @@ +package electrosphere.server.ai.nodes.checks; + +import electrosphere.entity.Entity; +import electrosphere.entity.state.movement.groundmove.ServerGroundMovementTree; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Checks if the ai is moving + */ +public class IsMovingNode implements AITreeNode { + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + if(ServerGroundMovementTree.getServerGroundMovementTree(entity) != null){ + ServerGroundMovementTree serverGroundMovementTree = ServerGroundMovementTree.getServerGroundMovementTree(entity); + if(serverGroundMovementTree.isMoving()){ + return AITreeNodeResult.SUCCESS; + } else { + return AITreeNodeResult.FAILURE; + } + } else { + return AITreeNodeResult.FAILURE; + } + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/checks/equip/HasWeaponNode.java b/src/main/java/electrosphere/server/ai/nodes/checks/equip/HasWeaponNode.java new file mode 100644 index 00000000..d65d0e37 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/checks/equip/HasWeaponNode.java @@ -0,0 +1,29 @@ +package electrosphere.server.ai.nodes.checks.equip; + +import electrosphere.entity.Entity; +import electrosphere.entity.state.equip.ServerEquipState; +import electrosphere.entity.types.item.ItemUtils; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Checks if the entity has a weapon + */ +public class HasWeaponNode implements AITreeNode { + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + if(!ServerEquipState.hasEquipState(entity)){ + return AITreeNodeResult.FAILURE; + } + ServerEquipState serverEquipState = ServerEquipState.getEquipState(entity); + for(String equippedPointId : serverEquipState.equippedPoints()){ + Entity item = serverEquipState.getEquippedItemAtPoint(equippedPointId); + if(ItemUtils.isWeapon(item)){ + return AITreeNodeResult.SUCCESS; + } + } + return AITreeNodeResult.FAILURE; + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/combat/BlockNode.java b/src/main/java/electrosphere/server/ai/nodes/combat/BlockNode.java deleted file mode 100644 index e9377a4a..00000000 --- a/src/main/java/electrosphere/server/ai/nodes/combat/BlockNode.java +++ /dev/null @@ -1,37 +0,0 @@ -package electrosphere.server.ai.nodes.combat; - -import electrosphere.entity.Entity; -import electrosphere.entity.state.block.ServerBlockTree; -import electrosphere.server.ai.AITreeNode; -import electrosphere.server.ai.Blackboard; - -/** - * Attempts to block - */ -public class BlockNode implements AITreeNode { - - /** - * The next node to execute - */ - AITreeNode next = null; - - /** - * Constructor - * @param next (Optional) The next node to execute - */ - public BlockNode(AITreeNode next){ - this.next = next; - } - - @Override - public AITreeNode evaluate(Entity entity, Blackboard blackboard) { - if(ServerBlockTree.getServerBlockTree(entity) != null){ - ServerBlockTree serverBlockTree = ServerBlockTree.getServerBlockTree(entity); - if(serverBlockTree.isIdle()){ - serverBlockTree.start(); - } - } - return next; - } - -} diff --git a/src/main/java/electrosphere/server/ai/nodes/combat/MeleeTargetingNode.java b/src/main/java/electrosphere/server/ai/nodes/combat/MeleeTargetingNode.java deleted file mode 100644 index e75eb113..00000000 --- a/src/main/java/electrosphere/server/ai/nodes/combat/MeleeTargetingNode.java +++ /dev/null @@ -1,60 +0,0 @@ -package electrosphere.server.ai.nodes.combat; - -import org.joml.Vector3d; - -import electrosphere.engine.Globals; -import electrosphere.entity.Entity; -import electrosphere.entity.EntityTags; -import electrosphere.entity.EntityUtils; -import electrosphere.game.data.creature.type.ai.AttackerTreeData; -import electrosphere.server.ai.AITreeNode; -import electrosphere.server.ai.Blackboard; -import electrosphere.server.datacell.Realm; -import electrosphere.server.datacell.utils.DataCellSearchUtils; - -/** - * BTree node to seek targets for melee attacks - */ -public class MeleeTargetingNode implements AITreeNode { - - /** - * The next node to execute - */ - AITreeNode next = null; - - /** - * The attacker tree data - */ - AttackerTreeData treeData; - - /** - * Constructor - * @param next (Optional) The next node to execute - */ - public MeleeTargetingNode(AITreeNode next){ - this.next = next; - } - - @Override - public AITreeNode evaluate(Entity entity, Blackboard blackboard) { - throw new UnsupportedOperationException("Unimplemented!"); - } - - /** - * Searches for a valid target - */ - private void searchForTarget(Entity parent){ - Vector3d position = EntityUtils.getPosition(parent); - Realm realm = Globals.realmManager.getEntityRealm(parent); - for(Entity current : DataCellSearchUtils.getEntitiesWithTagAroundLocation(realm,position,EntityTags.LIFE_STATE)){ - if(current != parent){ - Vector3d potentialTargetPosition = EntityUtils.getPosition(current); - if(position.distance(potentialTargetPosition) < treeData.getAggroRange()){ - // target = current; - break; - } - } - } - } - -} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/PrintNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/PrintNode.java new file mode 100644 index 00000000..fe34c8d0 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/PrintNode.java @@ -0,0 +1,59 @@ +package electrosphere.server.ai.nodes.meta; + +import electrosphere.entity.Entity; +import electrosphere.logger.Logger; +import electrosphere.logger.LoggerInterface; +import electrosphere.logger.Logger.LogLevel; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Logs a message + */ +public class PrintNode implements AITreeNode { + + /** + * The level to print at + */ + LogLevel level; + + /** + * The logger to print into + */ + Logger logger; + + /** + * The message to print + */ + String message; + + /** + * Constructor + * @param level The logging level to print at + * @param logger The logger to print into + * @param message The message to print + */ + public PrintNode(LogLevel level, Logger logger, String message){ + this.level = level; + this.logger = logger; + this.message = message; + } + + /** + * Constructor + * @param level The logging level to print at + * @param message The message to print + */ + public PrintNode(LogLevel level, String message){ + this.logger = LoggerInterface.loggerAI; + this.level = level; + this.message = message; + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + logger.PRINT(level, message); + return AITreeNodeResult.SUCCESS; + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/StartTimerNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/StartTimerNode.java new file mode 100644 index 00000000..a09850ef --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/StartTimerNode.java @@ -0,0 +1,50 @@ +package electrosphere.server.ai.nodes.meta; + +import electrosphere.engine.Globals; +import electrosphere.entity.Entity; +import electrosphere.logger.LoggerInterface; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; +import electrosphere.server.ai.services.TimerService; + +/** + * Waits for a provided number of frames + */ +public class StartTimerNode implements AITreeNode { + + /** + * The id of the timer in the service that this node references + */ + int timerId = -1; + + /** + * Gets the number of frames to wait + */ + int frameCount = -1; + + /** + * Constructor + * @param next (Optional) The next node to execute + */ + public StartTimerNode(int frameCount){ + if(frameCount < 1){ + LoggerInterface.loggerAI.ERROR(new IllegalArgumentException("Frame count provided to timer is <=0!")); + } + this.frameCount = frameCount; + //create the timer + Globals.aiManager.getTimerService().createTimer(this.frameCount); + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + TimerService timerService = Globals.aiManager.getTimerService(); + if(timerService.isActive(this.timerId)){ + return AITreeNodeResult.RUNNING; + } else { + timerService.resetTimer(timerId, frameCount); + return AITreeNodeResult.SUCCESS; + } + } + + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/collections/CollectionNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/collections/CollectionNode.java new file mode 100644 index 00000000..03499d54 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/collections/CollectionNode.java @@ -0,0 +1,18 @@ +package electrosphere.server.ai.nodes.meta.collections; + +import java.util.List; + +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * A node that has a collection of children + */ +public interface CollectionNode extends AITreeNode { + + /** + * Gets the list of all children that this node can branch into + * @return The list of all children this node can branch into + */ + public List getAllChildren(); + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/collections/RandomizerNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/collections/RandomizerNode.java new file mode 100644 index 00000000..dd5914a5 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/collections/RandomizerNode.java @@ -0,0 +1,76 @@ +package electrosphere.server.ai.nodes.meta.collections; + +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +import electrosphere.engine.Globals; +import electrosphere.entity.Entity; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Executes a random child until it either succeeds or fails + */ +public class RandomizerNode implements CollectionNode { + + /** + * The child nodes of the selector + */ + List children; + + + /** + * The currently executed child + */ + AITreeNode currentChild = null; + + /** + * Constructor + * @param children All the children of the randomizer + */ + public RandomizerNode(List children){ + if(children == null){ + throw new IllegalArgumentException("Trying to create randomizer node with no children!"); + } + if(children.size() < 1){ + throw new IllegalArgumentException("Trying to create randomizer node with no children!"); + } + this.children = children; + } + + /** + * Constructor + * @param children All the children of the randomizer + */ + public RandomizerNode(AITreeNode ... children){ + if(children == null){ + throw new IllegalArgumentException("Trying to create randomizer node with no children!"); + } + if(children.length < 1){ + throw new IllegalArgumentException("Trying to create randomizer node with no children!"); + } + this.children = Arrays.asList(children); + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + if(this.currentChild == null){ + Random random = Globals.aiManager.getRandom(); + int selectedChild = random.nextInt(children.size()); + this.currentChild = this.children.get(selectedChild); + } + AITreeNodeResult childResult = this.currentChild.evaluate(entity, blackboard); + if(childResult == AITreeNodeResult.RUNNING){ + return AITreeNodeResult.RUNNING; + } + this.currentChild = null; + return childResult; + } + + @Override + public List getAllChildren() { + return this.children; + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/collections/SelectorNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/collections/SelectorNode.java new file mode 100644 index 00000000..bbf0d435 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/collections/SelectorNode.java @@ -0,0 +1,64 @@ +package electrosphere.server.ai.nodes.meta.collections; + +import java.util.Arrays; +import java.util.List; + +import electrosphere.entity.Entity; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * A selector node + */ +public class SelectorNode implements CollectionNode { + + /** + * The child nodes of the selector + */ + List children; + + /** + * Constructor + * @param children All the children of the selector + */ + public SelectorNode(List children){ + if(children == null){ + throw new IllegalArgumentException("Trying to create selector node with no children!"); + } + if(children.size() < 1){ + throw new IllegalArgumentException("Trying to create selector node with no children!"); + } + this.children = children; + } + + /** + * Constructor + * @param children All the children of the selector + */ + public SelectorNode(AITreeNode ... children){ + if(children == null){ + throw new IllegalArgumentException("Trying to create selector node with no children!"); + } + if(children.length < 1){ + throw new IllegalArgumentException("Trying to create selector node with no children!"); + } + this.children = Arrays.asList(children); + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + for(AITreeNode child : children){ + AITreeNodeResult result = child.evaluate(entity, blackboard); + if(result != AITreeNodeResult.FAILURE){ + return result; + } + } + return AITreeNodeResult.FAILURE; + } + + @Override + public List getAllChildren() { + return this.children; + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/collections/SequenceNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/collections/SequenceNode.java new file mode 100644 index 00000000..fb9c2eb3 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/collections/SequenceNode.java @@ -0,0 +1,64 @@ +package electrosphere.server.ai.nodes.meta.collections; + +import java.util.Arrays; +import java.util.List; + +import electrosphere.entity.Entity; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * A sequence node + */ +public class SequenceNode implements CollectionNode { + + /** + * The child nodes of the sequence + */ + List children; + + /** + * Constructor + * @param children All the children of the sequence + */ + public SequenceNode(List children){ + if(children == null){ + throw new IllegalArgumentException("Trying to create sequence node with no children!"); + } + if(children.size() < 1){ + throw new IllegalArgumentException("Trying to create sequence node with no children!"); + } + this.children = children; + } + + /** + * Constructor + * @param children All the children of the sequence + */ + public SequenceNode(AITreeNode ... children){ + if(children == null){ + throw new IllegalArgumentException("Trying to create sequence node with no children!"); + } + if(children.length < 1){ + throw new IllegalArgumentException("Trying to create sequence node with no children!"); + } + this.children = Arrays.asList(children); + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + for(AITreeNode child : children){ + AITreeNodeResult result = child.evaluate(entity, blackboard); + if(result != AITreeNodeResult.SUCCESS){ + return result; + } + } + return AITreeNodeResult.SUCCESS; + } + + @Override + public List getAllChildren() { + return this.children; + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/debug/DebuggerNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/debug/DebuggerNode.java new file mode 100644 index 00000000..4cb7284d --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/debug/DebuggerNode.java @@ -0,0 +1,19 @@ +package electrosphere.server.ai.nodes.meta.debug; + +import electrosphere.entity.Entity; +import electrosphere.logger.LoggerInterface; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Triggers a debugger + */ +public class DebuggerNode implements AITreeNode { + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + LoggerInterface.loggerAI.DEBUG("Start debugger!"); + return AITreeNodeResult.SUCCESS; + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/debug/PublishStatusNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/debug/PublishStatusNode.java new file mode 100644 index 00000000..5ed079ac --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/debug/PublishStatusNode.java @@ -0,0 +1,32 @@ +package electrosphere.server.ai.nodes.meta.debug; + +import electrosphere.entity.Entity; +import electrosphere.server.ai.AI; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Publishes a status to the parent ai + */ +public class PublishStatusNode implements AITreeNode { + + /** + * The status + */ + String status = "Idle"; + + /** + * Constructor + * @param status The message to set the status to + */ + public PublishStatusNode(String status){ + this.status = status; + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + AI.getAI(entity).setStatus(status); + return AITreeNodeResult.SUCCESS; + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/decorators/DecoratorNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/DecoratorNode.java new file mode 100644 index 00000000..d30b1705 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/DecoratorNode.java @@ -0,0 +1,16 @@ +package electrosphere.server.ai.nodes.meta.decorators; + +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * A decorator node (ie it has a single direct child) + */ +public interface DecoratorNode extends AITreeNode { + + /** + * Returns the child node of this decorator + * @return The child node + */ + public AITreeNode getChild(); + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/decorators/FailerNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/FailerNode.java new file mode 100644 index 00000000..3929e170 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/FailerNode.java @@ -0,0 +1,38 @@ +package electrosphere.server.ai.nodes.meta.decorators; + +import electrosphere.entity.Entity; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Forces the return value from a child to be failure + */ +public class FailerNode implements DecoratorNode { + + /** + * The child node + */ + AITreeNode child; + + /** + * Constructor + * @param child (Optional) Child node to execute before returning + */ + public FailerNode(AITreeNode child){ + this.child = child; + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + if(child != null){ + child.evaluate(entity, blackboard); + } + return AITreeNodeResult.FAILURE; + } + + @Override + public AITreeNode getChild() { + return child; + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/decorators/InverterNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/InverterNode.java new file mode 100644 index 00000000..72a1955d --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/InverterNode.java @@ -0,0 +1,47 @@ +package electrosphere.server.ai.nodes.meta.decorators; + +import electrosphere.entity.Entity; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Inverts a node's result + */ +public class InverterNode implements DecoratorNode { + + /** + * The child node + */ + AITreeNode child; + + /** + * Constructor + * @param child The child node + */ + public InverterNode(AITreeNode child){ + if(child == null){ + throw new IllegalArgumentException("Trying to create inverter node with no children!"); + } + this.child = child; + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + AITreeNodeResult childResult = child.evaluate(entity, blackboard); + switch(childResult){ + case SUCCESS: + return AITreeNodeResult.FAILURE; + case FAILURE: + return AITreeNodeResult.SUCCESS; + case RUNNING: + return AITreeNodeResult.RUNNING; + } + return AITreeNodeResult.RUNNING; + } + + @Override + public AITreeNode getChild() { + return child; + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/decorators/RepeaterNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/RepeaterNode.java new file mode 100644 index 00000000..11ae1649 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/RepeaterNode.java @@ -0,0 +1,59 @@ +package electrosphere.server.ai.nodes.meta.decorators; + +import java.util.function.Supplier; + +import electrosphere.entity.Entity; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Repeats execution of a node until a condition is satisfied + */ +public class RepeaterNode implements DecoratorNode { + + /** + * The child node + */ + AITreeNode child; + + /** + * The conditional to check for repetition + */ + Supplier conditional; + + /** + * Constructor + * @param child The child node + */ + public RepeaterNode(Supplier conditional, AITreeNode child){ + if(child == null){ + throw new IllegalArgumentException("Trying to create repeater node with no children!"); + } + if(conditional == null){ + throw new IllegalArgumentException("Trying to create repeater node with no conditional for repeat!"); + } + this.conditional = conditional; + this.child = child; + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + // + //evaluate child + child.evaluate(entity, blackboard); + + // + //determine result + boolean conditionCheck = conditional.get(); + if(conditionCheck){ + return AITreeNodeResult.SUCCESS; + } + return AITreeNodeResult.RUNNING; + } + + @Override + public AITreeNode getChild() { + return child; + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/decorators/RunnerNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/RunnerNode.java new file mode 100644 index 00000000..3e78897d --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/RunnerNode.java @@ -0,0 +1,38 @@ +package electrosphere.server.ai.nodes.meta.decorators; + +import electrosphere.entity.Entity; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * A node that always returns running + */ +public class RunnerNode implements DecoratorNode { + + /** + * The child node + */ + AITreeNode child; + + /** + * Constructor + * @param child (Optional) Child node to execute before returning + */ + public RunnerNode(AITreeNode child){ + this.child = child; + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + if(child != null){ + child.evaluate(entity, blackboard); + } + return AITreeNodeResult.RUNNING; + } + + @Override + public AITreeNode getChild() { + return child; + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/decorators/SucceederNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/SucceederNode.java new file mode 100644 index 00000000..ed7318ac --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/SucceederNode.java @@ -0,0 +1,38 @@ +package electrosphere.server.ai.nodes.meta.decorators; + +import electrosphere.entity.Entity; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Forces the return value to be success + */ +public class SucceederNode implements DecoratorNode { + + /** + * The child node + */ + AITreeNode child; + + /** + * Constructor + * @param child (Optional) Child node to execute before returning + */ + public SucceederNode(AITreeNode child){ + this.child = child; + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + if(child != null){ + child.evaluate(entity, blackboard); + } + return AITreeNodeResult.SUCCESS; + } + + @Override + public AITreeNode getChild() { + return child; + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/decorators/TimerNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/TimerNode.java new file mode 100644 index 00000000..38800cbe --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/TimerNode.java @@ -0,0 +1,70 @@ +package electrosphere.server.ai.nodes.meta.decorators; + +import electrosphere.engine.Globals; +import electrosphere.entity.Entity; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * A node that waits until a timer fires before it executes the child + */ +public class TimerNode implements DecoratorNode { + + /** + * The child node + */ + AITreeNode child; + + /** + * The id for the timer for this node + */ + int timerId = -1; + + /** + * The number of frames to wait for + */ + int frameCount = -1; + + /** + * Controls whether the timer has been started or not + */ + boolean hasReset = false; + + /** + * Constructor + * @param child The child node + */ + public TimerNode(AITreeNode child, int frameCount){ + if(child == null){ + throw new IllegalArgumentException("Trying to create timer node with no children!"); + } + if(frameCount <= 0){ + throw new IllegalArgumentException("Trying to create timer node with frameCount of zero or lower!"); + } + this.child = child; + this.frameCount = frameCount; + this.timerId = Globals.aiManager.getTimerService().createTimer(); + System.out.println("Created timer node with id " + this.timerId); + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + if(Globals.aiManager.getTimerService().isActive(timerId)){ + return AITreeNodeResult.RUNNING; + } + if(hasReset){ + this.hasReset = false; + return AITreeNodeResult.SUCCESS; + } else { + Globals.aiManager.getTimerService().resetTimer(timerId, frameCount); + this.hasReset = true; + return AITreeNodeResult.RUNNING; + } + } + + @Override + public AITreeNode getChild() { + return child; + } + +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/decorators/UntilNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/UntilNode.java new file mode 100644 index 00000000..16a7e87f --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/UntilNode.java @@ -0,0 +1,50 @@ +package electrosphere.server.ai.nodes.meta.decorators; + +import electrosphere.entity.Entity; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; + +/** + * Executes until the child returns a specific result + */ +public class UntilNode implements DecoratorNode { + + /** + * The child node + */ + AITreeNode child; + + /** + * The result to check for repetition + */ + AITreeNodeResult requiredResult = null; + + /** + * Constructor + * @param child The child node + */ + public UntilNode(AITreeNodeResult requiredResult, AITreeNode child){ + if(child == null){ + throw new IllegalArgumentException("Trying to create until node with no children!"); + } + if(requiredResult == null){ + throw new IllegalArgumentException("Trying to create until node with no required result!"); + } + this.requiredResult = requiredResult; + this.child = child; + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + AITreeNodeResult result = child.evaluate(entity, blackboard); + if(result == requiredResult){ + return AITreeNodeResult.SUCCESS; + } + return AITreeNodeResult.RUNNING; + } + + @Override + public AITreeNode getChild() { + return child; + } +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/decorators/conditional/ConditionalNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/conditional/ConditionalNode.java new file mode 100644 index 00000000..90ec268c --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/conditional/ConditionalNode.java @@ -0,0 +1,61 @@ +package electrosphere.server.ai.nodes.meta.decorators.conditional; + +import java.util.function.Supplier; + +import electrosphere.entity.Entity; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; +import electrosphere.server.ai.nodes.meta.decorators.DecoratorNode; + +/** + * Executes a child node only if the provided conditional succeeds + */ +public class ConditionalNode implements DecoratorNode { + + /** + * The child node + */ + AITreeNode child; + + /** + * The conditional to check for repetition + */ + Supplier conditional; + + /** + * Constructor + * @param child The child node + */ + public ConditionalNode(Supplier conditional, AITreeNode child){ + if(child == null){ + throw new IllegalArgumentException("Trying to create conditional node with no children!"); + } + if(conditional == null){ + throw new IllegalArgumentException("Trying to create conditional node with no conditional!"); + } + this.conditional = conditional; + this.child = child; + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) { + + // + //check if should run child + boolean conditionCheck = conditional.get(); + if(conditionCheck){ + // + //evaluate child + return child.evaluate(entity, blackboard); + } + + // + //Failed to run child + return AITreeNodeResult.FAILURE; + } + + @Override + public AITreeNode getChild() { + return child; + } +} diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/decorators/conditional/OnFailureNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/conditional/OnFailureNode.java new file mode 100644 index 00000000..53712b79 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/conditional/OnFailureNode.java @@ -0,0 +1,59 @@ +package electrosphere.server.ai.nodes.meta.decorators.conditional; + +import electrosphere.entity.Entity; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; +import electrosphere.server.ai.nodes.meta.decorators.DecoratorNode; + +/** + * Runs the child if the conditional fails, otherwise returns failure + */ +public class OnFailureNode implements DecoratorNode { + + /** + * The child node + */ + AITreeNode child; + + /** + * The conditional to check for repetition + */ + AITreeNode conditional; + + /** + * Constructor + * @param child The child node + */ + public OnFailureNode(AITreeNode conditional, AITreeNode child){ + if(child == null){ + throw new IllegalArgumentException("Trying to create on failure node with no children!"); + } + if(conditional == null){ + throw new IllegalArgumentException("Trying to create on failure node with no conditional!"); + } + this.conditional = conditional; + this.child = child; + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard){ + + // + //check if should run child + AITreeNodeResult conditionalResult = conditional.evaluate(entity, blackboard); + if(conditionalResult == AITreeNodeResult.FAILURE){ + // + //evaluate child + return child.evaluate(entity, blackboard); + } + + // + //Failed to run child + return AITreeNodeResult.FAILURE; + } + + @Override + public AITreeNode getChild() { + return child; + } +} \ No newline at end of file diff --git a/src/main/java/electrosphere/server/ai/nodes/meta/decorators/conditional/OnSuccessNode.java b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/conditional/OnSuccessNode.java new file mode 100644 index 00000000..a5fe710f --- /dev/null +++ b/src/main/java/electrosphere/server/ai/nodes/meta/decorators/conditional/OnSuccessNode.java @@ -0,0 +1,59 @@ +package electrosphere.server.ai.nodes.meta.decorators.conditional; + +import electrosphere.entity.Entity; +import electrosphere.server.ai.blackboard.Blackboard; +import electrosphere.server.ai.nodes.AITreeNode; +import electrosphere.server.ai.nodes.meta.decorators.DecoratorNode; + +/** + * Runs the child if the conditional succeeds, otherwise returns failure + */ +public class OnSuccessNode implements DecoratorNode { + + /** + * The child node + */ + AITreeNode child; + + /** + * The conditional to check for repetition + */ + AITreeNode conditional; + + /** + * Constructor + * @param child The child node + */ + public OnSuccessNode(AITreeNode conditional, AITreeNode child){ + if(child == null){ + throw new IllegalArgumentException("Trying to create on success node with no children!"); + } + if(conditional == null){ + throw new IllegalArgumentException("Trying to create on success node with no conditional!"); + } + this.conditional = conditional; + this.child = child; + } + + @Override + public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard){ + + // + //check if should run child + AITreeNodeResult conditionalResult = conditional.evaluate(entity, blackboard); + if(conditionalResult == AITreeNodeResult.SUCCESS){ + // + //evaluate child + return child.evaluate(entity, blackboard); + } + + // + //Failed to run child + return AITreeNodeResult.FAILURE; + } + + @Override + public AITreeNode getChild() { + return child; + } +} diff --git a/src/main/java/electrosphere/server/ai/nodes/movement/MeleeMovement.java b/src/main/java/electrosphere/server/ai/nodes/movement/MeleeMovement.java deleted file mode 100644 index 0a46efcd..00000000 --- a/src/main/java/electrosphere/server/ai/nodes/movement/MeleeMovement.java +++ /dev/null @@ -1,84 +0,0 @@ -package electrosphere.server.ai.nodes.movement; - -import org.joml.Vector3d; - -import electrosphere.entity.Entity; -import electrosphere.entity.EntityUtils; -import electrosphere.entity.state.movement.groundmove.ClientGroundMovementTree; -import electrosphere.entity.state.movement.groundmove.ClientGroundMovementTree.MovementRelativeFacing; -import electrosphere.entity.state.movement.groundmove.ServerGroundMovementTree; -import electrosphere.entity.types.creature.CreatureUtils; -import electrosphere.game.data.creature.type.ai.AttackerTreeData; -import electrosphere.server.ai.AITreeNode; -import electrosphere.server.ai.Blackboard; - -/** - * Moves the ai into optimial positioning for melee combat - */ -public class MeleeMovement implements AITreeNode { - - /** - * The next node to execute - */ - AITreeNode next = null; - - /** - * The attacker tree data - */ - AttackerTreeData treeData; - - /** - * Constructor - * @param next (Optional) The next node to execute - */ - public MeleeMovement(AITreeNode next){ - this.next = next; - } - - @Override - public AITreeNode evaluate(Entity entity, Blackboard blackboard) { - throw new UnsupportedOperationException("Unimplemented!"); - } - - /** - * Checks if the target is within the aggression range - */ - private boolean inAggroRange(Entity parent, Entity target){ - boolean rVal = false; - Vector3d position = EntityUtils.getPosition(parent); - Vector3d targetPosition = EntityUtils.getPosition(target); - if(new Vector3d(position).distance(targetPosition) < treeData.getAggroRange()){ - rVal = true; - } - return rVal; - } - - /** - * Checks if the target is within range of an attack animation - * @return - */ - private boolean inAttackRange(Entity parent, Entity target){ - boolean rVal = false; - Vector3d position = EntityUtils.getPosition(parent); - Vector3d targetPosition = EntityUtils.getPosition(target); - if(new Vector3d(position).distance(targetPosition) < treeData.getAttackRange()){ - rVal = true; - } - return rVal; - } - - /** - * Attempts to move towards the target - */ - private void moveToTarget(Entity parent, Entity target){ - Vector3d targetPosition = EntityUtils.getPosition(target); - Vector3d characterPosition = EntityUtils.getPosition(parent); - Vector3d moveVector = new Vector3d(targetPosition).sub(characterPosition).normalize(); - CreatureUtils.setFacingVector(parent, new Vector3d((float)moveVector.x,(float)moveVector.y,(float)moveVector.z)); - ServerGroundMovementTree characterMoveTree = (ServerGroundMovementTree)CreatureUtils.serverGetEntityMovementTree(parent); - if(characterMoveTree.getState()==ClientGroundMovementTree.MovementTreeState.IDLE || characterMoveTree.getState()==ClientGroundMovementTree.MovementTreeState.SLOWDOWN){ - characterMoveTree.start(MovementRelativeFacing.FORWARD); - } - } - -} diff --git a/src/main/java/electrosphere/server/ai/services/AIService.java b/src/main/java/electrosphere/server/ai/services/AIService.java new file mode 100644 index 00000000..3e6f3ff4 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/services/AIService.java @@ -0,0 +1,13 @@ +package electrosphere.server.ai.services; + +/** + * A service + */ +public interface AIService { + + /** + * Executes the service + */ + public void exec(); + +} diff --git a/src/main/java/electrosphere/server/ai/services/TimerService.java b/src/main/java/electrosphere/server/ai/services/TimerService.java new file mode 100644 index 00000000..82cd2a54 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/services/TimerService.java @@ -0,0 +1,97 @@ +package electrosphere.server.ai.services; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Semaphore; + +import electrosphere.logger.LoggerInterface; + +public class TimerService implements AIService { + + /** + * The map of timer id -> number of frames left on the timer + */ + Map timerMap = new HashMap(); + + /** + * The iterator for timer ids + */ + int idIterator = 0; + + /** + * Lock for ids so we don't overlap + */ + Semaphore idLock = new Semaphore(1); + + @Override + public void exec() { + for(Integer timerId : timerMap.keySet()){ + int currentTime = timerMap.get(timerId); + int newTime = currentTime - 1; + if(newTime < 0){ + newTime = 0; + } + timerMap.put(timerId,newTime); + } + } + + /** + * Creates a timer + * @return The id of the timer + */ + public int createTimer(){ + int id = 0; + idLock.acquireUninterruptibly(); + id = idIterator++; + timerMap.put(id,0); + idLock.release(); + return id; + } + + /** + * Creates a timer + * @param frameCount The number of frames left in the timer + * @return The id of the timer + */ + public int createTimer(int frameCount){ + int id = 0; + idLock.acquireUninterruptibly(); + id = idIterator++; + timerMap.put(id,frameCount); + idLock.release(); + return id; + } + + /** + * Deletes a timer + * @param timerId The id of the timer + */ + public void deleteTimer(int timerId){ + timerMap.remove(timerId); + } + + /** + * Checks if the timer is still active + * @param timerId The id of the timer + * @return true if still active, false if has completed or has been deleted + */ + public boolean isActive(int timerId){ + if(!timerMap.containsKey(timerId)){ + return false; + } + return timerMap.get(timerId) > 0; + } + + /** + * Resets a timer to a given frame count + * @param timerId The id of the timer + * @param frameCount The number of frames remaining for the timer + */ + public void resetTimer(int timerId, int frameCount){ + if(!timerMap.containsKey(timerId)){ + LoggerInterface.loggerAI.ERROR(new IllegalArgumentException("Trying to reset a timer that does not exist!")); + } + timerMap.put(timerId,frameCount); + } + +} diff --git a/src/main/java/electrosphere/server/ai/trees/creature/AttackerAITree.java b/src/main/java/electrosphere/server/ai/trees/creature/AttackerAITree.java index 106c46c1..0b1a6c3f 100644 --- a/src/main/java/electrosphere/server/ai/trees/creature/AttackerAITree.java +++ b/src/main/java/electrosphere/server/ai/trees/creature/AttackerAITree.java @@ -1,7 +1,9 @@ package electrosphere.server.ai.trees.creature; -import electrosphere.server.ai.AITreeNode; -import electrosphere.server.ai.nodes.combat.BlockNode; +import electrosphere.game.data.creature.type.ai.AttackerTreeData; +import electrosphere.server.ai.nodes.AITreeNode; +import electrosphere.server.ai.nodes.meta.collections.SequenceNode; +import electrosphere.server.ai.trees.creature.melee.MeleeAITree; /** * Figures out how if it should attack and how it can, then executes that subtree @@ -17,8 +19,10 @@ public class AttackerAITree { * Creates an attacker ai tree * @return The root node of the tree */ - public static AITreeNode create(){ - return new BlockNode(null); + public static AITreeNode create(AttackerTreeData attackerTreeData){ + return new SequenceNode( + MeleeAITree.create(attackerTreeData) + ); } } diff --git a/src/main/java/electrosphere/server/ai/trees/creature/melee/MeleeAITree.java b/src/main/java/electrosphere/server/ai/trees/creature/melee/MeleeAITree.java new file mode 100644 index 00000000..67ec6e26 --- /dev/null +++ b/src/main/java/electrosphere/server/ai/trees/creature/melee/MeleeAITree.java @@ -0,0 +1,109 @@ +package electrosphere.server.ai.trees.creature.melee; + +import electrosphere.entity.state.movement.groundmove.ClientGroundMovementTree.MovementRelativeFacing; +import electrosphere.game.data.creature.type.ai.AttackerTreeData; +import electrosphere.server.ai.nodes.AITreeNode; +import electrosphere.server.ai.nodes.AITreeNode.AITreeNodeResult; +import electrosphere.server.ai.nodes.actions.combat.AttackStartNode; +import electrosphere.server.ai.nodes.actions.combat.MeleeRangeCheckNode; +import electrosphere.server.ai.nodes.actions.combat.MeleeTargetingNode; +import electrosphere.server.ai.nodes.actions.combat.MeleeRangeCheckNode.MeleeRangeCheckType; +import electrosphere.server.ai.nodes.actions.move.FaceTargetNode; +import electrosphere.server.ai.nodes.actions.move.WalkStartNode; +import electrosphere.server.ai.nodes.actions.move.WalkStopNode; +import electrosphere.server.ai.nodes.checks.IsMovingNode; +import electrosphere.server.ai.nodes.checks.equip.HasWeaponNode; +import electrosphere.server.ai.nodes.meta.collections.RandomizerNode; +import electrosphere.server.ai.nodes.meta.collections.SelectorNode; +import electrosphere.server.ai.nodes.meta.collections.SequenceNode; +import electrosphere.server.ai.nodes.meta.debug.PublishStatusNode; +import electrosphere.server.ai.nodes.meta.decorators.FailerNode; +import electrosphere.server.ai.nodes.meta.decorators.InverterNode; +import electrosphere.server.ai.nodes.meta.decorators.SucceederNode; +import electrosphere.server.ai.nodes.meta.decorators.TimerNode; +import electrosphere.server.ai.nodes.meta.decorators.UntilNode; +import electrosphere.server.ai.nodes.meta.decorators.conditional.OnFailureNode; + +/** + * Creates a melee ai tree + */ +public class MeleeAITree { + + /** + * Name of the tree + */ + public static final String TREE_NAME = "Melee"; + + /** + * Creates a melee tree + * @return The root node of the tree + */ + public static AITreeNode create(AttackerTreeData attackerTreeData){ + + return new SequenceNode( + //preconditions here + new HasWeaponNode(), + new MeleeTargetingNode(attackerTreeData.getAggroRange()), + //determine strategy + new RandomizerNode( + //wait + new SequenceNode( + new PublishStatusNode("Waiting"), + new FaceTargetNode(), + new TimerNode(new SucceederNode(null), 1200) + ), + + //move to hover distance + new SequenceNode( + new PublishStatusNode("Strafing right"), + new InverterNode(new OnFailureNode( + new WalkStartNode(MovementRelativeFacing.RIGHT), + new FailerNode(null) + )), + new FaceTargetNode(), + new TimerNode(new SucceederNode(null), 600), + new SucceederNode(new WalkStopNode()) + ), + + //move from hover distance to melee range + new SequenceNode( + new PublishStatusNode("Strafing left"), + new InverterNode(new OnFailureNode( + new WalkStartNode(MovementRelativeFacing.LEFT), + new FailerNode(null) + )), + new FaceTargetNode(), + new TimerNode(new SucceederNode(null), 600), + new SucceederNode(new WalkStopNode()) + ), + + //move towards target and attack + new SequenceNode( + new PublishStatusNode("Attack target"), + //move towards target if its outside of melee range + new UntilNode(AITreeNodeResult.SUCCESS, + //or + new SelectorNode( + //in range + new MeleeRangeCheckNode(attackerTreeData,MeleeRangeCheckType.ATTACK), + //approaching target + new SequenceNode( + new PublishStatusNode("Approaching target"), + new FaceTargetNode(), + new OnFailureNode(new IsMovingNode(), new WalkStartNode(MovementRelativeFacing.FORWARD)), + new MeleeRangeCheckNode(attackerTreeData,MeleeRangeCheckType.ATTACK) + ) + ) + ), + //stop walking now that we're in range + new PublishStatusNode("Slowing down"), + new WalkStopNode(), + new UntilNode(AITreeNodeResult.FAILURE, new IsMovingNode()), + new PublishStatusNode("Attacking"), + new AttackStartNode() + ) + ) + ); + } + +} diff --git a/src/main/java/electrosphere/server/ai/trees/test/BlockerAITree.java b/src/main/java/electrosphere/server/ai/trees/test/BlockerAITree.java index d80ec215..6595376f 100644 --- a/src/main/java/electrosphere/server/ai/trees/test/BlockerAITree.java +++ b/src/main/java/electrosphere/server/ai/trees/test/BlockerAITree.java @@ -1,8 +1,11 @@ package electrosphere.server.ai.trees.test; import electrosphere.game.data.creature.type.ai.BlockerTreeData; -import electrosphere.server.ai.AITreeNode; -import electrosphere.server.ai.nodes.combat.BlockNode; +import electrosphere.server.ai.nodes.AITreeNode; +import electrosphere.server.ai.nodes.actions.BlockStartNode; +import electrosphere.server.ai.nodes.actions.combat.MeleeTargetingNode; +import electrosphere.server.ai.nodes.actions.move.FaceTargetNode; +import electrosphere.server.ai.nodes.meta.collections.SequenceNode; /** * Creates a blocker test tree @@ -19,7 +22,11 @@ public class BlockerAITree { * @return The root node of the tree */ public static AITreeNode create(BlockerTreeData data){ - return new BlockNode(null); + return new SequenceNode( + new BlockStartNode(), + new MeleeTargetingNode(5.0f), + new FaceTargetNode() + ); } }