fix: make EnemyAI StateMachineAI and fix patrol bug

This commit is contained in:
Renge 2022-04-24 19:51:23 -04:00
parent 7550a4381c
commit 7c63a7957b
7 changed files with 40 additions and 270 deletions

View File

@ -1,12 +1,6 @@
import GoapActionPlanner from "../../Wolfie2D/AI/GoapActionPlanner";
import StateMachineAI from "../../Wolfie2D/AI/StateMachineAI"; import StateMachineAI from "../../Wolfie2D/AI/StateMachineAI";
import StateMachineGoapAI from "../../Wolfie2D/AI/StateMachineGoapAI";
import GoapAction from "../../Wolfie2D/DataTypes/Interfaces/GoapAction";
import AABB from "../../Wolfie2D/DataTypes/Shapes/AABB"; import AABB from "../../Wolfie2D/DataTypes/Shapes/AABB";
import Stack from "../../Wolfie2D/DataTypes/Stack";
import State from "../../Wolfie2D/DataTypes/State/State";
import Vec2 from "../../Wolfie2D/DataTypes/Vec2"; import Vec2 from "../../Wolfie2D/DataTypes/Vec2";
import GameEvent from "../../Wolfie2D/Events/GameEvent";
import GameNode from "../../Wolfie2D/Nodes/GameNode"; import GameNode from "../../Wolfie2D/Nodes/GameNode";
import AnimatedSprite from "../../Wolfie2D/Nodes/Sprites/AnimatedSprite"; import AnimatedSprite from "../../Wolfie2D/Nodes/Sprites/AnimatedSprite";
import OrthogonalTilemap from "../../Wolfie2D/Nodes/Tilemaps/OrthogonalTilemap"; import OrthogonalTilemap from "../../Wolfie2D/Nodes/Tilemaps/OrthogonalTilemap";
@ -19,15 +13,13 @@ import { GameState, Statuses } from "../sword_enums";
import Sprite from "../../Wolfie2D/Nodes/Sprites/Sprite"; import Sprite from "../../Wolfie2D/Nodes/Sprites/Sprite";
import MathUtils from "../../Wolfie2D/Utils/MathUtils";
import { Player_Events } from "../sword_enums"; import { Player_Events } from "../sword_enums";
import InputWrapper from "../Tools/InputWrapper"; import InputWrapper from "../Tools/InputWrapper";
import Timer from "../../Wolfie2D/Timing/Timer"; import Timer from "../../Wolfie2D/Timing/Timer";
import PlayerController from "../Player/PlayerController"; import PlayerController from "../Player/PlayerController";
import Rect from "../../Wolfie2D/Nodes/Graphics/Rect"; import Rect from "../../Wolfie2D/Nodes/Graphics/Rect";
import Color from "../../Wolfie2D/Utils/Color"; import Color from "../../Wolfie2D/Utils/Color";
export default class EnemyAI extends StateMachineGoapAI implements BattlerAI { export default class EnemyAI extends StateMachineAI implements BattlerAI {
/** The owner of this AI */ /** The owner of this AI */
owner: AnimatedSprite; owner: AnimatedSprite;
@ -52,9 +44,6 @@ export default class EnemyAI extends StateMachineGoapAI implements BattlerAI {
// The last known position of the player // The last known position of the player
lastPlayerPos: Vec2; lastPlayerPos: Vec2;
// Attack range
inRange: number;
// Path to player // Path to player
//path: NavigationPath; //path: NavigationPath;
@ -91,7 +80,8 @@ export default class EnemyAI extends StateMachineGoapAI implements BattlerAI {
//add states //add states
// Patrol mode // Patrol mode
this.addState(EnemyStates.DEFAULT, new Patrol(this, owner)); this.addState(EnemyStates.PATROL, new Patrol(this, owner));
// this.addState(EnemyStates.ALERT,)
this.maxHealth = options.health; this.maxHealth = options.health;
@ -101,22 +91,10 @@ export default class EnemyAI extends StateMachineGoapAI implements BattlerAI {
this.player = options.player; this.player = options.player;
this.inRange = options.inRange;
this.goal = options.goal;
this.currentStatus = options.status;
this.possibleActions = options.actions;
this.plan = new Stack<GoapAction>();
this.planner = new GoapActionPlanner();
//TODO - get correct tilemap //TODO - get correct tilemap
this.tilemap = <OrthogonalTilemap>this.owner.getScene().getLayer("Wall").getItems()[0]; this.tilemap = <OrthogonalTilemap>this.owner.getScene().getLayer("Wall").getItems()[0];
// Initialize to the default state // Initialize to the default state
this.initialize(EnemyStates.DEFAULT); this.initialize(EnemyStates.PATROL);
this.direction = 1; //default moving to the right this.direction = 1; //default moving to the right
@ -131,8 +109,6 @@ export default class EnemyAI extends StateMachineGoapAI implements BattlerAI {
this.attackTimer = new Timer(2500); this.attackTimer = new Timer(2500);
} }
activate(options: Record<string, any>): void { }
damage(damage: number): void { damage(damage: number): void {
// enemy already dead, do not send new event // enemy already dead, do not send new event
if (this.CURRENT_HP <= 0) { if (this.CURRENT_HP <= 0) {
@ -144,11 +120,6 @@ export default class EnemyAI extends StateMachineGoapAI implements BattlerAI {
this.owner.animation.play("HURT",false); this.owner.animation.play("HURT",false);
console.log(damage +" damage taken, "+this.CURRENT_HP+" hp left"); console.log(damage +" damage taken, "+this.CURRENT_HP+" hp left");
// If we're low enough, add Low Health status to enemy
if (this.CURRENT_HP <= Math.floor(this.maxHealth/2)) {
}
// If health goes below 0, disable AI and fire enemyDied event // If health goes below 0, disable AI and fire enemyDied event
if (this.CURRENT_HP <= 0) { if (this.CURRENT_HP <= 0) {
this.owner.setAIActive(false, {}); this.owner.setAIActive(false, {});
@ -172,17 +143,10 @@ export default class EnemyAI extends StateMachineGoapAI implements BattlerAI {
} }
this.emitter.fireEvent(Player_Events.ENEMY_KILLED, {owner: this.owner.id, ai:this}); this.emitter.fireEvent(Player_Events.ENEMY_KILLED, {owner: this.owner.id, ai:this});
if (Math.random() < 0.05) {
// give buff maybe
//this.emitter.fireEvent("giveBuff", { position: this.owner.position });
}
} }
} }
//TODO - need to modify for side view //TODO - need to modify for side view
isPlayerVisible(pos: Vec2): Vec2{ isPlayerVisible(pos: Vec2): Vec2{
//Check ifplayer is visible, taking into account walls //Check ifplayer is visible, taking into account walls
@ -235,12 +199,9 @@ export default class EnemyAI extends StateMachineGoapAI implements BattlerAI {
* @returns position of the player if visible, else null * @returns position of the player if visible, else null
*/ */
getPlayerPosition(): Vec2 { getPlayerPosition(): Vec2 {
//TODO - check if player is visible return this.isPlayerVisible(this.player.position);
if(this.isPlayerVisible(this.player.position))
return this.player.position;
else
return null;
} }
update(deltaT: number){ update(deltaT: number){
if (InputWrapper.getState() != GameState.GAMING) { if (InputWrapper.getState() != GameState.GAMING) {
this.owner.animation.pause(); this.owner.animation.pause();
@ -249,22 +210,6 @@ export default class EnemyAI extends StateMachineGoapAI implements BattlerAI {
this.owner.animation.resume(); this.owner.animation.resume();
super.update(deltaT); super.update(deltaT);
// This is the plan that is executed in the Active state, so whenever we don't have a plan, acquire a new one given the current statuses the enemy has
/*
if (this.plan.isEmpty()) {
//get a new plan
if(this.possibleActions === undefined){
console.log("undefined possiblse actions");
}
if(this.currentState === undefined){
console.log("undefined current status");
}
this.plan = this.planner.plan(Statuses.REACHED_GOAL, this.possibleActions, this.currentStatus, null);
}
*/
//TODO
if(this.burnTimer.isStopped() && this.burnCounter >0){ if(this.burnTimer.isStopped() && this.burnCounter >0){
this.burnCounter --; this.burnCounter --;
this.burnTimer.start(); this.burnTimer.start();
@ -281,10 +226,6 @@ export default class EnemyAI extends StateMachineGoapAI implements BattlerAI {
this.damage(5 + (<PlayerController>this.player._ai).extraDotDmg + (<PlayerController>this.player._ai).CURRENT_ATK * .08); this.damage(5 + (<PlayerController>this.player._ai).extraDotDmg + (<PlayerController>this.player._ai).CURRENT_ATK * .08);
} }
if (this.healthBar) { if (this.healthBar) {
this.healthBar.position = this.owner.collisionShape.center.clone().add(new Vec2(0, -((<AABB>this.owner.collisionShape).hh+5))); this.healthBar.position = this.owner.collisionShape.center.clone().add(new Vec2(0, -((<AABB>this.owner.collisionShape).hh+5)));
this.healthBar.fillWidth = this.CURRENT_HP/this.maxHealth * this.owner.collisionShape.hw * 3; this.healthBar.fillWidth = this.CURRENT_HP/this.maxHealth * this.owner.collisionShape.hw * 3;
@ -314,7 +255,7 @@ export default class EnemyAI extends StateMachineGoapAI implements BattlerAI {
} }
export enum EnemyStates { export enum EnemyStates {
DEFAULT = "default", PATROL = "patrol",
ALERT = "alert", ALERT = "alert",
PREVIOUS = "previous" ATTACK = "attack"
} }

View File

@ -1,47 +0,0 @@
import StateMachineGoapAI from "../../../Wolfie2D/AI/StateMachineGoapAI";
import GoapAction from "../../../Wolfie2D/DataTypes/Interfaces/GoapAction";
import Vec2 from "../../../Wolfie2D/DataTypes/Vec2";
import Emitter from "../../../Wolfie2D/Events/Emitter";
import GameNode from "../../../Wolfie2D/Nodes/GameNode";
import EnemyAI from "../EnemyAI";
export default class AttackAction extends GoapAction {
protected emitter: Emitter;
constructor(cost: number, preconditions: Array<string>, effects: Array<string>, options?: Record<string, any>) {
super();
this.cost = cost;
this.preconditions = preconditions;
this.effects = effects;
}
performAction(statuses: Array<string>, actor: StateMachineGoapAI, deltaT: number, target?: StateMachineGoapAI): Array<string> {
//Check if preconditions are met for this action to be performed
if (this.checkPreconditions(statuses)){
let enemy = <EnemyAI>actor;
//If the player is out of sight, don't bother attacking
if (enemy.getPlayerPosition() == null){
return null;
}
//Randomize attack direction, gives the enemy gun users stormtrooper aim
let dir = enemy.getPlayerPosition().clone().sub(enemy.owner.position).normalize();
dir.rotateCCW(Math.PI / 4 * Math.random() - Math.PI/8);
if(enemy.weapon.use(enemy.owner, "enemy", dir)){
// If we fired, face that direction
enemy.owner.rotation = Vec2.UP.angleToCCW(dir);
}
return this.effects;
}
return null;
}
updateCost(options: Record<string, number>): void {}
toString(): string {
return "(AttackAction)";
}
}

View File

@ -1,47 +0,0 @@
import StateMachineGoapAI from "../../../Wolfie2D/AI/StateMachineGoapAI";
import GoapAction from "../../../Wolfie2D/DataTypes/Interfaces/GoapAction";
import Vec2 from "../../../Wolfie2D/DataTypes/Vec2";
import Emitter from "../../../Wolfie2D/Events/Emitter";
import NavigationPath from "../../../Wolfie2D/Pathfinding/NavigationPath";
import EnemyAI from "../EnemyAI";
export default class Move extends GoapAction {
private inRange: number;
private path: NavigationPath;
protected emitter: Emitter;
constructor(cost: number, preconditions: Array<string>, effects: Array<string>, options?: Record<string, any>) {
super();
this.cost = cost;
this.preconditions = preconditions;
this.effects = effects;
this.loopAction = true;
this.inRange = options.inRange;
}
performAction(statuses: Array<string>, actor: StateMachineGoapAI, deltaT: number, target?: StateMachineGoapAI): Array<string> {
if (this.checkPreconditions(statuses)){
//Check distance from player
let enemy = <EnemyAI>actor;
let playerPos = enemy.lastPlayerPos;
let distance = enemy.owner.position.distanceTo(playerPos);
//If close enough, we've moved far enough and this loop action is done
if (distance <= this.inRange){
return this.effects;
}
//Otherwise move
return null;
}
return this.effects;
}
updateCost(options: Record<string, number>): void {}
toString(): string {
return "(Move)";
}
}

View File

@ -1,23 +1,32 @@
import AABB from "../../../Wolfie2D/DataTypes/Shapes/AABB";
import State from "../../../Wolfie2D/DataTypes/State/State"; import State from "../../../Wolfie2D/DataTypes/State/State";
import Vec2 from "../../../Wolfie2D/DataTypes/Vec2";
import GameEvent from "../../../Wolfie2D/Events/GameEvent";
import GameNode from "../../../Wolfie2D/Nodes/GameNode"; import GameNode from "../../../Wolfie2D/Nodes/GameNode";
import EnemyAI from "../EnemyAI"; import EnemyAI from "../EnemyAI";
export default abstract class EnemyState extends State { export default abstract class EnemyState extends State {
protected parent: EnemyAI; protected parent: EnemyAI;
protected owner: GameNode; protected owner: GameNode;
gravity: number = 1500; //TODO - can change later gravity: number = 1500; //TODO - can change later
constructor(parent: EnemyAI, owner: GameNode){ constructor(parent: EnemyAI, owner: GameNode) {
super(parent); super(parent);
this.owner = owner; this.owner = owner;
} }
handleInput(event: GameEvent): void { }
canWalk() {
update(deltaT: number): void { let collision = <AABB>this.owner.collisionShape;
// Do gravity let colrow = this.parent.tilemap.getColRowAt(collision.center.clone().add(new Vec2(this.parent.direction * (collision.hw+2))));
this.parent.velocity.y += this.gravity*deltaT; return !this.parent.tilemap.isTileCollidable(colrow.x, colrow.y) && this.parent.tilemap.isTileCollidable(colrow.x,colrow.y+1);
} }
update(deltaT: number): void {
// Do gravity
this.parent.velocity.y += this.gravity * deltaT;
this.owner.move(this.parent.velocity.scaled(deltaT));
}
} }

View File

@ -8,63 +8,27 @@ import EnemyState from "./EnemyState";
import Sprite from "../../../Wolfie2D/Nodes/Sprites/Sprite"; import Sprite from "../../../Wolfie2D/Nodes/Sprites/Sprite";
import MathUtils from "../../../Wolfie2D/Utils/MathUtils"; import MathUtils from "../../../Wolfie2D/Utils/MathUtils";
import AnimatedSprite from "../../../Wolfie2D/Nodes/Sprites/AnimatedSprite"; import AnimatedSprite from "../../../Wolfie2D/Nodes/Sprites/AnimatedSprite";
import AABB from "../../../Wolfie2D/DataTypes/Shapes/AABB";
export default class Patrol extends EnemyState { export default class Patrol extends EnemyState {
// A return object for exiting this state
protected retObj: Record<string, any>;
constructor(parent: EnemyAI, owner: GameNode){
super(parent, owner);
}
onEnter(options: Record<string, any>): void { onEnter(options: Record<string, any>): void {
//this.currentPath = this.getNextPath(); (<AnimatedSprite>this.owner).animation.playIfNotAlready("IDLE", true);
//if(!(<AnimatedSprite>this.owner).animation.isPlaying("DYING")){
//(<AnimatedSprite>this.owner).animation.queue("IDLE", true);
//}
//else{
(<AnimatedSprite>this.owner).animation.playIfNotAlready("IDLE", true);
//}
} }
handleInput(event: GameEvent): void { }
update(deltaT: number): void { update(deltaT: number): void {
super.update(deltaT); if(!this.canWalk()){
this.parent.direction *= -1;
//no goap rn, just adding some moving
let colrow = this.parent.tilemap.getColRowAt(this.owner.position.clone());
//check if next tile on walking path is collidable
if(this.parent.tilemap.isTileCollidable(colrow.x+this.parent.direction,colrow.y) || this.parent.tilemap.isTileCollidable(colrow.x+this.parent.direction,colrow.y-1)){
//turn around
this.parent.direction *= -1;
}
//check if next ground tile is collidable
else if(!this.parent.tilemap.isTileCollidable(colrow.x+this.parent.direction,colrow.y+1)){
//turn around
this.parent.direction *=-1;
}
else{
//keep moving
} }
//move //move
this.parent.velocity.x = this.parent.direction * this.parent.speed; this.parent.velocity.x = this.parent.direction * this.parent.speed;
(<Sprite>this.owner).invertX = this.parent.direction ==1? true: false ; (<Sprite>this.owner).invertX = this.parent.direction === 1 ? true : false ;
//move this elsewhere later super.update(deltaT);
this.owner.move(this.parent.velocity.scaled(deltaT));
//console.log("enemy moving");
} }
onExit(): Record<string, any> { onExit(): Record<string, any> {
return this.retObj; (<AnimatedSprite>this.owner).animation.stop();
return null;
} }
} }

View File

@ -34,8 +34,6 @@ import Story from "../Tools/DataTypes/Story";
import Sprite from "../../Wolfie2D/Nodes/Sprites/Sprite"; import Sprite from "../../Wolfie2D/Nodes/Sprites/Sprite";
import TextInput from "../../Wolfie2D/Nodes/UIElements/TextInput"; import TextInput from "../../Wolfie2D/Nodes/UIElements/TextInput";
import { TiledTilemapData } from "../../Wolfie2D/DataTypes/Tilesets/TiledData"; import { TiledTilemapData } from "../../Wolfie2D/DataTypes/Tilesets/TiledData";
import AttackAction from "../AI/EnemyActions/AttackAction";
import Move from "../AI/EnemyActions/Move";
import GameOver from "./GameOver"; import GameOver from "./GameOver";
import Porcelain from "./Porcelain"; import Porcelain from "./Porcelain";
import Tutorial from "./Tutorial"; import Tutorial from "./Tutorial";
@ -867,19 +865,7 @@ export default class GameLevel extends Scene {
(<EnemyAI>enemy._ai).bleedStat.scale.set(1, 1); (<EnemyAI>enemy._ai).bleedStat.scale.set(1, 1);
enemy.setGroup("Enemy"); enemy.setGroup("Enemy");
enemy.setTrigger("player", Player_Events.PLAYER_COLLIDE, null); enemy.setTrigger("player", Player_Events.PLAYER_COLLIDE, null);
let actionsDefault = [new AttackAction(3, [Statuses.IN_RANGE], [Statuses.REACHED_GOAL]),
new Move(2, [], [Statuses.IN_RANGE], {inRange: 60}),
];
let statusArray : Array<string> = [Statuses.CAN_RETREAT, Statuses.CAN_BERSERK];
//TODO - not working correctly
if ( "status" !in aiOptions ){
aiOptions["status"] = statusArray;
}
if( "actions" !in aiOptions){
aiOptions["actions"] = actionsDefault;
}
//add enemy to the enemy array //add enemy to the enemy array
this.enemies.push(enemy); this.enemies.push(enemy);
//this.battleManager.setEnemies(this.enemies.map(enemy => <BattlerAI>enemy._ai)); //this.battleManager.setEnemies(this.enemies.map(enemy => <BattlerAI>enemy._ai));
@ -889,34 +875,13 @@ export default class GameLevel extends Scene {
//TODO - give each enemy unique weapon //TODO - give each enemy unique weapon
protected initializeEnemies( enemies: Enemy[]){ protected initializeEnemies( enemies: Enemy[]){
let actionsDefault = [new AttackAction(3, [Statuses.IN_RANGE], [Statuses.REACHED_GOAL]),
new Move(2, [], [Statuses.IN_RANGE], {inRange: 60}),
];
let statusArray : Array<string> = [Statuses.CAN_RETREAT, Statuses.CAN_BERSERK];
for (let enemy of enemies) { for (let enemy of enemies) {
switch (enemy.type) { switch (enemy.type) {
case "test_dummy":
this.addEnemy("test_dummy", enemy.position.scale(32), {
player: this.player,
health: 100,
tilemap: "Main",
actions: actionsDefault,
status: statusArray,
goal: Statuses.REACHED_GOAL,
//size: new AABB(Vec2.ZERO, new Vec2(16, 25)),
exp: 100,
weapon : this.createWeapon("knife")
})
break;
case "Snake": //Snake enemies drop from sky("trees")? or could just be very abundant case "Snake": //Snake enemies drop from sky("trees")? or could just be very abundant
this.addEnemy("Snake", enemy.position.scale(32), { this.addEnemy("Snake", enemy.position.scale(32), {
player: this.player, player: this.player,
health: 50, health: 50,
tilemap: "Main", tilemap: "Main",
actions: actionsDefault,
status: statusArray,
goal: Statuses.REACHED_GOAL, goal: Statuses.REACHED_GOAL,
size: new Vec2(14,10), size: new Vec2(14,10),
offset : new Vec2(0, 22), offset : new Vec2(0, 22),
@ -929,11 +894,8 @@ export default class GameLevel extends Scene {
player: this.player, player: this.player,
health: 200, health: 200,
tilemap: "Main", tilemap: "Main",
goal: Statuses.REACHED_GOAL,
exp: 100, exp: 100,
weapon : this.createWeapon("knife"), weapon : this.createWeapon("knife"),
actions: actionsDefault,
status: statusArray,
}) })
break; break;
@ -943,11 +905,8 @@ export default class GameLevel extends Scene {
health: 200, health: 200,
tilemap: "Main", tilemap: "Main",
//actions:actions, //actions:actions,
goal: Statuses.REACHED_GOAL,
exp: 50, exp: 50,
weapon : this.createWeapon("knife"), weapon : this.createWeapon("knife"),
actions: actionsDefault,
status: statusArray,
}) })
break; break;
case "black_pudding": case "black_pudding":
@ -956,14 +915,11 @@ export default class GameLevel extends Scene {
health: 200, health: 200,
tilemap: "Main", tilemap: "Main",
//actions:actions, //actions:actions,
goal: Statuses.REACHED_GOAL,
scale: .25, scale: .25,
size: new Vec2(16,10), size: new Vec2(16,10),
offset : new Vec2(0,6), offset : new Vec2(0,6),
exp: 50, exp: 50,
weapon : this.createWeapon("knife"), weapon : this.createWeapon("knife"),
actions: actionsDefault,
status: statusArray,
}) })
break; break;
default: default:

View File

@ -11,8 +11,6 @@ import { Statuses } from "../sword_enums";
import AABB from "../../Wolfie2D/DataTypes/Shapes/AABB"; import AABB from "../../Wolfie2D/DataTypes/Shapes/AABB";
import EnemyAI from "../AI/EnemyAI"; import EnemyAI from "../AI/EnemyAI";
import BattlerAI from "../AI/BattlerAI"; import BattlerAI from "../AI/BattlerAI";
import AttackAction from "../AI/EnemyActions/AttackAction";
import Move from "../AI/EnemyActions/Move";
import Porcelain from "./Porcelain"; import Porcelain from "./Porcelain";
import MainMenu from "./MainMenu"; import MainMenu from "./MainMenu";
@ -45,13 +43,9 @@ export default class Tutorial extends GameLevel {
player: this.player, player: this.player,
health: 50, health: 50,
tilemap: "Main", tilemap: "Main",
goal: Statuses.REACHED_GOAL,
size: new Vec2(14,10), size: new Vec2(14,10),
offset : new Vec2(0, 22), offset : new Vec2(0, 22),
exp: 50, exp: 50,
actions: [new AttackAction(3, [Statuses.IN_RANGE], [Statuses.REACHED_GOAL]),
new Move(2, [], [Statuses.IN_RANGE], {inRange: 60})],
status : [Statuses.CAN_RETREAT, Statuses.CAN_BERSERK],
weapon : this.createWeapon("knife") weapon : this.createWeapon("knife")
}) })
} }