ShatteredSword/src/shattered_sword/Player/PlayerController.ts

561 lines
20 KiB
TypeScript

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 {
ATK = "attack",
DEF = "defence",
HEALTH = "health",
SPEED = "speed",
RANGE = "range",
ATKSPEED = "attackspeed",
DOUBLESTRIKE = "doublestrike",
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",
JUMP = "jump"
}
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<Item>;
// The inventory of the player
inventory: InventoryManager;
static invincibilityTimer: Timer;
static buffPool : Array<BuffCategory> = new Array();
static appliedBuffs: Array<Buff> = new Array();
//add to current_buffs later
hasBleed : Boolean = false;
hasPoison : Boolean = false;
hasBurn : Boolean = false;
hasShield : Boolean = false;
shieldDamage : number = 1;
hasDoubleStrike : Boolean = false;
hasLifesteal : Boolean = false;
lifestealratio : number = 0; //percent of damage to steal
hasOneShot: Boolean = false;
extraDotDmg : number =0;
lives: number = 1;
cooldownMultiplier : number = 1;
//TODO - add new buffs here
CURRENT_BUFFS: {
atk: number; //flat value to add to weapon
hp: number; //flat value
def: number; //flat value
speed: number; //flat value
range:number; //range will be a multiplier value: 1.5 = 150% range
}
//TODO - get the correct tilemap
initializeAI(owner: GameNode, options: Record<string, any>){
this.owner = owner;
this.initializePlatformer();
this.tilemap = this.owner.getScene().getTilemap(options.tilemap) as OrthogonalTilemap;
this.inventory = options.inventory;
this.lookDirection = new Vec2();
this.CURRENT_BUFFS = {hp:0, atk:0, def:0, speed:0, range:0};
//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);
}
//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});
}
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();
(<AnimatedSprite>this.owner).animation.play("ATTACK", true);
//TODO - get proper look direction
this.lookDirection.x = (<Sprite>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);
}
}
}
// 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){
(<EnemyAI>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");
(<AnimatedSprite>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 --;
(<AnimatedSprite>this.owner).animation.play("DYING");
(<AnimatedSprite>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_BUFFS.hp){
this.CURRENT_HP = this.MAX_HP + this.CURRENT_BUFFS.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_BUFF);
}
}
//TODO - balance buff value generation
/**
* returns an array of three randomly generated buffs
* @param val optional value to give buff
* @returns array of three Buffs
*/
generateBuffs( 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
let num = parseFloat(Math.random().toPrecision(1)) * 10; //random number from 1 to 10 if no value given
if(typeof val !== 'undefined'){
num = val;
}
//TODO - implement better buff genertion - some buffs dont want multiple of
let attackBuffs : Buff[] = [
{type:BuffType.RANGE, value:num/10, category: BuffCategory.ATTACK},
{type:BuffType.ATKSPEED, value:num, category: BuffCategory.ATTACK},
];
if(!this.hasDoubleStrike){
attackBuffs.push({type:BuffType.DOUBLESTRIKE, value:num, category: BuffCategory.ATTACK, string:"your attacks are \nfollowed by a \nweaker strike"});
}
let dotBuffs : Buff[] = [
];
if(!this.hasBleed){
dotBuffs.push({type:BuffType.BLEED, value:1, category: BuffCategory.DOT, string: "Your hits \napply Bleed"});
}
if(!this.hasBurn){
dotBuffs.push({type:BuffType.BURN, value:1, category: BuffCategory.DOT, string: "Your hits \napply Burn"});
}
if(!this.hasPoison){
dotBuffs.push({type:BuffType.POISON, value:1, category: BuffCategory.DOT, string: "Your 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: "increase your \nDOT damage"});
}
let shieldBuffs : Buff[] = [
{type:BuffType.HEALTH, value:num, category: BuffCategory.SHIELD},
];
//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: "Gain Shield \nWhen Damaged \n Shields return \nthe damage taken \nto attacker"});
}
else{
shieldBuffs.push({type:BuffType.SHIELD_DMG, value:num, category: BuffCategory.SHIELD, string: "increase damage \nreturned by shield"});
}
let healthBuffs : Buff[] = [
{type:BuffType.DEF, value: num/10, category: BuffCategory.HEALTH, string: "decrease damage by"+num/10+"%"}
];
if(!this.hasLifesteal){
healthBuffs.push({type:BuffType.LIFESTEAL, value:1, category: BuffCategory.HEALTH, string:"Gain lifesteal"});
}
else{
healthBuffs.push({type:BuffType.LIFESTEALBUFF, value:num/10, category: BuffCategory.HEALTH, string:"Increase Lifesteal \nstrength by "+ num+ "%"});
}
let extraBuffs : Buff[] = [
{type:BuffType.EXTRALIFE, value:1, category: BuffCategory.EXTRA, string: "Gain an \nExtra Life"},
{type:BuffType.SPEED, value:num, category: BuffCategory.EXTRA},
{type:BuffType.ATK, value:num, category: BuffCategory.EXTRA}
];
if(!this.hasOneShot){ //only add oneshot buff if it isnt already included
extraBuffs.push({type:BuffType.ONESHOT, value:1, category: BuffCategory.EXTRA, string: "Your 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.RANGE, value:num/10, category: BuffCategory.ATTACK});
}
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: "increase 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: "Gain 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/10, category: BuffCategory.HEALTH, string: "decrease damage\n taken by "+num*10+"%"});
}
else{
selected.push(healthBuffs.pop());
}
break;
case BuffCategory.SHIELD:
shieldBuffs.sort(() => 0.5 - Math.random());
if(shieldBuffs.length ==0 ){
selected.push({type:BuffType.HEALTH, value:num, category: BuffCategory.SHIELD});
}
else{
selected.push(shieldBuffs.pop());
}
break;
}
}
return selected;
}
/**
* Add given buff to the player
* @param buff Given buff
*/
addBuff(buff: Buff): void {
//increase weight of selected buff category
PlayerController.buffPool.push(buff.category);
//add buff to array of applied buffs
PlayerController.appliedBuffs.push(buff);
// 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 - decide what to do with atk stat
this.CURRENT_BUFFS.atk += buff.value;
this.CURRENT_ATK +=buff.value;
break;
case BuffType.SPEED:
this.CURRENT_BUFFS.speed += buff.value;
this.speed += buff.value;
break;
case BuffType.DEF:
this.damage_multiplier *= (1-buff.value);
break;
case BuffType.RANGE:
this.CURRENT_BUFFS.range += buff.value;
if (item) {
(<Weapon>item).EXTRA_RANGE += buff.value;
}
break;
//TODO
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
(<Weapon>item).cooldownTimer = new Timer((<Weapon>item).cooldown * this.cooldownMultiplier )
}
break;
case BuffType.DOUBLESTRIKE:
//TODO -
break;
case BuffType.SHIELD_DMG:
this.shieldDamage += buff.value/10 ;
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;
}
}
/**
*
* @returns record of the player stats
*/
getStats(): Record<string, any>{
let stats = {} as Record<string,any>;
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{
return "";
}
}