import StateMachineAI from "../../Wolfie2D/AI/StateMachineAI"; import GameNode, { TweenableProperties } from "../../Wolfie2D/Nodes/GameNode"; import Vec2 from "../../Wolfie2D/DataTypes/Vec2"; import Sprite from "../../Wolfie2D/Nodes/Sprites/Sprite"; import OrthogonalTilemap from "../../Wolfie2D/Nodes/Tilemaps/OrthogonalTilemap"; import { Player_Events } from "../sword_enums"; import Fall from "./PlayerStates/Fall"; import Idle from "./PlayerStates/Idle"; import InAir from "./PlayerStates/InAir"; import Jump from "./PlayerStates/Jump"; import Walk from "./PlayerStates/Walk"; import Debug from "../../Wolfie2D/Debug/Debug"; import Item from "../GameSystems/items/Item"; import InventoryManager from "../GameSystems/InventoryManager"; import BattlerAI from "../AI/BattlerAI"; import MathUtils from "../../Wolfie2D/Utils/MathUtils"; import Weapon from "../GameSystems/items/Weapon"; import AnimatedSprite from "../../Wolfie2D/Nodes/Sprites/AnimatedSprite"; import InputWrapper from "../Tools/InputWrapper"; import EnemyAI from "../AI/EnemyAI"; import Timer from "../../Wolfie2D/Timing/Timer"; import PlayerState from "./PlayerStates/PlayerState"; import { GameEventType } from "../../Wolfie2D/Events/GameEventType"; export enum PlayerType { PLATFORMER = "platformer", TOPDOWN = "topdown" } export enum PlayerStates { IDLE = "idle", WALK = "walk", JUMP = "jump", FALL = "fall", PREVIOUS = "previous" } export enum BuffType { FLAT_ATK = "attack", PERCENT_ATK = "percent_attack", DEF = "defence", FLAT_HEALTH = "health", PERCENT_HEALTH = "percent_health", SPEED = "speed", RANGE = "range", ATKSPEED = "attackspeed", POISON = "poison", BLEED = "bleed", BURN = "burn", EXTRA_DOT = "extradot", SHIELD = "shield", SHIELD_DMG = "shielddmg", //increase shield dmg ratio LIFESTEAL = "lifesteal", LIFESTEALBUFF = "lifestealbuff", EXTRALIFE= "extralife", ONESHOT = "oneshot", FULLHPBONUSDMG = "fullhpbonusdmg" } export class Buff { "type": BuffType; "value": number; //"bonus": boolean, //need to determine what bonus gives "string"? : string; "category" : BuffCategory } //TODO - need better names export enum BuffCategory{ ATTACK = "ATTACK", DOT = "DOT", SHIELD = "SHIELD", HEALTH = "HEALTH", EXTRA = "EXTRA" } //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; BASE_HP: number = 100; MAX_HP: number = 100; CURRENT_HP: number = 100; BASE_ATK: number = 100; CURRENT_ATK: number = 100; damage_multiplier: number = 1; CURRENT_EXP : number = 0; MAX_EXP : number = 100; CURRENT_SHIELD : number =0; MAX_SHIELD : number = 20; invincible : boolean = false; level : number = 1; godMode: boolean = false; tilemap: OrthogonalTilemap; //for doublejumps maybe = # of jumps in air allowed MAX_airjumps: number = 1; airjumps:number = 0; private lookDirection: Vec2; /** A list of items in the game world */ private items: Array; // The inventory of the player inventory: InventoryManager; static invincibilityTimer: Timer; static buffPool : Array = new Array(); static appliedBuffs: Array = new Array(); //add to current_buffs later hasBleed : Boolean = false; hasPoison : Boolean = false; hasBurn : Boolean = false; hasShield : Boolean = false; shieldDamage : number = 1; hasLifesteal : Boolean = false; lifestealratio : number = 0; //percent of damage to steal hasOneShot: Boolean = false; extraDotDmg : number =0; lives: number = 1; cooldownMultiplier : number = 1; fullHpBonus: Boolean = false; poisonTimer : Timer; poisonCounter : number = 0; burnTimer : Timer ; burnCounter : number =0; bleedTimer : Timer; bleedCounter :number = 0; static enemiesKilled : number =0; //TODO - get the correct tilemap initializeAI(owner: GameNode, options: Record){ this.owner = owner; this.initializePlatformer(); this.tilemap = this.owner.getScene().getTilemap(options.tilemap) as OrthogonalTilemap; this.inventory = options.inventory; this.lookDirection = new Vec2(); //i frame timer PlayerController.invincibilityTimer = new Timer(2000); //initialize the buff pool - each has same weight at first PlayerController.buffPool = new Array(); for( let i=0 ; i< 4; i++){ PlayerController.buffPool.push(BuffCategory.ATTACK); PlayerController.buffPool.push(BuffCategory.EXTRA); PlayerController.buffPool.push(BuffCategory.DOT); PlayerController.buffPool.push(BuffCategory.SHIELD); PlayerController.buffPool.push(BuffCategory.HEALTH); } //initialize dot timers this.burnTimer = new Timer(1000); this.bleedTimer = new Timer(1000); this.poisonTimer = new Timer(1000); //to test the buffs //this.addBuff( {type:BuffType.HEALTH, value:1} ); //this.addBuff({type:BuffType.BURN, value:1, category:BuffCategory.DOT}); //this.addBuff({type:BuffType.BLEED, value:1, category:BuffCategory.DOT}); //this.addBuff({type:BuffType.POISON, value:1, category:BuffCategory.DOT}); if(PlayerController.appliedBuffs.length != 0){ PlayerController.appliedBuffs.forEach(buff => this.addBuff(buff,true)); } } initializePlatformer(): void { this.speed = 400; let idle = new Idle(this, this.owner); this.addState(PlayerStates.IDLE, idle); let walk = new Walk(this, this.owner); this.addState(PlayerStates.WALK, walk); let jump = new Jump(this, this.owner); this.addState(PlayerStates.JUMP, jump); let fall = new Fall(this, this.owner); this.addState(PlayerStates.FALL, fall); this.initialize(PlayerStates.IDLE); } changeState(stateName: string): void { // If we jump or fall, push the state so we can go back to our current state later // unless we're going from jump to fall or something if((stateName === PlayerStates.JUMP || stateName === PlayerStates.FALL) && !(this.stack.peek() instanceof InAir)){ this.stack.push(this.stateMap.get(stateName)); } super.changeState(stateName); } update(deltaT: number): void { super.update(deltaT); if(PlayerController.invincibilityTimer.isStopped()){ this.invincible = false; } if(this.currentState instanceof Jump){ Debug.log("playerstate", "Player State: Jump"); } else if (this.currentState instanceof Walk){ Debug.log("playerstate", "Player State: Walk"); } else if (this.currentState instanceof Idle){ Debug.log("playerstate", "Player State: Idle"); } else if(this.currentState instanceof Fall){ Debug.log("playerstate", "Player State: Fall"); } Debug.log("player speed", "player speed: x: " + this.velocity.x + ", y:" + this.velocity.y); Debug.log("player Coords:", "Player Coords:" +this.owner.position ); //testing the attacks here, may be moved to another place later if(InputWrapper.isAttackJustPressed()){ let item = this.inventory.getItem(); (this.owner).animation.play("ATTACK", true); //TODO - get proper look direction this.lookDirection.x = (this.owner).invertX ? -1 : 1; // If there is an item in the current slot, use it if (item) { item.use(this.owner, "player", this.lookDirection); //this.emitter.fireEvent(GameEventType.PLAY_SOUND, {key: "sword", loop: false, holdReference: false}); } } //check dot effects if(this.burnTimer.isStopped() && this.burnCounter >0){ this.burnCounter --; this.burnTimer.start(); this.damage(5); } if(this.poisonTimer.isStopped() && this.poisonCounter >0){ this.poisonCounter --; this.poisonTimer.start(); this.damage( Math.round(this.CURRENT_HP/33) ); } if(this.bleedTimer.isStopped() && this.bleedCounter >0){ this.bleedCounter --; this.bleedTimer.start(); this.damage( 2 + Math.round(this.CURRENT_HP/50) ); } } // TODO - figure out attacker damage(damage: number, attacker?: GameNode): void { if (this.godMode) { //console.log("godmode"); return; } if( !this.invincible && PlayerState.dashTimer.isStopped()){ //console.log("take damage"); //i frame here PlayerController.invincibilityTimer.start(); this.invincible = true; //shield absorbs the damage and sends dmg back to attacker if(this.CURRENT_SHIELD > 0){ let newshield = Math.max(0, this.CURRENT_SHIELD - damage ); //calculate the new shield value if( attacker !== undefined){ (attacker._ai).damage((this.CURRENT_SHIELD - newshield) * this.shieldDamage); //damage the attacker the dmg taken to shield } this.CURRENT_SHIELD = newshield; //update shield value } else{ //i frame here PlayerController.invincibilityTimer.start(); this.invincible = true; //console.log("hurt anim"); (this.owner).animation.play("HURT" ); damage *= this.damage_multiplier; damage = parseFloat(damage.toPrecision(2)); this.CURRENT_HP -= damage; this.emitter.fireEvent(GameEventType.PLAY_SOUND, {key: "hurt", loop: false, holdReference: false}); //if player has shield buff give them shield when damaged if(this.hasShield){ this.CURRENT_SHIELD += damage * .5; } } } else{ //console.log("player is invincible"); } if(this.CURRENT_HP <= 0){ this.lives --; (this.owner).animation.play("DYING"); (this.owner).animation.queue("DEAD", true, Player_Events.PLAYER_KILLED); this.emitter.fireEvent(Player_Events.PLAYER_KILLED); } } /** * gives the player a certain amount of shield * @param shield amount of shield to add to player */ addShield(shield : number){ this.CURRENT_SHIELD = (this.CURRENT_SHIELD + shield) % this.MAX_SHIELD; } /** * gives health to the player * @param health health to give player */ addHealth(health : number){ this.CURRENT_HP += health; if(this.CURRENT_HP > this.MAX_HP ){ this.CURRENT_HP = this.MAX_HP ; } } /** * gives the player exp * @param exp amount of exp to give the player */ giveExp(exp: number){ this.CURRENT_EXP += exp; //if > than max exp level up (give buff) if(this.CURRENT_EXP >= this.MAX_EXP){ this.CURRENT_EXP -= this.MAX_EXP; this.MAX_EXP += 50; //increase max exp needed for level up this.level++ ; this.emitter.fireEvent(GameEventType.PLAY_SOUND, {key: "level_up", loop: false, holdReference: false}); this.emitter.fireEvent(Player_Events.GIVE_REGULAR_BUFF); } } /** * generates an array of regular buffs * @param val optional value to give buff * @returns array of three buffs */ generateRegularBuffs( val? : number) : Buff[]{ //random number from 5 to 15 if no value given let num = Math.floor(Math.random() *10) +5; num = Math.round(num); if(typeof val !== 'undefined'){ num = val; } let buffs = new Array(); buffs.push({type:BuffType.FLAT_ATK, value:num, category: BuffCategory.EXTRA}, {type:BuffType.SPEED, value:num, category: BuffCategory.EXTRA}, {type:BuffType.FLAT_HEALTH, value:num, category: BuffCategory.SHIELD}, {type:BuffType.RANGE, value:num/100, category: BuffCategory.ATTACK, string: "\n\nIncrease range \nby "+num+"%"}, {type:BuffType.ATKSPEED, value:num, category: BuffCategory.ATTACK}, ); //shuffle pool of buffs buffs.sort(() => 0.5 - Math.random()); // Get sub-array of first 3 elements after shuffled let selected = buffs.slice(0, 3); //3 buff categories return selected; } /** * generates an array of special buffs * @param val optional value to give the buff * @returns array of 3 Buffs */ generateSpecialBuffs( val? : number) : Buff[]{ //shuffle pool of buff categories PlayerController.buffPool.sort(() => 0.5 - Math.random()); // Get sub-array of first 3 elements after shuffled let shuffled = PlayerController.buffPool.slice(0, 3); //3 buff categories //random number from 5 to 15 if no value given let num = Math.floor(Math.random() *10) +5; num = Math.round(num); if(typeof val !== 'undefined'){ num = val; } //TODO - implement better buff genertion - some buffs dont want multiple of let attackBuffs : Buff[] = [ {type:BuffType.PERCENT_ATK, value:num/100, category: BuffCategory.ATTACK, string:"\n\nIncrease Attack \nby "+num+"%"} ]; let dotBuffs : Buff[] = [ ]; if(!this.hasBleed){ dotBuffs.push({type:BuffType.BLEED, value:1, category: BuffCategory.DOT, string: "\n\nYour hits \napply Bleed"}); } if(!this.hasBurn){ dotBuffs.push({type:BuffType.BURN, value:1, category: BuffCategory.DOT, string: "\n\nYour hits \napply Burn"}); } if(!this.hasPoison){ dotBuffs.push({type:BuffType.POISON, value:1, category: BuffCategory.DOT, string: "\n\nYour hits \napply poison"}); } //only add extra dot if at least one dot is acquired for(let i=dotBuffs.length; i< 3 ; i++){ dotBuffs.push({type:BuffType.EXTRA_DOT, value:num, category: BuffCategory.DOT, string: "\n\nIncrease your \nDOT damage"}); } let shieldBuffs : Buff[] = [ {type:BuffType.PERCENT_HEALTH, value:num/100, category: BuffCategory.SHIELD, string: "\n\nIncrease max hp \nby "+num+"%"}, ]; //if player doesnt have shield buff, give them the option, otherwise give buff shield option if(!this.hasShield){ shieldBuffs.push({type:BuffType.SHIELD, value:1, category: BuffCategory.SHIELD, string: "\n\nGain Shield \nWhen Damaged \n Shields return \nthe damage taken \nto attacker"}); } else{ shieldBuffs.push({type:BuffType.SHIELD_DMG, value:num, category: BuffCategory.SHIELD, string: "\n\nIncrease damage \nreturned by shield"}); } let healthBuffs : Buff[] = [ {type:BuffType.DEF, value: num/100, category: BuffCategory.HEALTH, string: "\n\nDecrease damage \ntaken by "+num+"%"} ]; if(!this.fullHpBonus){ healthBuffs.push({type:BuffType.FULLHPBONUSDMG, value:1, category:BuffCategory.HEALTH, string:"\n\nDeal 10x damage \n when at full HP"}) } if(!this.hasLifesteal){ healthBuffs.push({type:BuffType.LIFESTEAL, value:1, category: BuffCategory.HEALTH, string:"\n\nGain lifesteal"}); } else{ healthBuffs.push({type:BuffType.LIFESTEALBUFF, value:num/100, category: BuffCategory.HEALTH, string:"\n\nIncrease Lifesteal \nstrength by "+ num+ "%"}); } let extraBuffs : Buff[] = [ {type:BuffType.EXTRALIFE, value:1, category: BuffCategory.EXTRA, string: "\n\nGain an \nExtra Life"}, ]; if(!this.hasOneShot){ //only add oneshot buff if it isnt already included extraBuffs.push({type:BuffType.ONESHOT, value:1, category: BuffCategory.EXTRA, string: "\n\nYour hits hurt \n100x more but \nyour max health \nis set to 1 "}); }; let selected = new Array(); while( shuffled.length != 0){ let cat = shuffled.pop(); switch(cat){ case BuffCategory.ATTACK: attackBuffs.sort(() => 0.5 - Math.random()); if(attackBuffs.length == 0){ selected.push({type:BuffType.PERCENT_HEALTH, value:num/100, category: BuffCategory.ATTACK, string: "\n\nIncrease Attack \nby "+num+"%"}); } else{ selected.push(attackBuffs.pop()); } break; case BuffCategory.DOT: dotBuffs.sort(() => 0.5 - Math.random()); if(dotBuffs.length == 0){ selected.push({type:BuffType.EXTRA_DOT, value:num, category: BuffCategory.DOT, string: "\n\nIncrease your \nDOT damage"}); } else{ selected.push(dotBuffs.pop()); } break; case BuffCategory.EXTRA: extraBuffs.sort(() => 0.5 - Math.random()); if(extraBuffs.length ==0 ){ selected.push({type:BuffType.EXTRALIFE, value:1, category: BuffCategory.EXTRA, string: "\n\nGain an \nExtra Life"}); } else{ selected.push(extraBuffs.pop()); } break; case BuffCategory.HEALTH: healthBuffs.sort(() => 0.5 - Math.random()); if(healthBuffs.length == 0){ selected.push({type:BuffType.DEF, value: num/100, category: BuffCategory.HEALTH, string: "\n\nDecrease damage\n taken by "+num+"%"}); } else{ selected.push(healthBuffs.pop()); } break; case BuffCategory.SHIELD: shieldBuffs.sort(() => 0.5 - Math.random()); if(shieldBuffs.length ==0 ){ selected.push({type:BuffType.FLAT_HEALTH, value:num, category: BuffCategory.SHIELD}); } else{ selected.push(shieldBuffs.pop()); } break; } } return selected; } /** * Add given buff to the player * @param buff Given buff * @param init whether or not this is being used during the initialization of the player */ addBuff(buff: Buff, init? :Boolean ): void { //add buff to array of applied buffs if not being used to init if(init === undefined){ //increase weight of selected buff category PlayerController.buffPool.push(buff.category); PlayerController.appliedBuffs.push(buff); } else if (!init){ //increase weight of selected buff category PlayerController.buffPool.push(buff.category); PlayerController.appliedBuffs.push(buff); } // TODO let item = this.inventory.getItem(); switch(buff.type){ case BuffType.FLAT_HEALTH: //this.CURRENT_BUFFS.hp += buff.value; this.CURRENT_HP += buff.value; this.MAX_HP += buff.value; break; case BuffType.PERCENT_HEALTH: this.CURRENT_HP *= (1+buff.value); this.MAX_HP *= (1+buff.value) ; this.CURRENT_HP = Math.round(this.CURRENT_HP); this.MAX_HP = Math.round(this.MAX_HP); break; case BuffType.FLAT_ATK: this.CURRENT_ATK +=buff.value; break; case BuffType.PERCENT_ATK: this.CURRENT_ATK *=buff.value; this.CURRENT_ATK = Math.round(this.CURRENT_ATK); break; case BuffType.SPEED: this.speed += buff.value; break; case BuffType.DEF: this.damage_multiplier *= (1-buff.value); break; case BuffType.RANGE: if (item) { (item).EXTRA_RANGE += buff.value; } break; case BuffType.BLEED: this.hasBleed = true; break; case BuffType.BURN: this.hasBurn = true; break; case BuffType.POISON: this.hasPoison = true; break; case BuffType.EXTRA_DOT: this.extraDotDmg += buff.value; break; case BuffType.SHIELD: this.hasShield = true; break; case BuffType.ATKSPEED: if (item) { this.cooldownMultiplier -= buff.value; //reduce cooldowntimer (item).cooldownTimer = new Timer((item).cooldown * this.cooldownMultiplier ) } break; case BuffType.SHIELD_DMG: this.shieldDamage += buff.value ; break; case BuffType.EXTRALIFE: this.lives ++; break; case BuffType.LIFESTEAL: this.hasLifesteal = true; this.lifestealratio = .2; //20% lifesteal break; case BuffType.LIFESTEALBUFF: this.lifestealratio += buff.value; break; case BuffType.ONESHOT: this.MAX_HP = 1; this.CURRENT_HP = 1; this.CURRENT_ATK *= 100; break; case BuffType.FULLHPBONUSDMG: this.fullHpBonus = true; break; } } /** * * @returns record of the player stats */ getStats(): Record{ let stats = {} as Record; stats.CURRENT_HP = this.CURRENT_HP; stats.CURRENT_ATK = this.CURRENT_ATK; stats.CURRENT_SHIELD = this.CURRENT_SHIELD; stats.CURRENT_EXP = this.CURRENT_EXP; return } toString(): string{ let s = "HP: " + this.CURRENT_HP +"/"+this.MAX_HP+ "\nATK: " + this.CURRENT_ATK + "\nSpeed: " + this.speed + "\nDamage Ratio: " + this.damage_multiplier + "\nEnemies Killed: " + PlayerController.enemiesKilled + "\nSword Effects:" + (this.hasBleed ? "\n Bleed," : " ") + (this.hasBurn ? " Burn," : "") + (this.hasPoison ? " Poison," : "") + (this.hasShield ? " Shield," :"") + (this.fullHpBonus ? " FullHealth deal x10 Dmg" : "") + (this.hasLifesteal ? " Lifesteal," : "") + (this.hasOneShot ? " Atk x 100, maxhp set to 1" : "") ; return s; } }