From 417315cf0877a2cc0aa8900e4e7eba48daee6243 Mon Sep 17 00:00:00 2001 From: OfficialCHenry Date: Sun, 10 Apr 2022 18:58:09 -0400 Subject: [PATCH] added super simple ai no GOAP yet --- src/shattered_sword/AI/EnemyAI.ts | 196 ++++++++++++++++++ .../AI/EnemyStates/EnemyState.ts | 23 ++ .../AI/EnemyStates/OnGround.ts | 27 +++ src/shattered_sword/AI/EnemyStates/Patrol.ts | 71 +++++++ .../GameSystems/BattleManager.ts | 15 +- .../Player/PlayerController.ts | 52 ++++- src/shattered_sword/Scenes/GameLevel.ts | 53 +++-- src/shattered_sword/sword_enums.ts | 7 + 8 files changed, 412 insertions(+), 32 deletions(-) create mode 100644 src/shattered_sword/AI/EnemyAI.ts create mode 100644 src/shattered_sword/AI/EnemyStates/EnemyState.ts create mode 100644 src/shattered_sword/AI/EnemyStates/OnGround.ts create mode 100644 src/shattered_sword/AI/EnemyStates/Patrol.ts diff --git a/src/shattered_sword/AI/EnemyAI.ts b/src/shattered_sword/AI/EnemyAI.ts new file mode 100644 index 0000000..1e895d5 --- /dev/null +++ b/src/shattered_sword/AI/EnemyAI.ts @@ -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): 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(); + + this.planner = new GoapActionPlanner(); + + //TODO - get correct tilemap + //this.tilemap = this.owner.getScene().getTilemap(options.tilemap) as OrthogonalTilemap; + this.tilemap = this.owner.getScene().getLayer("Wall").getItems()[0]; + //this.tilemap = 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): 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 = 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" +} \ No newline at end of file diff --git a/src/shattered_sword/AI/EnemyStates/EnemyState.ts b/src/shattered_sword/AI/EnemyStates/EnemyState.ts new file mode 100644 index 0000000..0e10f2e --- /dev/null +++ b/src/shattered_sword/AI/EnemyStates/EnemyState.ts @@ -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; + } + +} + diff --git a/src/shattered_sword/AI/EnemyStates/OnGround.ts b/src/shattered_sword/AI/EnemyStates/OnGround.ts new file mode 100644 index 0000000..6fddba2 --- /dev/null +++ b/src/shattered_sword/AI/EnemyStates/OnGround.ts @@ -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): 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 { + return {}; + } +} \ No newline at end of file diff --git a/src/shattered_sword/AI/EnemyStates/Patrol.ts b/src/shattered_sword/AI/EnemyStates/Patrol.ts new file mode 100644 index 0000000..ab40b7f --- /dev/null +++ b/src/shattered_sword/AI/EnemyStates/Patrol.ts @@ -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; + + constructor(parent: EnemyAI, owner: GameNode){ + super(parent, owner); + + } + + onEnter(options: Record): 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; + (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; + (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 { + return this.retObj; + } + + + +} \ No newline at end of file diff --git a/src/shattered_sword/GameSystems/BattleManager.ts b/src/shattered_sword/GameSystems/BattleManager.ts index 906b5b7..acdbd37 100644 --- a/src/shattered_sword/GameSystems/BattleManager.ts +++ b/src/shattered_sword/GameSystems/BattleManager.ts @@ -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): void { @@ -44,4 +39,8 @@ export default class BattleManager { setEnemies(enemies: Array): void { this.enemies = enemies; } + + addEnemy(enemy : BattlerAI){ + this.enemies.push(enemy); + } } \ No newline at end of file diff --git a/src/shattered_sword/Player/PlayerController.ts b/src/shattered_sword/Player/PlayerController.ts index 8bd43ec..8985af4 100644 --- a/src/shattered_sword/Player/PlayerController.ts +++ b/src/shattered_sword/Player/PlayerController.ts @@ -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){ 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 { @@ -167,7 +203,7 @@ export default class PlayerController extends StateMachineAI implements BattlerA } } - + } diff --git a/src/shattered_sword/Scenes/GameLevel.ts b/src/shattered_sword/Scenes/GameLevel.ts index defc666..0406268 100644 --- a/src/shattered_sword/Scenes/GameLevel.ts +++ b/src/shattered_sword/Scenes/GameLevel.ts @@ -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"; @@ -63,6 +63,9 @@ export default class GameLevel extends Scene { // A list of items in the scene private items: Array; + + // A list of enemies + private enemies: Array; 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 = (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,8 +171,18 @@ 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 =