added super simple ai

no GOAP yet
This commit is contained in:
OfficialCHenry 2022-04-10 18:58:09 -04:00
parent d8223358d8
commit 417315cf08
8 changed files with 412 additions and 32 deletions

View File

@ -0,0 +1,196 @@
import GoapActionPlanner from "../../Wolfie2D/AI/GoapActionPlanner";
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 Stack from "../../Wolfie2D/DataTypes/Stack";
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 AnimatedSprite from "../../Wolfie2D/Nodes/Sprites/AnimatedSprite";
import OrthogonalTilemap from "../../Wolfie2D/Nodes/Tilemaps/OrthogonalTilemap";
import NavigationPath from "../../Wolfie2D/Pathfinding/NavigationPath";
import Weapon from "../GameSystems/items/Weapon";
import BattlerAI from "./BattlerAI";
import Patrol from "./EnemyStates/Patrol";
import { Statuses } from "../sword_enums";
import Sprite from "../../Wolfie2D/Nodes/Sprites/Sprite";
import MathUtils from "../../Wolfie2D/Utils/MathUtils";
export default class EnemyAI extends StateMachineGoapAI implements BattlerAI {
/** The owner of this AI */
owner: AnimatedSprite;
/** The total possible amount of health this entity has */
maxHealth: number;
/** The current amount of health this entity has */
CURRENT_HP: number;
/** The default movement speed of this AI */
speed: number = 20;
/** The weapon this AI has */
weapon: Weapon;
/** A reference to the player object */
player: GameNode;
// The current known position of the player
playerPos: Vec2;
// The last known position of the player
lastPlayerPos: Vec2;
// Attack range
inRange: number;
// Path to player
//path: NavigationPath;
// Path away from player
retreatPath: NavigationPath;
tilemap: OrthogonalTilemap;
velocity: Vec2 = Vec2.ZERO;
direction: number; //1 for right, -1 for left
initializeAI(owner: AnimatedSprite, options: Record<string, any>): void {
this.owner = owner;
//add states
// Patrol mode
this.addState(EnemyStates.DEFAULT, new Patrol(this, owner));
this.maxHealth = options.health;
this.CURRENT_HP = options.health;
this.weapon = options.weapon;
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
//this.tilemap = this.owner.getScene().getTilemap(options.tilemap) as OrthogonalTilemap;
this.tilemap = <OrthogonalTilemap>this.owner.getScene().getLayer("Wall").getItems()[0];
//this.tilemap = <OrthogonalTilemap>this.owner.getScene().getTilemap("Main");
// Initialize to the default state
this.initialize(EnemyStates.DEFAULT);
//this.getPlayerPosition();
this.direction = 1; //default moving to the right
}
activate(options: Record<string, any>): void { }
damage(damage: number): void {
this.CURRENT_HP -= damage;
// 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 (this.CURRENT_HP <= 0) {
this.owner.setAIActive(false, {});
this.owner.isCollidable = false;
this.owner.visible = false;
this.emitter.fireEvent("enemyDied", {enemy: this.owner})
if (Math.random() < 0.05) {
// give buff maybe
//this.emitter.fireEvent("giveBuff", { position: this.owner.position });
}
}
}
//TODO - need to modify for side view
/*
isPlayerVisible(pos: Vec2): Vec2{
//Check if one player is visible, taking into account walls
// Get the new player location
let start = this.owner.position.clone();
let delta = pos.clone().sub(start);
// Iterate through the tilemap region until we find a collision
let minX = Math.min(start.x, pos.x);
let maxX = Math.max(start.x, pos.x);
let minY = Math.min(start.y, pos.y);
let maxY = Math.max(start.y, pos.y);
// Get the wall tilemap
let walls = <OrthogonalTilemap>this.owner.getScene().getLayer("Wall").getItems()[0];
let minIndex = walls.getColRowAt(new Vec2(minX, minY));
let maxIndex = walls.getColRowAt(new Vec2(maxX, maxY));
let tileSize = walls.getTileSize();
for (let col = minIndex.x; col <= maxIndex.x; col++) {
for (let row = minIndex.y; row <= maxIndex.y; row++) {
if (walls.isTileCollidable(col, row)) {
// Get the position of this tile
let tilePos = new Vec2(col * tileSize.x + tileSize.x / 2, row * tileSize.y + tileSize.y / 2);
// Create a collider for this tile
let collider = new AABB(tilePos, tileSize.scaled(1 / 2));
let hit = collider.intersectSegment(start, delta, Vec2.ZERO);
if (hit !== null && start.distanceSqTo(hit.pos) < start.distanceSqTo(pos)) {
// We hit a wall, we can't see the player
return null;
}
}
}
}
return pos;
}
*/
update(deltaT: number){
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
this.plan = this.planner.plan(Statuses.REACHED_GOAL, this.possibleActions, this.currentStatus, null);
}
*/
}
}
export enum EnemyStates {
DEFAULT = "default",
ALERT = "alert",
PREVIOUS = "previous"
}

