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

492 lines
18 KiB
C#

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
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 BossGenie : GameObject
{
private readonly BodyComponent _body;
private readonly AiComponent _aiComponent;
private readonly DamageFieldComponent _damageField;
private readonly AiDamageState _damageState;
private readonly ShadowBodyDrawComponent _shadowComponent;
private readonly BossGenieBottle _objBottle;
private readonly Animator _smokeBottom;
private readonly Animator _smokeTop;
private readonly Animator _bodyAnimator;
private readonly Animator _tailAnimator;
private readonly CSprite _sprite;
private readonly string _saveKey;
private const float FollowSpeed = 1.5f;
private const int AttackTime = 10000;
private const int RotateTime = 2100;
private const int Lives = 8;
private Vector2 _spawnPosition;
private readonly Vector2 _roomCenter;
private BossGenieFireball[] _fireballs;
private int _fireballCount;
private int _fireballIndex;
private bool _isVisible;
private const int RotationOffsetY = 38;
private float _currentRotation;
private float _rotationDistance;
private Vector2 _smokePosition0;
private Vector2 _smokePosition1;
private float _spawnCounter;
private bool _drawSmoke;
private bool _attackMode;
public BossGenie(Map.Map map, string saveKey, Vector3 position, BossGenieBottle objBottle) : base(map)
{
_saveKey = saveKey;
_objBottle = objBottle;
Tags = Values.GameObjectTag.Enemy;
EntityPosition = new CPosition(position.X, position.Y, position.Z);
EntitySize = new Rectangle(-20, -80, 40, 80);
_roomCenter = Map.GetRoomCenter(position.X, position.Y);
_bodyAnimator = AnimatorSaveLoad.LoadAnimator("Nightmares/genie");
_bodyAnimator.Play("idle");
_tailAnimator = AnimatorSaveLoad.LoadAnimator("Nightmares/genie");
_tailAnimator.Play("tail");
_smokeTop = AnimatorSaveLoad.LoadAnimator("Nightmares/genie smoke");
_smokeBottom = AnimatorSaveLoad.LoadAnimator("Nightmares/genie smoke");
_sprite = new CSprite(EntityPosition);
_body = new BodyComponent(EntityPosition, -5, -10, 10, 10, 8)
{
IgnoresZ = true,
CollisionTypes = Values.CollisionTypes.None
};
var hittableBox = new CBox(EntityPosition, -15, -38, 0, 30, 26, 8, true);
var damageCollider = new CBox(EntityPosition, -15, -38, 0, 30, 30, 8, true);
var stateIdle = new AiState();
var stateSpawn = new AiState(UpdateSpawn) { Init = InitSpawn };
var stateSpawnDelay = new AiState();
stateSpawnDelay.Trigger.Add(new AiTriggerCountdown(1000, null, SpawnDelayEnd));
var stateDespawn = new AiState(UpdateDespawn) { Init = InitDespawn };
var stateAttack = new AiState { Init = InitAttack };
stateAttack.Trigger.Add(new AiTriggerCountdown(AttackTime, AttackTick, AttackEnd));
var stateFollow = new AiState(UpdateFollow) { Init = InitFollow };
var stateRotate = new AiState { Init = InitRotation };
stateRotate.Trigger.Add(new AiTriggerCountdown(RotateTime, RotateTick, RotateEnd));
_aiComponent = new AiComponent();
_aiComponent.States.Add("idle", stateIdle);
_aiComponent.States.Add("spawn", stateSpawn);
_aiComponent.States.Add("spawnDelay", stateSpawnDelay);
_aiComponent.States.Add("despawn", stateDespawn);
_aiComponent.States.Add("attack", stateAttack);
_aiComponent.States.Add("follow", stateFollow);
_aiComponent.States.Add("rotate", stateRotate);
_damageState = new AiDamageState(this, _body, _aiComponent, _sprite, Lives, true, false);
_damageState.AddBossDamageState(OnDeath);
_damageState.ExplosionOffsetY = -8;
_damageState.BossHitSound = true;
_aiComponent.ChangeState("idle");
AddComponent(AiComponent.Index, _aiComponent);
AddComponent(DamageFieldComponent.Index, _damageField = new DamageFieldComponent(damageCollider, HitType.Enemy, 4) { IsActive = false });
AddComponent(HittableComponent.Index, new HittableComponent(hittableBox, OnHit));
AddComponent(BodyComponent.Index, _body);
AddComponent(UpdateComponent.Index, new UpdateComponent(Update));
AddComponent(DrawComponent.Index, new DrawComponent(Draw, Values.LayerPlayer, EntityPosition));
AddComponent(DrawShadowComponent.Index, _shadowComponent = new ShadowBodyDrawComponent(EntityPosition) { ShadowWidth = 18, ShadowHeight = 6 });
}
public void Spawn(Vector3 position)
{
EntityPosition.Set(position);
_aiComponent.ChangeState("spawn");
_spawnPosition = new Vector2(position.X, position.Y);
}
public void AttackSpawn(Vector3 position)
{
EntityPosition.Set(position);
_aiComponent.ChangeState("spawn");
// do not start at the beginning
_smokeTop.SetFrame(3);
_spawnCounter = 600;
UpdateSpawn();
_attackMode = true;
}
private void SpawnDelayEnd()
{
if (_attackMode)
{
Game1.GameManager.StartDialogPath("d2_boss_3");
_aiComponent.ChangeState("follow");
}
else
{
Game1.GameManager.StartDialogPath("d2_boss_1");
_aiComponent.ChangeState("attack");
}
}
private void InitDespawn()
{
_damageField.IsActive = false;
_smokePosition0 = new Vector2(EntityPosition.X, EntityPosition.Y - 54);
_smokePosition1 = new Vector2(EntityPosition.X, EntityPosition.Y - 54);
_smokeTop.Play("despawn");
_spawnCounter = _smokeTop.GetAnimationTime();
Game1.GameManager.PlaySoundEffect("D360-31-1F");
_drawSmoke = true;
}
private void UpdateDespawn()
{
_spawnCounter -= Game1.DeltaTime;
UpdateSmoke(_spawnCounter);
if (!_smokeTop.IsPlaying)
DespawnEnd();
// despawn the genie
if (_smokeTop.CurrentFrameIndex > 0)
_isVisible = false;
}
private void DespawnEnd()
{
_drawSmoke = false;
_shadowComponent.IsActive = false;
_aiComponent.ChangeState("idle");
_objBottle.StartFollowing();
}
private void InitSpawn()
{
_spawnCounter = 0;
_smokePosition0 = new Vector2(EntityPosition.X, EntityPosition.Y - 29);
_smokePosition1 = new Vector2(EntityPosition.X, EntityPosition.Y - 29);
_smokeTop.Play("top");
Game1.GameManager.PlaySoundEffect("D360-06-06");
_shadowComponent.IsActive = true;
_drawSmoke = true;
}
private void UpdateSpawn()
{
_spawnCounter += Game1.DeltaTime;
UpdateSmoke(_spawnCounter);
if (!_smokeTop.IsPlaying)
SpawnEnd();
// spawn the genie
if (_smokeTop.CurrentFrameIndex == 6)
_isVisible = true;
}
private void SpawnEnd()
{
_damageField.IsActive = true;
_drawSmoke = false;
_aiComponent.ChangeState("spawnDelay");
}
private void UpdateSmoke(float state)
{
_smokeBottom.Update();
_smokeTop.Update();
if (330 < _spawnCounter && _spawnCounter < 600)
{
_smokeBottom.Play("bottom");
_smokePosition1.Y = EntityPosition.Y - 29 - ((_spawnCounter - 330) / 600f) * 25;
}
else
{
_smokeBottom.Stop();
}
var movePercentage = MathF.Sin(_spawnCounter / 600f * MathF.PI / 3) / MathF.Sin(MathF.PI / 3);
// move up
if (_spawnCounter < 600)
_smokePosition0.Y = EntityPosition.Y - 29 - movePercentage * 25;
else
_smokePosition0.Y = EntityPosition.Y - 54;
}
private void InitFollow()
{
_bodyAnimator.Play("attack");
}
private void UpdateFollow()
{
// fly towards the player
var playerDirection = MapManager.ObjLink.EntityPosition.Position - new Vector2(EntityPosition.X, EntityPosition.Y - EntityPosition.Z - 8);
if (playerDirection != Vector2.Zero)
{
playerDirection.Normalize();
// have momentum and not directly change the direction
var percentage = (float)Math.Pow(0.98, Game1.TimeMultiplier);
_body.VelocityTarget = percentage * _body.VelocityTarget + (1 - percentage) * playerDirection * FollowSpeed;
}
}
private void InitRotation()
{
_shadowComponent.IsActive = false;
_damageField.IsActive = false;
_body.VelocityTarget = Vector2.Zero;
var centerOffset = new Vector2(EntityPosition.Position.X, EntityPosition.Position.Y - RotationOffsetY) - _roomCenter;
_currentRotation = MathF.Atan2(centerOffset.Y, centerOffset.X);
_rotationDistance = centerOffset.Length();
}
private void RotateTick(double counter)
{
// do not get to close the mirrored version
if (_rotationDistance > 4)
_rotationDistance -= Game1.TimeMultiplier * MathHelper.Clamp(_rotationDistance / 100, 0, 0.3f);
// move the same distance each frame independent of how far we are away from the rotation origin
var rotationSpeed = MathHelper.Clamp(_rotationDistance, 0, 6);
_currentRotation += rotationSpeed / (_rotationDistance * MathF.PI) * Game1.TimeMultiplier;
var newPosition = new Vector2(_roomCenter.X, _roomCenter.Y + RotationOffsetY) + new Vector2(MathF.Cos(_currentRotation), MathF.Sin(_currentRotation)) * _rotationDistance;
EntityPosition.Set(newPosition);
}
private void RotateEnd()
{
_shadowComponent.IsActive = true;
_damageField.IsActive = true;
// throw fireball
var fireball = new BossGenieFireball(Map, EntityPosition.ToVector3());
Map.Objects.SpawnObject(fireball);
// spawn the ball on the left or right side
if (MapManager.ObjLink.EntityPosition.X < EntityPosition.X)
_fireballIndex = 0;
else
_fireballIndex = 1;
ThrowFireball(fireball);
_aiComponent.ChangeState("follow");
}
private void InitAttack()
{
_fireballIndex = 0;
_fireballCount = 8;
_fireballs = new BossGenieFireball[_fireballCount];
for (var i = 0; i < _fireballCount; i++)
{
// spawn behind the genie for the initial frame
_fireballs[i] = new BossGenieFireball(Map, new Vector3(EntityPosition.X, EntityPosition.Y - 1, EntityPosition.Z + 12));
Map.Objects.SpawnObject(_fireballs[i]);
}
}
private void AttackTick(double counter)
{
var state = (float)((AttackTime - counter) / AttackTime);
for (var i = _fireballIndex; i < _fireballCount; i++)
{
// 5 fireballs form a full circle
var circleCount = 5;
var fullTurns = 7;
var radiant = (state * MathF.PI * 2 * fullTurns + i * 2 * (2 * MathF.PI) / 5);
var newPosition = new Vector3(EntityPosition.X - MathF.Sin(radiant) * 12, EntityPosition.Y - 1, EntityPosition.Z + 30 - MathF.Cos(radiant) * 15);
_fireballs[i].SetPosition(newPosition);
// throw fireball at the time where the ball is behind the genie
if (state > 2f / fullTurns + (_fireballIndex * (3 / (float)circleCount)) * (1 / (float)fullTurns))
{
ThrowFireball();
}
}
// move the genie left/right and up/down
var moveState = state * MathF.PI * 2 * 2;
var offsetX = MathF.Sin(moveState) * 27;
var offsetY = MathF.Sin(moveState * 7) * 4;
EntityPosition.Set(new Vector2(_spawnPosition.X + offsetX, _spawnPosition.Y + offsetY));
}
private void ThrowFireball()
{
ThrowFireball(_fireballs[_fireballIndex]);
_bodyAnimator.Play(_fireballIndex % 2 == 0 ? "attack_0" : "attack_1");
_fireballIndex++;
}
private void ThrowFireball(BossGenieFireball fireball)
{
fireball.EntityPosition.Set(new Vector3(
EntityPosition.X + (_fireballIndex % 2 == 0 ? -15 : 15), EntityPosition.Y, EntityPosition.Z + 30));
// throw the fireball in the direction of the player
var playerDirection = MapManager.ObjLink.EntityPosition.ToVector3() -
fireball.EntityPosition.ToVector3();
if (playerDirection != Vector3.Zero)
{
playerDirection.Normalize();
playerDirection *= 2.25f;
}
fireball.ThrowFireball(playerDirection);
Game1.GameManager.PlaySoundEffect("D378-40-28");
}
private void AttackEnd()
{
_aiComponent.ChangeState("despawn");
}
private void Update()
{
_bodyAnimator.Update();
_tailAnimator.Update();
}
private void Draw(SpriteBatch spriteBatch)
{
if (_isVisible)
{
var color = Color.White;
// change the draw effect
if (_sprite.SpriteShader != null)
{
spriteBatch.End();
ObjectManager.SpriteBatchBegin(spriteBatch, _sprite.SpriteShader);
}
if (_aiComponent.CurrentStateId == "rotate")
{
color *= 0.5f;
var drawPosition = new Vector2(EntityPosition.Position.X, EntityPosition.Position.Y);
var centerOffset = drawPosition - new Vector2(_roomCenter.X, _roomCenter.Y + RotationOffsetY);
var newPosition = drawPosition - centerOffset * 2 + new Vector2(0, -EntityPosition.Z);
// draw the body
_bodyAnimator.Draw(spriteBatch, newPosition, color);
// draw the tail
_tailAnimator.Draw(spriteBatch, newPosition, color);
}
// draw the body
_bodyAnimator.Draw(spriteBatch, new Vector2(EntityPosition.X, EntityPosition.Y - EntityPosition.Z), color);
// draw the tail
_tailAnimator.Draw(spriteBatch, new Vector2(EntityPosition.X, EntityPosition.Y - EntityPosition.Z), color);
// change the draw effect
if (_sprite.SpriteShader != null)
{
spriteBatch.End();
ObjectManager.SpriteBatchBegin(spriteBatch, null);
}
}
// draw the smoke while spawning/despawning
if (_drawSmoke)
DrawSpawn(spriteBatch);
}
private void DrawSpawn(SpriteBatch spriteBatch)
{
if (_smokeTop.IsPlaying)
_smokeTop.Draw(spriteBatch, _smokePosition0, Color.White);
if (_smokeBottom.IsPlaying)
_smokeBottom.Draw(spriteBatch, _smokePosition1, Color.White);
}
private void OnDeath()
{
if (!string.IsNullOrEmpty(_saveKey))
Game1.GameManager.SaveManager.SetString(_saveKey, "1");
// stop boss music
Game1.GameManager.SetMusic(-1, 2);
var heartPosition = new Vector2(EntityPosition.X, EntityPosition.Y - EntityPosition.Z);
var centerDistance = heartPosition - _roomCenter;
centerDistance.X = MathHelper.Clamp(centerDistance.X, -56, 56);
centerDistance.Y = MathHelper.Clamp(centerDistance.Y, -24, 54);
heartPosition = _roomCenter + centerDistance;
// spawn big heart
Map.Objects.SpawnObject(new ObjItem(Map,
(int)heartPosition.X - 8, (int)heartPosition.Y - 16, "j", "d2_nHeart", "heartMeterFull", null));
// remove the boss from the map
Map.Objects.DeleteObjects.Add(this);
}
private Values.HitCollision OnHit(GameObject gameObject, Vector2 direction, HitType damageType, int damage, bool pieceOfPower)
{
// can only get attacked in the follow state
if (_aiComponent.CurrentStateId != "follow" ||
_damageState.IsInDamageState() ||
damageType == HitType.MagicPowder)
return Values.HitCollision.None;
_aiComponent.ChangeState("rotate");
if (damageType == HitType.Bomb)
damage *= 2;
var damageReturn = _damageState.OnHit(MapManager.ObjLink, direction, HitType.ThrownObject, damage, pieceOfPower);
// stop if we are dead
if (_damageState.CurrentLives <= 0)
_body.VelocityTarget = Vector2.Zero;
return damageReturn;
}
}
}