LADXHD/InGame/GameObjects/Bosses/BossSlimeEel.cs
2023-12-14 17:21:22 -05:00

630 lines
22 KiB
C#

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using ProjectZ.Base;
using ProjectZ.InGame.GameObjects.Base;
using ProjectZ.InGame.GameObjects.Base.CObjects;
using ProjectZ.InGame.GameObjects.Base.Components;
using ProjectZ.InGame.GameObjects.Base.Components.AI;
using ProjectZ.InGame.GameObjects.Things;
using ProjectZ.InGame.Map;
using ProjectZ.InGame.SaveLoad;
using ProjectZ.InGame.Things;
namespace ProjectZ.InGame.GameObjects.Bosses
{
class BossSlimeEel : GameObject
{
private readonly Animator _animator;
private readonly AnimationComponent _animationComponent;
private readonly BodyComponent _body;
private readonly AiComponent _aiComponent;
private readonly CSprite _sprite;
private readonly DamageFieldComponent _damageField;
private readonly AiDamageState _aiDamageState;
private readonly Animator _explosionAnimator;
private readonly BossSlimeEelSpawn _eelSpawner;
// parts are used to deal damage and have a hittable box
private readonly BossSlimeEelTail[] _tailParts = new BossSlimeEelTail[5];
private readonly DictAtlasEntry _spriteTail;
private readonly DictAtlasEntry _spriteTailEnd;
private readonly DictAtlasEntry _spriteHeart0;
private readonly DictAtlasEntry _spriteHeart1;
private readonly Vector2 _centerPosition;
private Vector2 _startPosition;
private Vector2 _hockshotPosition;
private Vector2 _hookshotOffset;
private float _pullSound;
private double _retreatCount;
private float _moveSpeed;
private int _moveDirection = 1;
private bool _attackOut;
private bool _isFullyOut;
private float _moveRotation;
private float _rotationDir = 1;
private const float MoveSpeed = 0.75f;
private readonly float[] _tailDistance = { 10.0f, 8f, 9f };
private readonly Vector2[] _savedPosition = new Vector2[30];
private readonly Vector2[] _tailPositions = new Vector2[3];
private string _saveKey;
private bool _attackSound;
private float _saveInterval = 33;
private float _saveCounter;
private int _saveIndex;
public BossSlimeEel(Map.Map map, Vector2 centerPosition, BossSlimeEelSpawn eelSpawner, string saveKey) : base(map)
{
Tags = Values.GameObjectTag.Enemy;
_centerPosition = centerPosition;
_eelSpawner = eelSpawner;
_saveKey = saveKey;
EntityPosition = new CPosition(centerPosition.X, centerPosition.Y, 0);
EntitySize = new Rectangle(-16, -16, 32, 32);
_body = new BodyComponent(EntityPosition, -8, -8, 16, 16, 8)
{
MoveCollision = OnCollision,
IgnoreHoles = true
};
_spriteTail = Resources.GetSprite("eel_tail_0");
_spriteTailEnd = Resources.GetSprite("eel_tail_2");
_spriteHeart0 = Resources.GetSprite("eel_heart_0");
_spriteHeart1 = Resources.GetSprite("eel_heart_1");
for (var i = 0; i < _tailParts.Length; i++)
{
if (i == 0)
_tailParts[i] = new BossSlimeEelTail(Map, EntityPosition.Position, 0, OnHitHeart);
else
_tailParts[i] = new BossSlimeEelTail(Map, EntityPosition.Position, 0, null);
_tailParts[i].SetActive(false);
Map.Objects.SpawnObject(_tailParts[i]);
}
_explosionAnimator = AnimatorSaveLoad.LoadAnimator("Objects/explosion");
_animator = AnimatorSaveLoad.LoadAnimator("Nightmares/slime eel");
_sprite = new CSprite(EntityPosition);
_animationComponent = new AnimationComponent(_animator, _sprite, Vector2.Zero);
_aiComponent = new AiComponent();
var stateSpawn = new AiState { Init = InitSpawn };
var stateSpawnAttack = new AiState(UpdateSpawnAttack);
var stateHidden = new AiState { Init = InitHidden };
stateHidden.Trigger.Add(new AiTriggerCountdown(1000, null, EndHidden));
var stateAttack = new AiState(UpdateAttack) { Init = InitAttack };
var statePulled = new AiState(UpdatePulled) { Init = InitPulled };
var stateRetreat = new AiState(UpdateRetreat) { Init = InitRetreat };
var stateJumpingOut = new AiState(UpdateJumpingOut) { Init = InitJumpingOut };
var statePulledOut = new AiState(UpdatePulledOut) { Init = InitPulledOut };
statePulledOut.Trigger.Add(new AiTriggerRandomTime(ChangeDirection, 650, 1000));
statePulledOut.Trigger.Add(new AiTriggerCountdown(1600, null, () => _aiComponent.ChangeState("blink")));
var stateBlink = new AiState { Init = InitBlink };
stateBlink.Trigger.Add(new AiTriggerCountdown(AiDamageState.CooldownTime, TickBlink, Explode));
var stateExplode = new AiState(UpdateExplode) { Init = InitExplode };
_aiComponent.States.Add("spawn", stateSpawn);
_aiComponent.States.Add("spawnAttack", stateSpawnAttack);
_aiComponent.States.Add("hidden", stateHidden);
_aiComponent.States.Add("attack", stateAttack);
_aiComponent.States.Add("pulled", statePulled);
_aiComponent.States.Add("retreat", stateRetreat);
_aiComponent.States.Add("jumpingOut", stateJumpingOut);
_aiComponent.States.Add("pulledOut", statePulledOut);
_aiComponent.States.Add("blink", stateBlink);
_aiComponent.States.Add("explode", stateExplode);
_aiDamageState = new AiDamageState(this, _body, _aiComponent, _sprite, 8, false, false)
{
HitMultiplierX = 0,
HitMultiplierY = 0,
ExplosionOffsetY = 16,
BossHitSound = true
};
_aiDamageState.AddBossDamageState(OnDeath);
_aiComponent.ChangeState("spawn");
var damageCollider = new CBox(EntityPosition, -6, -6, 0, 12, 12, 8);
var hittableBox = new CBox(EntityPosition, -6, -6, 0, 12, 12, 8);
AddComponent(AiComponent.Index, _aiComponent);
AddComponent(BodyComponent.Index, _body);
AddComponent(BaseAnimationComponent.Index, _animationComponent);
AddComponent(HittableComponent.Index, new HittableComponent(hittableBox, OnHit));
AddComponent(DrawComponent.Index, new DrawComponent(Draw, Values.LayerBottom, EntityPosition));
AddComponent(DamageFieldComponent.Index, _damageField = new DamageFieldComponent(damageCollider, HitType.Enemy, 2) { IsActive = false });
}
public void SpawnAttack(int positionIndex)
{
SetAttackPosition(positionIndex);
_attackSound = false;
_sprite.IsVisible = true;
_animator.Play("attack_spawn");
_aiComponent.ChangeState("spawnAttack");
}
public void ToSpawned()
{
_aiComponent.ChangeState("hidden");
}
private void InitSpawn()
{
_sprite.IsVisible = false;
}
private void UpdateSpawnAttack()
{
if (!_attackSound && _animator.CurrentFrameIndex == 2)
{
_attackSound = true;
Game1.GameManager.PlaySoundEffect("D370-22-16");
}
if (!_animator.IsPlaying)
{
_aiComponent.ChangeState("spawn");
}
}
private void InitExplode()
{
Game1.GameManager.PlaySoundEffect("D378-12-0C");
_explosionAnimator.Play("idle");
_isFullyOut = false;
_sprite.IsVisible = false;
}
private void UpdateExplode()
{
var collisionRect = _explosionAnimator.CollisionRectangle;
if (collisionRect != Rectangle.Empty)
{
var collisionBox = new Box(
EntityPosition.X + collisionRect.X,
EntityPosition.Y + collisionRect.Y, 0,
collisionRect.Width, collisionRect.Height, 16);
if (collisionBox.Intersects(MapManager.ObjLink._body.BodyBox.Box))
MapManager.ObjLink.HitPlayer(collisionBox, HitType.Bomb, 4);
}
_explosionAnimator.Update();
if (!_explosionAnimator.IsPlaying)
_aiComponent.ChangeState("hidden");
}
private void InitBlink()
{
_damageField.IsActive = false;
_body.VelocityTarget = Vector2.Zero;
}
private void TickBlink(double counter)
{
_sprite.SpriteShader = (AiDamageState.CooldownTime - counter) % (AiDamageState.BlinkTime * 2) <
AiDamageState.BlinkTime ? Resources.DamageSpriteShader0 : null;
}
private void Explode()
{
_aiComponent.ChangeState("explode");
}
private void InitJumpingOut()
{
Game1.GameManager.PlaySoundEffect("D370-22-16");
_animator.Play("head_0");
_body.VelocityTarget = _moveDirection * new Vector2(0, 1) * 1.5f;
_moveRotation = _moveDirection != 1 ? MathF.PI : 0;
_isFullyOut = true;
for (var i = 0; i < _savedPosition.Length; i++)
_savedPosition[i] = _startPosition;
}
private void UpdateJumpingOut()
{
var distance = Math.Abs(_startPosition.Y - EntityPosition.Y);
var tailState = 1 - Math.Clamp(distance / 48, 0, 1);
_eelSpawner.SetTailState(tailState);
if (distance > 48)
_aiComponent.ChangeState("pulledOut");
UpdateTailPositions();
}
private void ChangeDirection()
{
_rotationDir = -_rotationDir;
}
private void OnCollision(Values.BodyCollision collision)
{
_moveRotation += MathF.PI;
}
private void InitPulledOut()
{
}
private void UpdatePulledOut()
{
_moveRotation += _rotationDir * Game1.TimeMultiplier * 0.1f;
var newVelocity = new Vector2(-MathF.Sin(_moveRotation), MathF.Cos(_moveRotation)) * MoveSpeed;
_body.VelocityTarget = newVelocity;
UpdateTailPositions();
while (_moveRotation < MathF.PI * 2)
_moveRotation += MathF.PI * 2;
// update the animation
var animationIndex = (int)((_moveRotation + Math.PI / 8) / (MathF.PI / 4)) % 8;
if (animationIndex % 2 == 0)
{
if (animationIndex == 0 || animationIndex == 4)
_animator.Play("head_0");
else
_animator.Play("head_2");
}
else
{
_animationComponent.MirroredH = animationIndex > 4;
_animationComponent.MirroredV = animationIndex != 1 && animationIndex != 7;
_animator.Play("head_1");
}
}
private void OnDeath()
{
// spawn a heart
var objItem = new ObjItem(Map, (int)EntityPosition.X - 8, (int)EntityPosition.Y - 8, "j", "d5_nHeart", "heartMeterFull", null);
Map.Objects.SpawnObject(objItem);
Game1.GameManager.SaveManager.SetString(_saveKey, "1");
// stop boss music
Game1.GameManager.SetMusic(-1, 2);
DespawnObjects();
}
private void DespawnObjects()
{
// delete the head
Map.Objects.DeleteObjects.Add(this);
// delete the tail
for (var i = 0; i < _tailParts.Length; i++)
Map.Objects.DeleteObjects.Add(_tailParts[i]);
}
private void UpdateTail()
{
var distance = Math.Abs(_startPosition.Y - EntityPosition.Y);
var tailState = 1 - Math.Clamp(distance / 80, 0, 1);
_eelSpawner.SetTailState(tailState);
}
private void InitRetreat()
{
_retreatCount = 0;
_moveSpeed = 1 / 16f;
_animator.Play("head_open");
}
private void UpdateRetreat()
{
_retreatCount += Game1.DeltaTime;
var direction = _startPosition - EntityPosition.Position;
if (_retreatCount > 1500)
{
_moveSpeed = AnimationHelper.MoveToTarget(_moveSpeed, 1, 0.05f * Game1.TimeMultiplier);
}
var moveDistance = _moveSpeed * Game1.TimeMultiplier;
if (direction.Length() > moveDistance)
{
direction.Normalize();
EntityPosition.Offset(direction * moveDistance);
}
else
{
_attackOut = false;
EntityPosition.Set(_startPosition);
_aiComponent.ChangeState("hidden");
}
UpdateTail();
}
private void InitPulled()
{
_damageField.IsActive = false;
_attackOut = true;
_pullSound = 0;
_animator.Play("head_0");
}
private void UpdatePulled()
{
// finished pull
if (!MapManager.ObjLink.Hookshot.IsMoving)
{
FinishPull();
}
UpdateTail();
_pullSound += Game1.DeltaTime;
if (_pullSound > 75)
{
_pullSound -= 75;
Game1.GameManager.PlaySoundEffect("D360-41-29");
}
}
private void FinishPull()
{
MapManager.ObjLink.Hookshot.HookshotPosition.PositionChangedDict.Remove(typeof(BossSlimeEel));
_aiComponent.ChangeState("retreat");
}
private void InitAttack()
{
SetAttackPosition(Game1.RandomNumber.Next(0, 4));
// activate damage
for (var i = 0; i < _tailParts.Length; i++)
_tailParts[i].SetActive(true);
_damageField.IsActive = true;
_sprite.IsVisible = true;
_animator.Play("attack");
}
private void SetAttackPosition(int index)
{
_moveDirection = index < 2 ? 1 : -1;
var spawnPosition = new Vector2(
_centerPosition.X + (index % 2 == 0 ? -32 : 32),
_centerPosition.Y + (index < 2 ? -56 : 56));
_animationComponent.MirroredV = index >= 2;
_animationComponent.MirroredH = index >= 2;
EntityPosition.Set(spawnPosition);
_startPosition = spawnPosition;
}
private void UpdateAttack()
{
if (!_attackSound && _animator.CurrentFrameIndex == 3)
{
_attackSound = true;
Game1.GameManager.PlaySoundEffect("D370-22-16");
}
// finished attacking?
if (!_animator.IsPlaying)
{
_aiComponent.ChangeState("hidden");
}
}
private void InitHidden()
{
_sprite.IsVisible = false;
_eelSpawner.SetTailIsMoving(true);
}
private void EndHidden()
{
_attackSound = false;
_aiComponent.ChangeState("attack");
}
private void Draw(SpriteBatch spriteBatch)
{
// change the draw effect
if (_sprite.SpriteShader != null)
{
spriteBatch.End();
ObjectManager.SpriteBatchBegin(spriteBatch, _sprite.SpriteShader);
}
// draw the tail
if (_attackOut)
for (var i = 0; i < 5; i++)
{
var centerPosition = new Vector2(EntityPosition.X, EntityPosition.Y + (14 + i * 14) * -_moveDirection);
var position = new Vector2(centerPosition.X - 8, centerPosition.Y - 8);
_tailParts[i].EntityPosition.Set(centerPosition);
if (_moveDirection == 1 && position.Y + 16 < _startPosition.Y ||
_moveDirection == -1 && position.Y > _startPosition.Y)
continue;
var sprite = _spriteTail;
// draw the part with the heart
if (i == 0)
sprite = Game1.TotalGameTime % (32000 / 60f) < (16000 / 60f) ? _spriteHeart0 : _spriteHeart1;
DrawHelper.DrawNormalized(spriteBatch, sprite, position, Color.White);
}
// draw the tail while moving around outside
if (_isFullyOut)
{
for (var i = _tailPositions.Length - 1; i >= 0; i--)
{
var sprite = i == 2 ? _spriteTailEnd : _spriteTail;
var position = new Vector2(_tailPositions[i].X - 8, _tailPositions[i].Y - 8);
DrawHelper.DrawNormalized(spriteBatch, sprite, position, Color.White);
}
}
// draw the head
_sprite.Draw(spriteBatch);
// change the draw effect
if (_sprite.SpriteShader != null)
{
spriteBatch.End();
ObjectManager.SpriteBatchBegin(spriteBatch, null);
}
// draw the explosion
if (_explosionAnimator.IsPlaying)
_explosionAnimator.Draw(spriteBatch, EntityPosition.Position, Color.White);
}
public Values.HitCollision OnHitHeart(GameObject originObject, Vector2 direction, HitType type, int damage, bool pieceOfPower)
{
// can not get attacked while not visible
if (!_attackOut || (type & HitType.Sword) == 0)
return Values.HitCollision.None;
var wasAlive = _aiDamageState.CurrentLives > 0;
var hitReturn = _aiDamageState.OnHit(originObject, direction, type, damage, false);
if (_aiDamageState.CurrentLives <= 0 && wasAlive)
{
Game1.GameManager.StartDialogPath("slime_eel_1");
_eelSpawner.ToDespawn();
}
return hitReturn;
}
private Values.HitCollision OnHit(GameObject originObject, Vector2 direction, HitType type, int damage, bool pieceOfPower)
{
// can get pulled out by the hookshot at animation frame 1, 2 and 3
if (_aiComponent.CurrentStateId == "attack" && 0 < _animator.CurrentFrameIndex && _animator.CurrentFrameIndex < 4 && type == HitType.Hookshot)
{
_hockshotPosition = MapManager.ObjLink.Hookshot.HookshotPosition.Position;
_hookshotOffset = EntityPosition.Position - _hockshotPosition;
//TODO: not sure what the real condition for this is
if (_aiDamageState.CurrentLives <= 4 && Game1.RandomNumber.Next(0, 10) < 5)
_aiComponent.ChangeState("jumpingOut");
else
{
MapManager.ObjLink.Hookshot.HookshotPosition.AddPositionListener(typeof(BossSlimeEel), OnPositionChange);
_aiComponent.ChangeState("pulled");
}
_eelSpawner.SetTailIsMoving(false);
_eelSpawner.ChangeRotation();
return Values.HitCollision.Blocking;
}
if (_aiComponent.CurrentStateId == "attack")
return Values.HitCollision.RepellingParticle;
if (!_isFullyOut && !_attackOut)
return Values.HitCollision.None;
return Values.HitCollision.RepellingParticle;
}
private void OnPositionChange(CPosition position)
{
// head gets pulled out of the hole
var newPosition = position.Position + _hookshotOffset;
EntityPosition.Set(newPosition);
}
private void UpdateTailPositions()
{
SavePosition();
var indexCount = _saveIndex + (_saveCounter / _saveInterval);
var timeDiff = _saveCounter + _saveInterval * 1000;
for (var i = 0; i < _tailPositions.Length; i++)
{
indexCount -= _tailDistance[i];
if (indexCount < 0)
indexCount += _savedPosition.Length;
timeDiff -= _tailDistance[i] * _saveInterval;
var index = (int)indexCount;
_tailPositions[i] = Vector2.Lerp(
_savedPosition[index], _savedPosition[(index + 1) % _savedPosition.Length],
(timeDiff % _saveInterval) / _saveInterval);
}
}
// TODO: make this better; does not work correctly
private void SavePosition()
{
var position = EntityPosition.Position + _body.VelocityTarget * (Game1.DeltaTime / 16.6667f);
_saveCounter += Game1.DeltaTime;
var diff = _saveCounter % _saveInterval;
var updateSteps = (int)(_saveCounter / _saveInterval);
_saveIndex = (_saveIndex + updateSteps) % _savedPosition.Length;
var index = _saveIndex;
var currentDirection = _moveRotation;
while (_saveCounter >= _saveInterval)
{
_saveCounter -= _saveInterval;
index--;
if (index < 0)
index = _savedPosition.Length - 1;
var vecDir = new Vector2((float)Math.Sin(currentDirection), (float)Math.Cos(currentDirection));
_savedPosition[index] = position - vecDir * (diff / 16.6667f);
position = _savedPosition[index];
diff = _saveInterval;
currentDirection -= _moveDirection * 0.025f * (_saveInterval / 16.6667f);
}
}
}
}