mirror of
https://github.com/Phantop/LADXHD.git
synced 2024-11-01 04:14:22 +00:00
492 lines
18 KiB
C#
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;
|
|
}
|
|
}
|
|
} |