LADXHD/InGame/GameObjects/Base/Components/AI/AiDamageState.cs
2023-12-14 17:21:22 -05:00

521 lines
19 KiB
C#

using Microsoft.Xna.Framework;
using ProjectZ.InGame.GameObjects.Base.CObjects;
using ProjectZ.InGame.GameObjects.Things;
using ProjectZ.InGame.Things;
using System;
namespace ProjectZ.InGame.GameObjects.Base.Components.AI
{
class AiDamageState
{
public delegate void OnDeleteTemplate(bool pieceOfPower);
public OnDeleteTemplate OnDeath;
public delegate void OnLiveZero();
public OnLiveZero OnLiveZeroed;
public delegate void OnBurnDelegate();
public OnBurnDelegate OnBurn;
public int ExplosionOffsetY;
public Point FlameOffset;
public string SpawnItem;
public float HitMultiplierX = 5;
public float HitMultiplierY = 5;
public bool IgnoreZeroDamage;
public bool IsActive = true;
public bool MoveBody = true;
public bool UpdateLastStateFire;
public bool DeathAnimation = true;
public bool SpawnItems = true;
private readonly GameObject _gameObject;
private readonly BodyComponent _body;
private readonly AiComponent _aiComponent;
private readonly CSprite _sprite;
public SpriteShader DamageSpriteShader;
private SpriteShader _normalShader;
public AiTriggerCountdown DamageTrigger;
private AiTriggerCountdown _deathCountdown;
private bool _pieceOfPower;
private float _bodyDrag;
private float _bodyDragAir;
private double _pieceOfPowerCounter;
private int _pieceOfPowerDeathCount;
public int CurrentLives;
private bool _damageBlink;
private bool _returnState;
private readonly bool _hasBurnState;
public const int BlinkTime = 66;
public const int CooldownTime = BlinkTime * 6;
private readonly int _cooldownTime;
public int ExplostionWidth = 32;
public int ExplostionHeight = 32;
public bool HasDamageState;
public bool BossHitSound;
private float _deathCount = -1000;
public AiDamageState(GameObject gameObject, BodyComponent body, AiComponent aiComponent, CSprite sprite, int lives, bool hasDamageState = true, bool hasBurnState = true, int cooldownTime = CooldownTime)
{
_gameObject = gameObject;
_body = body;
_aiComponent = aiComponent;
_sprite = sprite;
_normalShader = sprite.SpriteShader;
CurrentLives = lives;
HasDamageState = hasDamageState;
_hasBurnState = hasBurnState;
_cooldownTime = cooldownTime;
// basic on death reaction
OnDeath = BaseOnDeath;
DamageSpriteShader = Resources.DamageSpriteShader0;
_aiComponent.Trigger.Add(DamageTrigger = new AiTriggerCountdown(cooldownTime, DamageTick, FinishDamage));
if (hasDamageState)
_aiComponent.States.Add("damage", new AiState());
var stateKnockBack = new AiState();
stateKnockBack.Trigger.Add(new AiTriggerCountdown(cooldownTime, null, FinishKnockback));
var statePieceOfPower = new AiState(UpdatePieceOfPower) { Init = InitPieceOfPower };
var stateBurning = new AiState(UpdateBurning) { Init = InitBurning };
var stateDamageDeath = new AiState { Init = () => OnDeath(false) };
_aiComponent.Trigger.Add(_deathCountdown = new AiTriggerCountdown(cooldownTime, DeathTick, () => DeathTick(0)));
_aiComponent.States.Add("knockBack", stateKnockBack);
_aiComponent.States.Add("pieceOfPower", statePieceOfPower);
if (hasBurnState)
_aiComponent.States.Add("burning", stateBurning);
_aiComponent.States.Add("damageDeath", stateDamageDeath);
}
public void AddBossDamageState(AiTriggerCountdown.TriggerEndFunction deathAnimationEnd)
{
OnDeath = OnDeathBoss;
var stateDeath = new AiState(UpdateDeath);
stateDeath.Trigger.Add(new AiTriggerCountdown(3000 / BlinkTime * BlinkTime, UpdateBlink, deathAnimationEnd));
_aiComponent.States.Add("deathBoss", stateDeath);
}
public bool IsInDamageState()
{
return DamageTrigger.CurrentTime > 0 ||
_aiComponent.CurrentStateId == "knockBack" ||
_aiComponent.CurrentStateId == "burning" ||
_aiComponent.CurrentStateId == "pieceOfPower";
}
public Values.HitCollision HitKnockBack(GameObject gameObject, Vector2 direction, HitType damageType, bool pieceOfPower, bool blink = true)
{
if (!IsActive || IsInDamageState())
return Values.HitCollision.None;
_aiComponent.ChangeState(pieceOfPower ? "pieceOfPower" : "knockBack");
_damageBlink = blink;
DamageTrigger.OnInit();
if (pieceOfPower)
{
_body.Velocity.X = direction.X * 3;
_body.Velocity.Y = direction.Y * 3;
}
else
{
_body.Velocity.X = direction.X * HitMultiplierX;
_body.Velocity.Y = direction.Y * HitMultiplierY;
}
_returnState = true;
return Values.HitCollision.Enemy;
}
public Values.HitCollision OnHit(GameObject gameObject, Vector2 direction, HitType damageType, int damage, bool pieceOfPower)
{
if (!IsActive || IsInDamageState() || damageType == HitType.PegasusBootsPush)
return Values.HitCollision.None;
// directly delete the gameObject if the attack comes from a bowwow
if (damageType == HitType.BowWow)
{
DeathAnimation = false;
OnDeath(false);
return Values.HitCollision.Enemy;
}
if (damage <= 0 && IgnoreZeroDamage || DamageTrigger.CurrentTime > 0)
return Values.HitCollision.Enemy;
CurrentLives -= damage;
// burn on powder impact
if ((damageType == HitType.MagicPowder || damageType == HitType.MagicRod) && _hasBurnState)
{
if (_aiComponent.CurrentStateId != "burning")
{
_aiComponent.ChangeState("burning");
var speedMultiply = (damageType == HitType.MagicPowder ? 0.125f : 0.5f);
if (MoveBody)
{
_body.Velocity.X = direction.X * HitMultiplierX * speedMultiply;
_body.Velocity.Y = direction.Y * HitMultiplierY * speedMultiply;
}
Game1.GameManager.PlaySoundEffect("D378-18-12");
return Values.HitCollision.Enemy;
}
}
if (_aiComponent.CurrentStateId == "burning")
return Values.HitCollision.None;
// play sound effect
if (!BossHitSound)
{
if (pieceOfPower)
Game1.GameManager.PlaySoundEffect("D370-17-11");
Game1.GameManager.PlaySoundEffect("D360-03-03");
}
else
{
if (CurrentLives <= 0)
Game1.GameManager.PlaySoundEffect("D378-19-13");
else
Game1.GameManager.PlaySoundEffect("D370-07-07");
}
if (pieceOfPower)
_aiComponent.ChangeState("pieceOfPower");
else
{
if (HasDamageState)
{
_returnState = true;
_aiComponent.ChangeState("damage");
}
}
DamageTrigger.OnInit();
_damageBlink = damage > 0;
if (MoveBody)
{
if (pieceOfPower)
{
_body.Velocity.X = direction.X * 3;
_body.Velocity.Y = direction.Y * 3;
}
else
{
_body.Velocity.X = direction.X * HitMultiplierX;
_body.Velocity.Y = direction.Y * HitMultiplierY;
}
}
// trigger death event?
if (CurrentLives <= 0)
{
OnLiveZeroed?.Invoke();
_deathCountdown.OnInit();
}
return Values.HitCollision.Enemy;
}
public void SetDamageState(bool blink = true)
{
_damageBlink = blink;
DamageTrigger.OnInit();
}
private void InitPieceOfPower()
{
_pieceOfPower = true;
_bodyDrag = _body.Drag;
_bodyDragAir = _body.DragAir;
_body.Drag = 1.0f;
_body.DragAir = 1.0f;
_pieceOfPowerDeathCount = 0;
}
private void UpdatePieceOfPower()
{
if (!HasDamageState)
_aiComponent.States[_aiComponent.LastStateId].Update?.Invoke();
// draw a trail
if (_pieceOfPowerCounter <= 0)
{
_pieceOfPowerCounter = 80;
var animation = new ObjAnimator(_gameObject.Map, 0, 0, 0, 0, Values.LayerPlayer, "Particles/pieceOfPowerTrail", "run", true);
animation.EntityPosition.Set(_body.Position.Position +
new Vector2(_body.OffsetX + _body.Width / 2f, _body.OffsetY + _body.Height / 2f));
animation.EntityPosition.Z = _body.Position.Z;
Game1.GameManager.MapManager.CurrentMap.Objects.SpawnObject(animation);
_pieceOfPowerDeathCount++;
}
_pieceOfPowerCounter -= Game1.DeltaTime;
var collision = false;
if ((_body.LastVelocityCollision & Values.BodyCollision.Horizontal) != 0)
_body.Velocity.X = 0;
if ((_body.LastVelocityCollision & Values.BodyCollision.Vertical) != 0)
_body.Velocity.Y = 0;
// glide on the wall depending on the angle the body moved towards the wall
if (((_body.LastVelocityCollision & Values.BodyCollision.Horizontal) != 0 && MathF.Abs(_body.Velocity.X) > MathF.Abs(_body.Velocity.Y)) ||
((_body.LastVelocityCollision & Values.BodyCollision.Vertical) != 0 && MathF.Abs(_body.Velocity.Y) > MathF.Abs(_body.Velocity.X)))
{
collision = true;
}
// last collision
if ((collision && _pieceOfPowerDeathCount > 1) || _pieceOfPowerDeathCount > 5)
{
_pieceOfPower = false;
_body.Drag = _bodyDrag;
_body.DragAir = _bodyDragAir;
if (CurrentLives <= 0)
{
_body.Velocity.X = 0;
_body.Velocity.Y = 0;
OnDeath(true);
}
else
{
_aiComponent.ChangeState(_aiComponent.LastStateId, true);
}
}
}
private void InitBurning()
{
OnBurn?.Invoke();
_body.VelocityTarget = Vector2.Zero;
// spawn explosion effect
var burnAnimator = new ObjAnimator(_gameObject.Map, 0, 0, 0, 0, Values.LayerTop, "Particles/flame", "idle", false);
burnAnimator.EntityPosition.Set(_gameObject.EntityPosition.Position);
// move the animation with the game object
burnAnimator.EntityPosition.SetParent(_gameObject.EntityPosition,
new Vector2((int)(_body.OffsetX + _body.Width / 2) + FlameOffset.X,
(int)(_body.OffsetY + _body.Height) - 8 + FlameOffset.Y));
// remove the burning sprite if the ai state changes (e.g. by falling down a hole)
burnAnimator.Animator.OnFrameChange = () =>
{
// @HACK
burnAnimator.AnimationComponent.UpdateSprite();
if (_aiComponent.Owner.Map == null || _aiComponent.CurrentStateId != "burning")
burnAnimator.Map.Objects.DeleteObjects.Add(burnAnimator);
};
burnAnimator.Animator.OnAnimationFinished = () =>
{
FinishBurning();
burnAnimator.Map.Objects.DeleteObjects.Add(burnAnimator);
};
Game1.GameManager.MapManager.CurrentMap.Objects.SpawnObject(burnAnimator);
}
private void UpdateBurning()
{
if (UpdateLastStateFire)
_aiComponent.States[_aiComponent.LastStateId].Update?.Invoke();
}
private void FinishBurning()
{
OnDeath(false);
}
private void DamageTick(double time)
{
if (_damageBlink)
_sprite.SpriteShader = (_cooldownTime - time) % (BlinkTime * 2) < BlinkTime ? DamageSpriteShader : _normalShader;
}
private void FinishDamage()
{
_sprite.SpriteShader = _normalShader;
if (CurrentLives > 0 &&
_aiComponent.CurrentStateId != "pieceOfPower" &&
_aiComponent.LastStateId != "pieceOfPower" &&
_aiComponent.LastStateId != "knockBack")
{
// go back to the previous state without calling the init methods
if (HasDamageState && _returnState)
{
_returnState = false;
_aiComponent.ChangeState(_aiComponent.LastStateId, true);
}
}
}
private void FinishKnockback()
{
_sprite.SpriteShader = _normalShader;
// go back to the previous state without calling the init methods
_aiComponent.ChangeState(_aiComponent.LastStateId, true);
}
private void DeathTick(double time)
{
// die when the time is over or the velocity of the body is low enough
if (time <= 0 || (time < _cooldownTime - 175 && _body.Velocity.Length() < 0.5f && HitMultiplierX > 0 && HitMultiplierY > 0))
{
if (_pieceOfPower)
{
_body.Drag = _bodyDrag;
_body.DragAir = _bodyDragAir;
}
_deathCountdown.Stop();
OnDeath(false);
}
}
private void UpdateBlink(double time)
{
var blinkTime = BlinkTime;
_sprite.SpriteShader = time % (blinkTime * 2) >= blinkTime ? DamageSpriteShader : _normalShader;
}
private void UpdateDeath()
{
_deathCount += Game1.DeltaTime;
if (_deathCount < 100)
return;
_deathCount -= 100;
Game1.GameManager.PlaySoundEffect("D378-19-13");
var posX = (int)_gameObject.EntityPosition.X - ExplostionWidth / 2 + Game1.RandomNumber.Next(0, ExplostionWidth) - 8;
var posY = (int)_gameObject.EntityPosition.Y - (int)_gameObject.EntityPosition.Z + ExplosionOffsetY - ExplostionHeight + Game1.RandomNumber.Next(0, ExplostionHeight) - 8;
// spawn explosion effect
_gameObject.Map.Objects.SpawnObject(new ObjAnimator(_gameObject.Map, posX, posY, Values.LayerTop, "Particles/spawn", "run", true));
}
public void OnDeathBoss(bool pieceOfPower)
{
Game1.GameManager.PlaySoundEffect("D370-16-10");
IsActive = false;
// start the death animation
_aiComponent.ChangeState("deathBoss");
}
public void BaseOnDeath(bool pieceOfPower)
{
if (_gameObject.Map == null)
return;
_gameObject.Map.Objects.DeleteObjects.Add(_gameObject);
// play sound effect
if (pieceOfPower)
Game1.GameManager.PlaySoundEffect("D370-18-12");
Game1.GameManager.PlaySoundEffect("D378-19-13");
// spawn explosion effect
var bodyCenter = _body.BodyBox.Box.Center;
bodyCenter.Y += ExplosionOffsetY;
if (DeathAnimation)
if (!pieceOfPower)
{
Game1.GameManager.MapManager.CurrentMap.Objects.SpawnObject(
new ObjAnimator(_gameObject.Map, (int)bodyCenter.X - 12, (int)(bodyCenter.Y - _body.Position.Z - 12 - 5),
Values.LayerTop, "Particles/explosion0", "run", true));
}
else
{
var animation = new ObjAnimator(_gameObject.Map, 0, 0, Values.LayerTop, "Particles/pieceOfPowerExplosion", "run", true);
animation.EntityPosition.Set(new Vector2(bodyCenter.X, bodyCenter.Y - _body.Position.Z));
Game1.GameManager.MapManager.CurrentMap.Objects.SpawnObject(animation);
}
if (!SpawnItems)
return;
Game1.GameManager.GuardianAcornCount++;
Game1.GameManager.PieceOfPowerCount++;
// TODO_End reevaluate
// spawn heart or ruby
string strObject = SpawnItem;
if (strObject == null)
{
var random = Game1.RandomNumber.Next(0, 100);
if (random < 33)
strObject = "ruby";
else if (random < 40)
strObject = "heart";
}
if (Game1.GameManager.GuardianAcornCount >= 12)
{
Game1.GameManager.GuardianAcornCount -= 12;
var objItem = new ObjItem(_gameObject.Map, 0, 0, "j", null, "guardianAcorn", null, true);
objItem.EntityPosition.Set(new Vector3(bodyCenter.X, bodyCenter.Y, _body.Position.Z));
_gameObject.Map.Objects.SpawnObject(objItem);
}
// 40 to 45 enemies?
// @TODO: remove
else if (Game1.GameManager.PieceOfPowerCount >= 45)
{
Game1.GameManager.PieceOfPowerCount -= 45;
var objItem = new ObjItem(_gameObject.Map, 0, 0, "j", null, "pieceOfPower", null, true);
objItem.EntityPosition.Set(new Vector3(bodyCenter.X, bodyCenter.Y, _body.Position.Z));
_gameObject.Map.Objects.SpawnObject(objItem);
}
else if (strObject != null)
{
// spawn a heart or a ruby
var objItem = new ObjItem(_gameObject.Map, 0, 0, "j", null, strObject, null, true);
objItem.EntityPosition.Set(new Vector3(bodyCenter.X, bodyCenter.Y, _body.Position.Z));
_gameObject.Map.Objects.SpawnObject(objItem);
}
}
}
}