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