diff --git a/dsl/src/runtime/GameEnvironment.java b/dsl/src/runtime/GameEnvironment.java index fe25435e67..f1b8947354 100644 --- a/dsl/src/runtime/GameEnvironment.java +++ b/dsl/src/runtime/GameEnvironment.java @@ -5,6 +5,7 @@ import ecs.components.AnimationComponent; import ecs.components.PositionComponent; import ecs.components.VelocityComponent; +import ecs.components.ai.AIComponent; import ecs.entities.Entity; import java.util.ArrayList; import java.util.HashMap; @@ -122,11 +123,14 @@ private static ArrayList buildBuiltInTypes() { typeBuilder.createTypeFromClass(Scope.NULL, AnimationComponent.class); var velocityComponentType = typeBuilder.createTypeFromClass(Scope.NULL, VelocityComponent.class); + var aiComponentType = typeBuilder.createTypeFromClass(Scope.NULL, AIComponent.class); types.add(questConfigType); types.add(entityComponentType); types.add(positionComponentType); types.add(animationComponentType); types.add(velocityComponentType); + types.add(aiComponentType); + return types; } diff --git a/game/src/ecs/components/ai/AIComponent.java b/game/src/ecs/components/ai/AIComponent.java new file mode 100644 index 0000000000..9e955ba170 --- /dev/null +++ b/game/src/ecs/components/ai/AIComponent.java @@ -0,0 +1,55 @@ +package ecs.components.ai; + +import ecs.components.Component; +import ecs.components.ai.fight.IFightAI; +import ecs.components.ai.idle.IIdleAI; +import ecs.components.ai.idle.RadiusWalk; +import ecs.components.ai.transition.ITransition; +import ecs.components.ai.transition.RangeTransition; +import ecs.entities.Entity; +import semanticAnalysis.types.DSLContextMember; +import semanticAnalysis.types.DSLType; + +/** AIComponent is a component that stores the idle and combat behavior of AI controlled entities */ +@DSLType(name = "ai_component") +public class AIComponent extends Component { + + public static String name = "AIComponent"; + private /*@DSLTypeMember*/ IFightAI fightAI; + private /*@DSLTypeMember*/ IIdleAI idleAI; + private /*@DSLTypeMember*/ ITransition transition; + + /** + * @param entity associated entity + * @param fightAI combat behavior + * @param idleAI idle behavior + * @param transition Determines when to fight + */ + public AIComponent(Entity entity, IFightAI fightAI, IIdleAI idleAI, ITransition transition) { + super(entity, name); + this.fightAI = fightAI; + this.idleAI = idleAI; + this.transition = transition; + } + + /** + * @param entity associated entity + */ + public AIComponent(@DSLContextMember(name = "entity") Entity entity) { + super(entity, name); + System.out.println("DEBUG AI"); + idleAI = new RadiusWalk(5); + transition = new RangeTransition(1.5f); + fightAI = + entity1 -> { + System.out.println("TIME TO FIGHT!"); + // todo replace with melee skill + }; + } + + /** Excecute the ai behavior */ + public void execute() { + if (transition.isInFightMode(entity)) fightAI.fight(entity); + else idleAI.idle(entity); + } +} diff --git a/game/src/ecs/components/ai/AITools.java b/game/src/ecs/components/ai/AITools.java new file mode 100644 index 0000000000..f4135bf09c --- /dev/null +++ b/game/src/ecs/components/ai/AITools.java @@ -0,0 +1,133 @@ +package ecs.components.ai; + +import com.badlogic.gdx.ai.pfa.GraphPath; +import ecs.components.PositionComponent; +import ecs.components.VelocityComponent; +import ecs.entities.Entity; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import level.elements.ILevel; +import level.elements.tile.Tile; +import level.tools.Coordinate; +import mydungeon.ECS; +import tools.Point; + +public class AITools { + private static final Random random = new Random(); + + /** + * Finds the path to a random (accessible) tile in the given radius, starting from the position + * of the given entity. + * + * @param entity Entity whose position is the center point + * @param radius Search radius + * @return Path from the position of the entity to the randomly selected tile + */ + public static GraphPath calculateNewPath(Entity entity, float radius) { + PositionComponent pc = (PositionComponent) entity.getComponent(PositionComponent.name); + VelocityComponent vc = (VelocityComponent) entity.getComponent(VelocityComponent.name); + if (pc != null && vc != null) { + ILevel level = ECS.currentLevel; + Point position = pc.getPosition(); + List tiles = new ArrayList<>(); + for (float x = position.x - radius; x <= position.x + radius; x++) { + for (float y = position.y - radius; y <= position.y + radius; y++) { + tiles.add(level.getTileAt(new Point(x, y).toCoordinate())); + } + } + tiles.removeIf(Objects::isNull); + tiles.removeIf(tile -> !tile.isAccessible()); + Coordinate newPosition = tiles.get(random.nextInt(tiles.size())).getCoordinate(); + return level.findPath( + level.getTileAt(position.toCoordinate()), level.getTileAt(newPosition)); + } + return null; + } + + /** + * Finds the path from the position of one entity to the position of another entity. + * + * @param from Entity whose position is the start point + * @param to Entity whose position is the goal point + * @return Path + */ + public static GraphPath calculateNewPath(Entity from, Entity to) { + PositionComponent myPositionComponent = + (PositionComponent) from.getComponent(PositionComponent.name); + PositionComponent heroPositionComponent = + (PositionComponent) to.getComponent(PositionComponent.name); + if (myPositionComponent != null && heroPositionComponent != null) { + ILevel level = ECS.currentLevel; + Coordinate myPosition = myPositionComponent.getPosition().toCoordinate(); + Coordinate heroposition = heroPositionComponent.getPosition().toCoordinate(); + return level.findPath(level.getTileAt(myPosition), level.getTileAt(heroposition)); + } + return null; + } + + /** + * Sets the velocity of the passed entity so that it takes the next necessary step to get to the + * end of the path. + * + * @param entity Entity moving on the path + * @param path Path on which the entity moves + */ + public static void move(Entity entity, GraphPath path) { + PositionComponent pc = (PositionComponent) entity.getComponent(PositionComponent.name); + VelocityComponent vc = (VelocityComponent) entity.getComponent(VelocityComponent.name); + ILevel level = ECS.currentLevel; + Tile currentTile = level.getTileAt(pc.getPosition().toCoordinate()); + int i = 0; + Tile nextTile = null; + do { + if (i >= path.getCount()) return; + if (path.get(i).equals(currentTile)) { + nextTile = path.get(i + 1); + } + i++; + } while (nextTile == null); + + switch (currentTile.directionTo(nextTile)[0]) { + case N -> vc.setY(vc.getySpeed()); + case S -> vc.setY(-vc.getySpeed()); + case E -> vc.setX(vc.getxSpeed()); + case W -> vc.setX(-vc.getxSpeed()); + } + if (currentTile.directionTo(nextTile).length > 1) + switch (currentTile.directionTo(nextTile)[1]) { + case N -> vc.setY(vc.getySpeed()); + case S -> vc.setY(-vc.getySpeed()); + case E -> vc.setX(vc.getxSpeed()); + case W -> vc.setX(-vc.getxSpeed()); + } + } + + /** + * Checks if the position of the player is within the given radius of the position of the given + * entity. + * + * @param entity Entity whose position specifies the center point + * @param range Reichweite die betrachtet werden soll + * @return Ob sich der Spieler in Reichweite befindet + */ + public static boolean playerInRange(Entity entity, float range) { + PositionComponent myPositionComponent = + (PositionComponent) entity.getComponent(PositionComponent.name); + if (ECS.hero != null) { + PositionComponent heroPositionComponent = + (PositionComponent) ECS.hero.getComponent(PositionComponent.name); + if (heroPositionComponent != null) { + Point myPosition = myPositionComponent.getPosition(); + Point heroPosition = heroPositionComponent.getPosition(); + + float xDiff = myPosition.x - heroPosition.x; + float yDiff = myPosition.y - heroPosition.y; + float distance = (float) Math.sqrt(xDiff * xDiff + yDiff * yDiff); + return distance <= range; + } + } + return false; + } +} diff --git a/game/src/ecs/components/ai/fight/IFightAI.java b/game/src/ecs/components/ai/fight/IFightAI.java new file mode 100644 index 0000000000..786ae5f97c --- /dev/null +++ b/game/src/ecs/components/ai/fight/IFightAI.java @@ -0,0 +1,13 @@ +package ecs.components.ai.fight; + +import ecs.entities.Entity; + +public interface IFightAI { + + /** + * Implements the combat behavior of an AI controlled entity + * + * @param entity associated entity + */ + void fight(Entity entity); +} diff --git a/game/src/ecs/components/ai/fight/MeleeAI.java b/game/src/ecs/components/ai/fight/MeleeAI.java new file mode 100644 index 0000000000..51a8372446 --- /dev/null +++ b/game/src/ecs/components/ai/fight/MeleeAI.java @@ -0,0 +1,48 @@ +package ecs.components.ai.fight; + +import com.badlogic.gdx.ai.pfa.GraphPath; +import ecs.components.ai.AITools; +import ecs.components.skill.Skill; +import ecs.entities.Entity; +import java.lang.reflect.InvocationTargetException; +import level.elements.tile.Tile; +import mydungeon.ECS; +import tools.Constants; + +public class MeleeAI implements IFightAI { + private final float attackRange; + private final int delay = Constants.FRAME_RATE; + private int timeSinceLastUpdate = 0; + private final Skill fightSkill; + private GraphPath path; + + /** + * Attacks the player if he is within the given range. Otherwise, it will move towards the + * player. + * + * @param attackRange Range in which the attack skill should be executed + * @param fightSkill Skill to be used when an attack is performed + */ + public MeleeAI(float attackRange, Skill fightSkill) { + this.attackRange = attackRange; + this.fightSkill = fightSkill; + } + + @Override + public void fight(Entity entity) { + if (AITools.playerInRange(entity, attackRange)) { + try { + fightSkill.execute(entity); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } else { + if (timeSinceLastUpdate >= delay) { + path = AITools.calculateNewPath(entity, ECS.hero); + timeSinceLastUpdate = -1; + } + timeSinceLastUpdate++; + AITools.move(entity, path); + } + } +} diff --git a/game/src/ecs/components/ai/idle/IIdleAI.java b/game/src/ecs/components/ai/idle/IIdleAI.java new file mode 100644 index 0000000000..15a74b1372 --- /dev/null +++ b/game/src/ecs/components/ai/idle/IIdleAI.java @@ -0,0 +1,13 @@ +package ecs.components.ai.idle; + +import ecs.entities.Entity; + +public interface IIdleAI { + + /** + * Implements the idle behavior of an AI controlled entity + * + * @param entity associated entity + */ + void idle(Entity entity); +} diff --git a/game/src/ecs/components/ai/idle/RadiusWalk.java b/game/src/ecs/components/ai/idle/RadiusWalk.java new file mode 100644 index 0000000000..0d5dd402f8 --- /dev/null +++ b/game/src/ecs/components/ai/idle/RadiusWalk.java @@ -0,0 +1,48 @@ +package ecs.components.ai.idle; + +import com.badlogic.gdx.ai.pfa.GraphPath; +import ecs.components.PositionComponent; +import ecs.components.ai.AITools; +import ecs.entities.Entity; +import level.elements.ILevel; +import level.elements.tile.Tile; +import mydungeon.ECS; +import tools.Constants; + +public class RadiusWalk implements IIdleAI { + private final float radius; + private GraphPath path; + private final int breakTime = Constants.FRAME_RATE * 5; + private int currentBreak = 0; + + /** + * Finds a point in the radius and then moves there. When the point has been reached, a new + * point in the radius is searched for from there. + * + * @param radius Radius in which a target point is to be searched for + */ + public RadiusWalk(float radius) { + this.radius = radius; + } + + @Override + public void idle(Entity entity) { + if (path == null || pathFinished(entity)) { + if (currentBreak >= breakTime) { + currentBreak = 0; + path = AITools.calculateNewPath(entity, radius); + idle(entity); + } + + currentBreak++; + + } else AITools.move(entity, path); + } + + private boolean pathFinished(Entity entity) { + PositionComponent pc = (PositionComponent) entity.getComponent(PositionComponent.name); + ILevel level = ECS.currentLevel; + return path.get(path.getCount() - 1) + .equals(level.getTileAt(pc.getPosition().toCoordinate())); + } +} diff --git a/game/src/ecs/components/ai/transition/ITransition.java b/game/src/ecs/components/ai/transition/ITransition.java new file mode 100644 index 0000000000..910c7cbd85 --- /dev/null +++ b/game/src/ecs/components/ai/transition/ITransition.java @@ -0,0 +1,15 @@ +package ecs.components.ai.transition; + +import ecs.entities.Entity; + +/** Determines when an ai switches between idle and fight */ +public interface ITransition { + + /** + * Function that determines whether an entity should be in combat mode + * + * @param entity associated entity + * @return if the entity should fight + */ + boolean isInFightMode(Entity entity); +} diff --git a/game/src/ecs/components/ai/transition/RangeTransition.java b/game/src/ecs/components/ai/transition/RangeTransition.java new file mode 100644 index 0000000000..133dfe4890 --- /dev/null +++ b/game/src/ecs/components/ai/transition/RangeTransition.java @@ -0,0 +1,23 @@ +package ecs.components.ai.transition; + +import ecs.components.ai.AITools; +import ecs.entities.Entity; + +public class RangeTransition implements ITransition { + + private final float range; + + /** + * Switches to combat mode when the player is within range of the entity. + * + * @param range Range of the entity. + */ + public RangeTransition(float range) { + this.range = range; + } + + @Override + public boolean isInFightMode(Entity entity) { + return AITools.playerInRange(entity, range); + } +} diff --git a/game/src/ecs/components/Skill.java b/game/src/ecs/components/skill/Skill.java similarity index 97% rename from game/src/ecs/components/Skill.java rename to game/src/ecs/components/skill/Skill.java index 918b450b6c..e5d94e3fda 100644 --- a/game/src/ecs/components/Skill.java +++ b/game/src/ecs/components/skill/Skill.java @@ -1,4 +1,4 @@ -package ecs.components; +package ecs.components.skill; import graphic.Animation; import java.lang.reflect.InvocationTargetException; diff --git a/game/src/ecs/components/SkillComponent.java b/game/src/ecs/components/skill/SkillComponent.java similarity index 93% rename from game/src/ecs/components/SkillComponent.java rename to game/src/ecs/components/skill/SkillComponent.java index 781757a327..538c5737e5 100644 --- a/game/src/ecs/components/SkillComponent.java +++ b/game/src/ecs/components/skill/SkillComponent.java @@ -1,5 +1,6 @@ -package ecs.components; +package ecs.components.skill; +import ecs.components.Component; import ecs.entities.Entity; import java.util.HashSet; import java.util.Set; diff --git a/game/src/ecs/systems/AISystem.java b/game/src/ecs/systems/AISystem.java new file mode 100644 index 0000000000..a034fee736 --- /dev/null +++ b/game/src/ecs/systems/AISystem.java @@ -0,0 +1,18 @@ +package ecs.systems; + +import ecs.components.ai.AIComponent; +import ecs.entities.Entity; +import mydungeon.ECS; + +/** Controls the AI */ +public class AISystem extends ECS_System { + @Override + public void update() { + for (Entity entity : ECS.entities) { + AIComponent aiComponent = (AIComponent) entity.getComponent(AIComponent.name); + if (aiComponent != null) { + aiComponent.execute(); + } + } + } +} diff --git a/game/src/ecs/systems/VelocitySystem.java b/game/src/ecs/systems/VelocitySystem.java index 2bf573ae5c..4f85a7cd0a 100644 --- a/game/src/ecs/systems/VelocitySystem.java +++ b/game/src/ecs/systems/VelocitySystem.java @@ -25,11 +25,12 @@ public void update() { // Update the position based on the velocity float newX = position.getPosition().x + velocity.getX(); float newY = position.getPosition().y + velocity.getY(); - Point newPosition = new Point(newX, newY); if (ECS.currentLevel.getTileAt(newPosition.toCoordinate()).isAccessible()) { position.setPosition(newPosition); movementAnimation(entity); + velocity.setY(0); + velocity.setX(0); } } } diff --git a/game/src/mydungeon/ECS.java b/game/src/mydungeon/ECS.java index 01064515c4..f6540fe78f 100644 --- a/game/src/mydungeon/ECS.java +++ b/game/src/mydungeon/ECS.java @@ -27,8 +27,8 @@ public class ECS extends Game { public static ILevel currentLevel; - private Hero hero; private PositionComponent heroPositionComponent; + public static Hero hero; @Override protected void setup() { @@ -43,6 +43,7 @@ protected void setup() { new VelocitySystem(); new DrawSystem(painter); new KeyboardSystem(); + new AISystem(); } @Override @@ -88,8 +89,8 @@ private void setupDSLInput() { position_component { }, velocity_component { - x_speed: 0.5, - y_speed: 0.5, + x_speed: 0.1, + y_speed: 0.1, move_right_animation:"monster/imp/runRight", move_left_animation: "monster/imp/runLeft" }, @@ -97,6 +98,8 @@ private void setupDSLInput() { idle_left: "monster/imp/idleLeft", idle_right: "monster/imp/idleRight", current_animation: "monster/imp/idleLeft" + }, + ai_component { } } diff --git a/game/test/ecs/components/SkillComponentTest.java b/game/test/ecs/components/SkillComponentTest.java index 8cdf37d49e..a99ce16387 100644 --- a/game/test/ecs/components/SkillComponentTest.java +++ b/game/test/ecs/components/SkillComponentTest.java @@ -2,6 +2,8 @@ import static org.junit.Assert.*; +import ecs.components.skill.Skill; +import ecs.components.skill.SkillComponent; import ecs.entities.Entity; import org.junit.Before; import org.junit.Test; diff --git a/game/test/ecs/components/SkillTest.java b/game/test/ecs/components/SkillTest.java index 5ed4bf20cd..a17ed4a371 100644 --- a/game/test/ecs/components/SkillTest.java +++ b/game/test/ecs/components/SkillTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.*; +import ecs.components.skill.Skill; import graphic.Animation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method;