true behavior trees
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good

This commit is contained in:
austin 2024-08-15 09:52:32 -04:00
parent bdec668fb8
commit 61c5599265
56 changed files with 1852 additions and 240 deletions

View File

@ -533,7 +533,9 @@
}, },
"aiTrees" : [ "aiTrees" : [
{ {
"name" : "Blocker" "name" : "Attacker",
"aggroRange" : 10,
"attackRange" : 0.8
} }
], ],
"boneGroups" : [ "boneGroups" : [

View File

@ -5,7 +5,6 @@
+ when you grab the sword, a tutorial popup appears to tell you how to use in + 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 + 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 + when popup is accepted, spawn an enemy with an effect
enemy ai
review combat code (lifestate, damage calculation, etc) review combat code (lifestate, damage calculation, etc)
Maybe a fade-out before deleting entity on death? Maybe a fade-out before deleting entity on death?
@ -15,6 +14,9 @@
+ fix the vibes + fix the vibes
Stability Stability
Movement penalty while swinging weapon 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 + bug fixes
Fix grass rendering distance Fix grass rendering distance

View File

@ -566,6 +566,15 @@ Fix deleting entities on server not properly deleting the whole entity
Windup and Cooldown animations on 2h block Windup and Cooldown animations on 2h block
AI rearchitecture to actually do behavior trees now that I know what they are 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 # TODO

View File

@ -431,7 +431,7 @@ public class Globals {
//script engine //script engine
scriptEngine = new ScriptEngine(); scriptEngine = new ScriptEngine();
//ai manager //ai manager
aiManager = new AIManager(); aiManager = new AIManager(0);
//realm & data cell manager //realm & data cell manager
realmManager = new RealmManager(); realmManager = new RealmManager();
entityDataCellMapper = new EntityDataCellMapper(); entityDataCellMapper = new EntityDataCellMapper();

View File

@ -511,6 +511,18 @@ public class ServerAttackTree implements BehaviorTree {
return rVal; 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 * Gets the next attack move
* @param moveset The moveset * @param moveset The moveset
@ -570,6 +582,15 @@ public class ServerAttackTree implements BehaviorTree {
this.collidedEntities.add(target); 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);
}
/** /**
* <p> Automatically generated </p> * <p> Automatically generated </p>
* <p> * <p>

View File

@ -431,6 +431,18 @@ public class ServerGroundMovementTree implements BehaviorTree {
networkMessageQueue.add(networkMessage); 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(){ public boolean canStartMoving(){
boolean rVal = true; boolean rVal = true;
return rVal; return rVal;

View File

@ -11,6 +11,8 @@ import electrosphere.entity.Entity;
import electrosphere.entity.EntityDataStrings; import electrosphere.entity.EntityDataStrings;
import electrosphere.entity.EntityUtils; import electrosphere.entity.EntityUtils;
import electrosphere.entity.btree.BehaviorTree; import electrosphere.entity.btree.BehaviorTree;
import electrosphere.entity.types.camera.CameraEntityUtils;
import electrosphere.entity.types.creature.CreatureUtils;
import electrosphere.net.parser.net.message.EntityMessage; import electrosphere.net.parser.net.message.EntityMessage;
/** /**
@ -40,6 +42,8 @@ public class ClientPhysicsSyncTree implements BehaviorTree {
public void simulate(float deltaTime) { public void simulate(float deltaTime) {
if(!hasPushesMessage && latestMessage != null){ if(!hasPushesMessage && latestMessage != null){
this.hasPushesMessage = true; this.hasPushesMessage = true;
//
//get values
Vector3d position = new Vector3d(latestMessage.getpositionX(),latestMessage.getpositionY(),latestMessage.getpositionZ()); Vector3d position = new Vector3d(latestMessage.getpositionX(),latestMessage.getpositionY(),latestMessage.getpositionZ());
Quaterniond rotationFromServer = new Quaterniond(latestMessage.getrotationX(),latestMessage.getrotationY(),latestMessage.getrotationZ(),latestMessage.getrotationW()); Quaterniond rotationFromServer = new Quaterniond(latestMessage.getrotationX(),latestMessage.getrotationY(),latestMessage.getrotationZ(),latestMessage.getrotationW());
Vector3d linearVelocity = new Vector3d(latestMessage.getlinVelX(), latestMessage.getlinVelY(), latestMessage.getlinVelZ()); 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 linearForce = new Vector3d(latestMessage.getlinForceX(), latestMessage.getlinForceY(), latestMessage.getlinForceZ());
Vector3d angularForce = new Vector3d(); Vector3d angularForce = new Vector3d();
DBody body = PhysicsEntityUtils.getDBody(parent); DBody body = PhysicsEntityUtils.getDBody(parent);
//
//Synchronize data
EntityUtils.getPosition(parent).set(position); EntityUtils.getPosition(parent).set(position);
EntityUtils.getRotation(parent).set(rotationFromServer); EntityUtils.getRotation(parent).set(rotationFromServer);
PhysicsUtils.synchronizeData(Globals.clientSceneWrapper.getCollisionEngine(), body, position, rotationFromServer, linearVelocity, angularVelocity, linearForce, angularForce); 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));
}
} }
} }

View File

@ -297,6 +297,22 @@ public class CameraEntityUtils {
return new Vector3d(rotationVecRaw.x,0,rotationVecRaw.z); 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 * Gets the rotation matrix from a yaw and pitch
* @param yaw The yaw * @param yaw The yaw

View File

@ -15,11 +15,6 @@ public class AttackerTreeData implements AITreeData {
*/ */
float attackRange; float attackRange;
/**
* The number of frames to wait before changing between states
*/
int stateChangeTimeout;
/** /**
* Gets the range at which this entity will locate enemies * Gets the range at which this entity will locate enemies
* @return The range * @return The range
@ -36,14 +31,6 @@ public class AttackerTreeData implements AITreeData {
return attackRange; return attackRange;
} }
/**
* Gets the number of frames to wait before changing between states
* @return The number of frames
*/
public int getStateChangeTimeout(){
return stateChangeTimeout;
}
@Override @Override
public String getName() { public String getName() {
return "Attacker"; return "Attacker";

View File

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

View File

@ -20,6 +20,7 @@ public class LoggerInterface {
public static Logger loggerAudio; public static Logger loggerAudio;
public static Logger loggerUI; public static Logger loggerUI;
public static Logger loggerScripts; public static Logger loggerScripts;
public static Logger loggerAI;
/** /**
* Initializes all logic objects * Initializes all logic objects
@ -36,6 +37,7 @@ public class LoggerInterface {
loggerAudio = new Logger(LogLevel.WARNING); loggerAudio = new Logger(LogLevel.WARNING);
loggerUI = new Logger(LogLevel.WARNING); loggerUI = new Logger(LogLevel.WARNING);
loggerScripts = new Logger(LogLevel.WARNING); loggerScripts = new Logger(LogLevel.WARNING);
loggerAI = new Logger(LogLevel.LOOP_DEBUG);
loggerStartup.INFO("Initialized loggers"); loggerStartup.INFO("Initialized loggers");
} }
} }

View File

@ -3,6 +3,7 @@ package electrosphere.menu.debug;
import electrosphere.engine.Globals; import electrosphere.engine.Globals;
import electrosphere.renderer.ui.imgui.ImGuiWindow; import electrosphere.renderer.ui.imgui.ImGuiWindow;
import electrosphere.renderer.ui.imgui.ImGuiWindow.ImGuiWindowCallback; import electrosphere.renderer.ui.imgui.ImGuiWindow.ImGuiWindowCallback;
import electrosphere.server.ai.AI;
import imgui.ImGui; import imgui.ImGui;
/** /**
@ -36,6 +37,12 @@ public class ImGuiAI {
if(ImGui.button("Toggle AI Manager")){ if(ImGui.button("Toggle AI Manager")){
Globals.aiManager.setActive(!Globals.aiManager.isActive()); Globals.aiManager.setActive(!Globals.aiManager.isActive());
} }
if(ImGui.collapsingHeader("Statuses")){
for(AI ai : Globals.aiManager.getAIList()){
ImGui.text(ai.getParent().getId() + " - " + ai.getStatus());
}
}
} }
}); });

View File

@ -7,8 +7,12 @@ import electrosphere.entity.Entity;
import electrosphere.entity.EntityDataStrings; import electrosphere.entity.EntityDataStrings;
import electrosphere.entity.types.creature.CreatureUtils; import electrosphere.entity.types.creature.CreatureUtils;
import electrosphere.game.data.creature.type.ai.AITreeData; 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.game.data.creature.type.ai.BlockerTreeData;
import electrosphere.logger.LoggerInterface; 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; import electrosphere.server.ai.trees.test.BlockerAITree;
/** /**
@ -42,6 +46,11 @@ public class AI {
*/ */
List<AITreeNode> evaluatedNodes = new LinkedList<AITreeNode>(); List<AITreeNode> evaluatedNodes = new LinkedList<AITreeNode>();
/**
* The status of the ai
*/
String status = "Idle";
/** /**
* Constructs an AI from a list of trees that should be present on the ai * 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 * @param treeData The list of data on trees to be provided
@ -56,6 +65,9 @@ public class AI {
case BlockerAITree.TREE_NAME: { case BlockerAITree.TREE_NAME: {
rVal.rootNode = BlockerAITree.create((BlockerTreeData) aiData); rVal.rootNode = BlockerAITree.create((BlockerTreeData) aiData);
} break; } break;
case AttackerAITree.TREE_NAME: {
rVal.rootNode = AttackerAITree.create((AttackerTreeData) aiData);
} break;
default: { default: {
LoggerInterface.loggerEngine.ERROR(new IllegalArgumentException("Trying to construct ai tree with undefined data type! " + aiData.getName())); LoggerInterface.loggerEngine.ERROR(new IllegalArgumentException("Trying to construct ai tree with undefined data type! " + aiData.getName()));
} break; } break;
@ -77,21 +89,7 @@ public class AI {
*/ */
protected void simulate(){ protected void simulate(){
if(this.shouldExecute()){ if(this.shouldExecute()){
AITreeNode currentNode = this.rootNode; this.rootNode.evaluate(this.parent, this.blackboard);
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);
}
} }
} }
@ -140,5 +138,21 @@ public class AI {
private boolean shouldExecute(){ private boolean shouldExecute(){
return !CreatureUtils.hasControllerPlayerId(this.parent); 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;
}
} }

View File

@ -4,10 +4,12 @@ import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Random;
import electrosphere.entity.Entity; import electrosphere.entity.Entity;
import electrosphere.game.data.creature.type.ai.AITreeData; import electrosphere.game.data.creature.type.ai.AITreeData;
import electrosphere.logger.LoggerInterface; import electrosphere.logger.LoggerInterface;
import electrosphere.server.ai.services.TimerService;
/** /**
* Server manager for all entity AIs * Server manager for all entity AIs
@ -33,18 +35,32 @@ public class AIManager {
* Controls whether the ai manager should simulate each frame or not * Controls whether the ai manager should simulate each frame or not
*/ */
boolean active = true; boolean active = true;
/**
* The timer service
*/
TimerService timerService = new TimerService();
/**
* The random of the ai
*/
Random random = null;
/** /**
* Constructor * Constructor
*/ */
public AIManager(){ public AIManager(long seed){
this.random = new Random(seed);
} }
/** /**
* Simulates all AIs currently available * Simulates all AIs currently available
*/ */
public void simulate(){ public void simulate(){
//exec the services
execServices();
//simulate each tree
if(isActive()){ if(isActive()){
for(AI ai : aiList){ for(AI ai : aiList){
ai.simulate(); ai.simulate();
@ -89,6 +105,7 @@ public class AIManager {
aiList.add(ai); aiList.add(ai);
entityAIMap.put(entity,ai); entityAIMap.put(entity,ai);
aiEntityMap.put(ai,entity); aiEntityMap.put(ai,entity);
AI.setAI(entity, ai);
} }
/** /**
@ -101,5 +118,28 @@ public class AIManager {
aiEntityMap.remove(targetAI); aiEntityMap.remove(targetAI);
entityAIMap.remove(entity); 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;
}
} }

View File

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

View File

@ -1,4 +1,4 @@
package electrosphere.server.ai; package electrosphere.server.ai.blackboard;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<AITreeNode> children;
/**
* The currently executed child
*/
AITreeNode currentChild = null;
/**
* Constructor
* @param children All the children of the randomizer
*/
public RandomizerNode(List<AITreeNode> 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<AITreeNode> getAllChildren() {
return this.children;
}
}

View File

@ -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<AITreeNode> children;
/**
* Constructor
* @param children All the children of the selector
*/
public SelectorNode(List<AITreeNode> 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<AITreeNode> getAllChildren() {
return this.children;
}
}

View File

@ -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<AITreeNode> children;
/**
* Constructor
* @param children All the children of the sequence
*/
public SequenceNode(List<AITreeNode> 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<AITreeNode> getAllChildren() {
return this.children;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Boolean> conditional;
/**
* Constructor
* @param child The child node
*/
public RepeaterNode(Supplier<Boolean> 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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Boolean> conditional;
/**
* Constructor
* @param child The child node
*/
public ConditionalNode(Supplier<Boolean> 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;
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,13 @@
package electrosphere.server.ai.services;
/**
* A service
*/
public interface AIService {
/**
* Executes the service
*/
public void exec();
}

View File

@ -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<Integer,Integer> timerMap = new HashMap<Integer,Integer>();
/**
* 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);
}
}

View File

@ -1,7 +1,9 @@
package electrosphere.server.ai.trees.creature; package electrosphere.server.ai.trees.creature;
import electrosphere.server.ai.AITreeNode; import electrosphere.game.data.creature.type.ai.AttackerTreeData;
import electrosphere.server.ai.nodes.combat.BlockNode; 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 * 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 * Creates an attacker ai tree
* @return The root node of the tree * @return The root node of the tree
*/ */
public static AITreeNode create(){ public static AITreeNode create(AttackerTreeData attackerTreeData){
return new BlockNode(null); return new SequenceNode(
MeleeAITree.create(attackerTreeData)
);
} }
} }

View File

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

View File

@ -1,8 +1,11 @@
package electrosphere.server.ai.trees.test; package electrosphere.server.ai.trees.test;
import electrosphere.game.data.creature.type.ai.BlockerTreeData; import electrosphere.game.data.creature.type.ai.BlockerTreeData;
import electrosphere.server.ai.AITreeNode; import electrosphere.server.ai.nodes.AITreeNode;
import electrosphere.server.ai.nodes.combat.BlockNode; 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 * Creates a blocker test tree
@ -19,7 +22,11 @@ public class BlockerAITree {
* @return The root node of the tree * @return The root node of the tree
*/ */
public static AITreeNode create(BlockerTreeData data){ public static AITreeNode create(BlockerTreeData data){
return new BlockNode(null); return new SequenceNode(
new BlockStartNode(),
new MeleeTargetingNode(5.0f),
new FaceTargetNode()
);
} }
} }