View File

@ -0,0 +1,23 @@
import State from "../../../Wolfie2D/DataTypes/State/State";
import GameNode from "../../../Wolfie2D/Nodes/GameNode";
import EnemyAI from "../EnemyAI";
export default abstract class EnemyState extends State {
protected parent: EnemyAI;
protected owner: GameNode;
gravity: number = 1500; //TODO - can change later
constructor(parent: EnemyAI, owner: GameNode){
super(parent);
this.owner = owner;
}
update(deltaT: number): void {
// Do gravity
this.parent.velocity.y += this.gravity*deltaT;
}
}

View File

@ -0,0 +1,27 @@
import GameEvent from "../../../Wolfie2D/Events/GameEvent";
import Input from "../../../Wolfie2D/Input/Input";
import Sprite from "../../../Wolfie2D/Nodes/Sprites/Sprite";
import MathUtils from "../../../Wolfie2D/Utils/MathUtils";
import EnemyState from "./EnemyState";
export default class OnGround extends EnemyState {
onEnter(options: Record<string, any>): void {}
update(deltaT: number): void {
if(this.parent.velocity.y > 0){
this.parent.velocity.y = 0;
}
super.update(deltaT);
this.finished("fall");
}
handleInput(event: GameEvent): void { }
onExit(): Record<string, any> {
return {};
}
}

View File

@ -0,0 +1,71 @@
import Vec2 from "../../../Wolfie2D/DataTypes/Vec2";
import GameEvent from "../../../Wolfie2D/Events/GameEvent";
import GameNode from "../../../Wolfie2D/Nodes/GameNode";
import NavigationPath from "../../../Wolfie2D/Pathfinding/NavigationPath";
import EnemyAI, { EnemyStates } from "../EnemyAI";
import EnemyState from "./EnemyState";
import Sprite from "../../../Wolfie2D/Nodes/Sprites/Sprite";
import MathUtils from "../../../Wolfie2D/Utils/MathUtils";
import OnGround from "./OnGround";
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 {
//this.currentPath = this.getNextPath();
}
handleInput(event: GameEvent): void { }
update(deltaT: number): void {
super.update(deltaT);
//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)){
//turn around
console.log(this.parent.tilemap.getTileAtRowCol(colrow));
this.parent.direction *= -1;
(<Sprite>this.owner).invertX = MathUtils.sign(1) < 0;
console.log("turn around cus wall in front");
}
//check if next ground tile is collidable
else if(this.parent.tilemap.isTileCollidable(colrow.x+this.parent.direction,colrow.y+1)){
//keep moving
//this.velocity.x = this.speed;
console.log("there is floor ahead");
}
else{
//turn around
this.parent.direction *=-1;
(<Sprite>this.owner).invertX = MathUtils.sign(1) < 0;
console.log("turn around cus no floor in front");
}
//move
this.parent.velocity.x = this.parent.direction * this.parent.speed;
//move this elsewhere later
this.owner.move(this.parent.velocity.scaled(deltaT));
//console.log("enemy moving");
}
onExit(): Record<string, any> {
return this.retObj;
}
}

View File

@ -11,12 +11,13 @@ export default class BattleManager {
handleInteraction(attackerType: string, weapon: Weapon) {
//may be unneeded since we are controlling the player -
//we determine enemy collision there
/*
if (attackerType === "player") {
// Check for collisions with enemies
for (let enemy of this.enemies) {
if (weapon.hits(enemy.owner)) {
enemy.damage(weapon.type.damage);
console.log("enemy took dmg");
}
}
} else {
@ -27,14 +28,8 @@ export default class BattleManager {
}
}
}
*/
// Check for collision with player
for (let player of this.players) {
if (weapon.hits(player.owner)) {
player.damage(weapon.type.damage);
}
}
}
setPlayers(player: Array<BattlerAI>): void {
@ -44,4 +39,8 @@ export default class BattleManager {
setEnemies(enemies: Array<BattlerAI>): void {
this.enemies = enemies;
}
addEnemy(enemy : BattlerAI){
this.enemies.push(enemy);
}
}

View File

@ -32,9 +32,13 @@ export enum PlayerStates {
export enum BuffType {
ATK = "attack",
DEF = "defence"
DEF = "defence",
HEALTH = "health",
SPEED = "speed",
RANGE = "range"
}
type Buff = {
"type": BuffType,
"value": number,
@ -45,10 +49,11 @@ type Buffs = [
Buff, Buff, Buff
]
//TODO - discuss max stats during refinement, unused for now
export default class PlayerController extends StateMachineAI implements BattlerAI{
owner: GameNode;
velocity: Vec2 = Vec2.ZERO;
//will need to discuss redundant stats
speed: number = 200;
MIN_SPEED: number = 200;
MAX_SPEED: number = 300;
@ -78,10 +83,11 @@ export default class PlayerController extends StateMachineAI implements BattlerA
inventory: InventoryManager;
CURRENT_BUFFS: {
atk: 0;
hp: 0;
def: 0;
speed: 0;
atk: number; //flat value to add to weapon
hp: number; //flat value
def: number; //falt value
speed: number;
range:number; //range will be a multiplier value: 1.5 = 150% range
}
@ -99,10 +105,36 @@ export default class PlayerController extends StateMachineAI implements BattlerA
* Add given buff to the player
* @param buff Given buff
*/
setBuff(buff: Buff): void {
addBuff(buff: Buff): void {
// TODO
let item = this.inventory.getItem();
switch(buff.type){
case BuffType.HEALTH:
this.CURRENT_BUFFS.hp += buff.value;
this.CURRENT_HP += buff.value;
break;
case BuffType.ATK:
//TODO - may have to modify the weapon dmg value here instead
this.CURRENT_BUFFS.atk += buff.value;
break;
case BuffType.SPEED:
this.CURRENT_BUFFS.speed += buff.value;
break;
case BuffType.DEF:
this.CURRENT_BUFFS.def += buff.value;
break;
case BuffType.RANGE:
this.CURRENT_BUFFS.range += buff.value;
if (item) {
//item.sprite.
}
break;
}
}
//TODO - get the correct tilemap
initializeAI(owner: GameNode, options: Record<string, any>){
this.owner = owner;
@ -114,6 +146,10 @@ export default class PlayerController extends StateMachineAI implements BattlerA
this.inventory = options.inventory;
this.lookDirection = new Vec2();
this.CURRENT_BUFFS = {hp:0, atk:0, def:0, speed:0, range:0};
this.addBuff( {type:BuffType.HEALTH, value:1, bonus:false} );
}
initializePlatformer(): void {

View File

@ -16,12 +16,12 @@ import Color from "../../Wolfie2D/Utils/Color";
import { EaseFunctionType } from "../../Wolfie2D/Utils/EaseFunctions";
import PlayerController from "../Player/PlayerController";
import MainMenu from "./MainMenu";
import { Player_Events } from "../sword_enums";
import { Player_Events, Statuses } from "../sword_enums";
import RegistryManager from "../../Wolfie2D/Registry/RegistryManager";
import WeaponType from "../GameSystems/items/WeaponTypes/WeaponType";
import Weapon from "../GameSystems/items/Weapon";
import BattleManager from "../GameSystems/BattleManager";
//import EnemyAI from "../AI/EnemyAI";
import EnemyAI from "../AI/EnemyAI";
import BattlerAI from "../AI/BattlerAI";
import InventoryManager from "../GameSystems/InventoryManager";
import Item from "../GameSystems/items/Item";
@ -64,6 +64,9 @@ export default class GameLevel extends Scene {
// A list of items in the scene
private items: Array<Item>;
// A list of enemies
private enemies: Array<AnimatedSprite>;
loadScene(): void {
//can load player sprite here
@ -82,6 +85,8 @@ export default class GameLevel extends Scene {
this.load.image("knife", "shattered_sword_assets/sprites/knife.png");
this.load.spritesheet("slice", "shattered_sword_assets/spritesheets/slice.json");
this.load.image("inventorySlot", "shattered_sword_assets/sprites/inventory.png");
this.load.spritesheet("test_dummy","shattered_sword_assets/spritesheets/test_dummy.json")
}
startScene(): void {
@ -95,10 +100,13 @@ export default class GameLevel extends Scene {
// Create the battle manager
this.battleManager = new BattleManager();
// TODO
this.initializeWeapons();
// Initialize the items array - this represents items that are in the game world
this.items = new Array();
// TODO
this.initializeWeapons();
// Initialize the items array - this represents items that are in the game world
this.items = new Array();
// Create an enemies array
this.enemies = new Array();
this.initPlayer();
this.subscribeToEvents();
@ -148,7 +156,7 @@ export default class GameLevel extends Scene {
//update health UI
let playerAI = (<PlayerController>this.player.ai);
this.healthLabel.text = "Player Health: "+ playerAI.CURRENT_HP +'/' + (playerAI.MAX_HP );
this.healthLabel.text = "Player Health: "+ playerAI.CURRENT_HP +'/' + (playerAI.MAX_HP +playerAI.CURRENT_BUFFS.hp );
//handle collisions - may be in battle manager instead
@ -163,7 +171,17 @@ export default class GameLevel extends Scene {
this.playerFalloff(viewportCenter, baseViewportSize);
//TODO - this is for testing
if(Input.isJustPressed("spawn")){
console.log("trying to spawn enemy");
this.addEnemy("test_dummy",this.player.position,{player: this.player,
health :100,
tilemap: "Main",
//actions:actions,
goal: Statuses.REACHED_GOAL,
});
}
}
@ -204,7 +222,7 @@ export default class GameLevel extends Scene {
*/
protected addUI(){
// In-game labels
this.healthLabel = <Label> this.add.uiElement(UIElementType.LABEL, "UI",{position: new Vec2(80, 30), text: "Player Health: "+ (<PlayerController>this.player.ai).CURRENT_HP });
this.healthLabel = <Label> this.add.uiElement(UIElementType.LABEL, "UI",{position: new Vec2(100, 30), text: "Player Health: "+ (<PlayerController>this.player.ai).CURRENT_HP });
this.healthLabel.textColor = Color.WHITE;
this.healthLabel.font = "PixelSimple";
@ -339,7 +357,7 @@ export default class GameLevel extends Scene {
}
this.player.position.copy(this.playerSpawn);
this.player.addPhysics(new AABB(Vec2.ZERO, new Vec2(32, 32))); //sets the collision shape
this.player.colliderOffset.set(0, 2);
this.player.colliderOffset.set(0, 0);
this.player.addAI(PlayerController, {
playerType: "platformer",
tilemap: "Main",
@ -367,14 +385,17 @@ export default class GameLevel extends Scene {
protected addEnemy(spriteKey: string, tilePos: Vec2, aiOptions: Record<string, any>): void {
let enemy = this.add.animatedSprite(spriteKey, "primary");
enemy.position.set(tilePos.x*32, tilePos.y*32);
//enemy.position.set(tilePos.x*32, tilePos.y*32);
enemy.position.copy(tilePos);
enemy.scale.set(2, 2);
enemy.addPhysics();
//enemy.addAI(EnemyAI, aiOptions); //TODO - add individual enemy AI
enemy.addPhysics(new AABB(Vec2.ZERO, new Vec2(16, 25)));
enemy.colliderOffset.set(0, 6);
enemy.addAI(EnemyAI, aiOptions); //TODO - add individual enemy AI
enemy.setGroup("Enemy");
enemy.setTrigger("player",Player_Events.PLAYER_HIT_ENEMY, null);
//add enemy to the enemy array
this.enemies.push(enemy);
this.battleManager.setEnemies(this.enemies.map(enemy => <BattlerAI>enemy._ai));
}

View File

@ -16,3 +16,10 @@ export enum Damage_Type {
ENVIRONMENT_DAMAGE = "EnvironmentDamage",
DOT_DAMAGE = "DOTDamage",
}
export enum Statuses {
IN_RANGE = "IN_RANGE",
LOW_HEALTH = "LOW_HEALTH",
CAN_RETREAT = "CAN_RETREAT",
REACHED_GOAL = "GOAL"
}