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.Things; namespace ProjectZ.InGame.GameObjects.MidBoss { class MBossMasterStalfos : GameObject { private readonly BodyComponent _body; private readonly AiComponent _aiComponent; private readonly DamageFieldComponent _damageField; private readonly AiDamageState _aiDamageState; private readonly ShadowBodyDrawComponent _shadowComponent; private readonly AiTriggerSwitch _damageCooldown; private SheetAnimator _animator; private CPosition[] _positions = new CPosition[5]; private CSprite[] _sprites = new CSprite[5]; private readonly CSprite _sprite; // quick fix: draw the swing behind the sword private int[] _drawOrder = { 0, 1, 2, 4, 3 }; private const float MovementSpeed = 0.5f; private float[] DamageTime = { 0, 175, 75, 0 }; private float[] _damageCounters = new float[4]; private float[] _partVelocity = new float[4]; private float[] _partOffset = { 0, 0, 3, 3 }; private float _partGravity = 0.15f; private float[] _standCounters = new float[4]; private const int WobbleTime = 750; private int _direction; private float _transparency; private readonly string _saveKey; private readonly int _encounterNumber; private string _encounterState; private bool _shownIntroText; private bool _damageState; private bool _flee; private bool _attackSound; private const int FleeTime = 400; public MBossMasterStalfos() : base("ms_shield") { } public MBossMasterStalfos(Map.Map map, int posX, int posY, string saveKey, int encounterNumber) : base(map) { EntityPosition = new CPosition(posX + 16, posY + 32, 100); EntitySize = new Rectangle(-24, -38, 48, 38); _saveKey = saveKey; _encounterNumber = encounterNumber; _animator = new SheetAnimator(); var aniStandLeft = new SheetAnimation("stand-1", 0, new AFrame(16, new ASprite(-15, -16), new ASprite(-14, -32, false, true), new ASprite(-24, -24), new ASprite(8, -37), null)); var aniStandRight = new SheetAnimation("stand1", 0, new AFrame(16, new ASprite(-15, -16), new ASprite(-13, -32), new ASprite(-16, -24), new ASprite(8, -40), null)); var aniPreJumpLeft = new SheetAnimation("preJump-1", 0, new AFrame(16, new ASprite(-15, -16), new ASprite(-14, -30, false, true), new ASprite(-24, -22), new ASprite(8, -35), null)); var aniPreJumpRight = new SheetAnimation("preJump1", 0, new AFrame(16, new ASprite(-15, -16), new ASprite(-13, -30), new ASprite(-16, -22), new ASprite(8, -38), null)); var aniWalkLeft = new SheetAnimation("walk-1", -1, new AFrame(16, new ASprite(-15, -16), new ASprite(-14, -32, false, true), new ASprite(-24, -24), new ASprite(8, -37), null), new AFrame(16, new ASprite(-15, -16, false, true), new ASprite(-14, -32, false, true), new ASprite(-24, -24), new ASprite(8, -37), null)); var aniWalkRight = new SheetAnimation("walk1", -1, new AFrame(16, new ASprite(-15, -16), new ASprite(-13, -32), new ASprite(-16, -24), new ASprite(8, -40), null), new AFrame(16, new ASprite(-15, -16, false, true), new ASprite(-13, -32), new ASprite(-16, -24), new ASprite(8, -40), null)); var aniHitLeft = new SheetAnimation("hit-1", 0, new AFrame(24, new ASprite(-15, -16, false, true), new ASprite(-14, -32, false, true), new ASprite(-24, -24), new ASprite(-16, -48), null), new AFrame(8, new ASprite(-15, -16, false, true), new ASprite(-14, -32), new ASprite(-24, -32), null, new ASprite(-24, -24)), new AFrame(8, new ASprite(-15, -16, false, true), new ASprite(-14, -32), new ASprite(-24, -36), new ASprite(8, -8, true), new ASprite(-24, -24)), new AFrame(40, new ASprite(-15, -16, false, true), new ASprite(-14, -32), new ASprite(-24, -36), new ASprite(8, -8, true), null)); var aniHitRight = new SheetAnimation("hit1", 0, new AFrame(24, new ASprite(-15, -16), new ASprite(-13, -32), new ASprite(-16, -24), new ASprite(1, -56, false, true), null), new AFrame(8, new ASprite(-15, -16), new ASprite(-14, -32, false, true), new ASprite(-24, -28), null, new ASprite(-8, -24, false, true)), new AFrame(8, new ASprite(-15, -16), new ASprite(-14, -32, false, true), new ASprite(-24, -32), new ASprite(-15, -8, true, true), new ASprite(-8, -24, false, true)), new AFrame(40, new ASprite(-15, -16), new ASprite(-14, -32, false, true), new ASprite(-24, -32), new ASprite(-15, -8, true, true), null)); _animator.Animations.Add(aniStandLeft); _animator.Animations.Add(aniStandRight); _animator.Animations.Add(aniPreJumpLeft); _animator.Animations.Add(aniPreJumpRight); _animator.Animations.Add(aniWalkLeft); _animator.Animations.Add(aniWalkRight); _animator.Animations.Add(aniHitLeft); _animator.Animations.Add(aniHitRight); _animator.Play("standLeft"); for (var i = 0; i < _positions.Length; i++) _positions[i] = new CPosition(0, 0, 0); // get all the sprites for the boss _sprites[0] = new CSprite("ms_feet", _positions[0], Vector2.Zero); _sprites[1] = new CSprite("ms_head", _positions[1], Vector2.Zero); _sprites[2] = new CSprite("ms_shield", _positions[2], Vector2.Zero); _sprites[3] = new CSprite("ms_sword", _positions[3], Vector2.Zero); _sprites[4] = new CSprite("ms_swing", _positions[4], Vector2.Zero); var animationComponent = new AnimationSheetComponent(_animator); _body = new BodyComponent(EntityPosition, -14, -16, 28, 16, 8) { IgnoreHoles = true, Gravity = -0.125f, FieldRectangle = Map.GetField(posX, posY, 16), IsActive = false }; _aiComponent = new AiComponent(); _aiComponent.Trigger.Add(_damageCooldown = new AiTriggerSwitch(500)); var stateHidden = new AiState(UpdateHidden); var statePreFall = new AiState(); statePreFall.Trigger.Add(new AiTriggerCountdown(500, null, () => _aiComponent.ChangeState("fall"))); var stateFall = new AiState(UpdateFall) { Init = InitFall }; var statePostFall = new AiState(); statePostFall.Trigger.Add(new AiTriggerCountdown(500, null, () => _aiComponent.ChangeState("idle"))); var stateIdle = new AiState { Init = InitIdle }; stateIdle.Trigger.Add(new AiTriggerCountdown(100, null, EndIdle)); var stateWalk = new AiState(UpdateWalking) { Init = InitWalk }; stateWalk.Trigger.Add(new AiTriggerCountdown(1000, null, EndWalking)); var statePreDamaged = new AiState(UpdatePreDamaged) { Init = InitPreDamageState }; var stateDamaged = new AiState(UpdateDamaged) { Init = InitDamageState }; stateDamaged.Trigger.Add(new AiTriggerCountdown(3000, null, () => _aiComponent.ChangeState("wobble"))); var stateWobble = new AiState(); stateWobble.Trigger.Add(new AiTriggerCountdown(WobbleTime, WobbleTick, WobbleEnd)); var stateStandingUp = new AiState { Init = InitStandingUp }; stateStandingUp.Trigger.Add(new AiTriggerCountdown(750, StandUpTick, StandUpEnd)); var statePreJump = new AiState { Init = InitPreJump }; statePreJump.Trigger.Add(new AiTriggerCountdown(500, null, () => _aiComponent.ChangeState("jump"))); var stateJump = new AiState(UpdateJump) { Init = InitJump }; var statePostJump = new AiState { Init = InitPostJump }; statePostJump.Trigger.Add(new AiTriggerCountdown(300, null, () => _aiComponent.ChangeState("idle"))); var stateAttack = new AiState(UpdateAttack) { Init = InitAttack }; var statePreFlee = new AiState { Init = InitPreJump }; statePreFlee.Trigger.Add(new AiTriggerCountdown(250, null, () => _aiComponent.ChangeState("flee"))); var stateFlee = new AiState { Init = InitFlee }; stateFlee.Trigger.Add(new AiTriggerCountdown(FleeTime, FleeTick, FleeEnd)); _aiComponent.States.Add("hidden", stateHidden); _aiComponent.States.Add("preFall", statePreFall); _aiComponent.States.Add("fall", stateFall); _aiComponent.States.Add("postFall", statePostFall); _aiComponent.States.Add("idle", stateIdle); _aiComponent.States.Add("walk", stateWalk); _aiComponent.States.Add("preDamaged", statePreDamaged); _aiComponent.States.Add("damaged", stateDamaged); _aiComponent.States.Add("wobble", stateWobble); _aiComponent.States.Add("standUp", stateStandingUp); _aiComponent.States.Add("preJump", statePreJump); _aiComponent.States.Add("jump", stateJump); _aiComponent.States.Add("postJump", statePostJump); _aiComponent.States.Add("attack", stateAttack); _aiComponent.States.Add("preFlee", statePreFlee); _aiComponent.States.Add("flee", stateFlee); // dummy sprite _sprite = new CSprite(EntityPosition); var lives = 6; if (_encounterNumber == 1 || _encounterNumber == 2) lives = 4; _aiDamageState = new AiDamageState(this, _body, _aiComponent, _sprite, lives, false, false) { BossHitSound = true, HitMultiplierX = 3, HitMultiplierY = 3 }; if (_encounterNumber != 3) _aiDamageState.OnDeath = OnDeath; else _aiDamageState.AddBossDamageState(OnBossDeath); _aiDamageState.ExplosionOffsetY = 8; _aiComponent.ChangeState("hidden"); var damageCollider = new CBox(EntityPosition, -14, -24, 0, 28, 24, 8); var hittableBox = new CBox(EntityPosition, -12, -26, 0, 24, 24, 8); AddComponent(DamageFieldComponent.Index, _damageField = new DamageFieldComponent(damageCollider, HitType.Enemy, 4)); AddComponent(PushableComponent.Index, new PushableComponent(damageCollider, OnPush)); AddComponent(HittableComponent.Index, new HittableComponent(hittableBox, OnHit)); AddComponent(AiComponent.Index, _aiComponent); AddComponent(BodyComponent.Index, _body); AddComponent(BaseAnimationComponent.Index, animationComponent); AddComponent(DrawComponent.Index, new DrawComponent(Draw, Values.LayerPlayer, EntityPosition)); AddComponent(DrawShadowComponent.Index, _shadowComponent = new ShadowBodyDrawComponent(EntityPosition) { IsActive = false, ShadowWidth = 22, ShadowHeight = 6 }); if (!string.IsNullOrEmpty(_saveKey)) { AddComponent(KeyChangeListenerComponent.Index, new KeyChangeListenerComponent(OnKeyChange)); // spawn the hookshot if the player did not collect it after var encounterState = Game1.GameManager.SaveManager.GetString(_saveKey); if (encounterState == "4" && _encounterNumber == 3) SpawnHookshot(); } // TODO: need to find a way to draw shadow for this //AddComponent(DrawShadowComponent.Index, new DrawShadowCSpriteComponent(sprite)); } private void OnKeyChange() { _encounterState = Game1.GameManager.SaveManager.GetString(_saveKey); if (_encounterState == null) _encounterState = "0"; } private void InitFlee() { Game1.GameManager.PlaySoundEffect("D378-62-3F"); _body.IsActive = false; _animator.Play("stand" + _direction); // stop the music Game1.GameManager.SetMusic(-1, 2); } private void FleeTick(double counter) { var state = (float)(FleeTime - counter) / FleeTime; EntityPosition.Z = MathF.Sin(state * MathF.PI * 0.4f) * 80; _transparency = MathF.Min((1 - state) / 0.25f, 1); _shadowComponent.Transparency = 1 - state; } private void FleeEnd() { if (!string.IsNullOrEmpty(_saveKey)) Game1.GameManager.SaveManager.SetString(_saveKey, (_encounterNumber + 1).ToString()); Map.Objects.DeleteObjects.Add(this); } private void UpdateHidden() { // player entered the room? => fall down if (_encounterState == _encounterNumber.ToString() && _body.FieldRectangle.Contains(MapManager.ObjLink.BodyRectangle)) { Game1.GameManager.SetMusic(79, 2); _aiComponent.ChangeState("preFall"); } } private void InitFall() { Game1.GameManager.PlaySoundEffect("D360-08-08"); _animator.Play("stand1"); _shadowComponent.IsActive = true; _body.IsActive = true; } private void UpdateFall() { _transparency = MathF.Min((100 - EntityPosition.Z) / 10f, 1); if (_body.IsGrounded) { _animator.Play("preJump1"); _aiComponent.ChangeState("postFall"); } } private void InitAttack() { _attackSound = false; _body.VelocityTarget = Vector2.Zero; _animator.Play("hit" + _direction); } private void UpdateAttack() { if (_animator.CurrentFrameIndex == 1 || _animator.CurrentFrameIndex == 2) { if (!_attackSound) { _attackSound = true; Game1.GameManager.PlaySoundEffect("D378-39-27"); } // attack the player var damageBox = new Box(EntityPosition.X - 16 + (_direction == -1 ? -6 : 6), EntityPosition.Y - 22, 0, 32, 38, 8); if (damageBox.Intersects(MapManager.ObjLink._body.BodyBox.Box)) { var direction = MapManager.ObjLink.EntityPosition.Position - EntityPosition.Position; if (direction != Vector2.Zero) direction.Normalize(); MapManager.ObjLink.HitPlayer(direction * 2.5f, HitType.Boss, 4, false); } } // finished attacking if (!_animator.IsPlaying) _aiComponent.ChangeState("idle"); } private void InitIdle() { _body.VelocityTarget = Vector2.Zero; // look at the player var playerDirection = MapManager.ObjLink.EntityPosition.Position - EntityPosition.Position; _direction = playerDirection.X < 0 ? -1 : 1; _animator.Play("stand" + _direction); } private void EndIdle() { if (_flee) { Game1.GameManager.StartDialogPath("master_stalfos_0"); _aiComponent.ChangeState("preFlee"); return; } if (_encounterNumber > 0 && !_shownIntroText) { _shownIntroText = true; Game1.GameManager.StartDialogPath(_encounterNumber == 3 ? "master_stalfos_2" : "master_stalfos_1"); } var playerDirection = MapManager.ObjLink.EntityPosition.Position - EntityPosition.Position; // player is standing in front of the boss? => attack if (Math.Abs(playerDirection.X) < 24 && 0 < playerDirection.Y && playerDirection.Y < 18) _aiComponent.ChangeState("attack"); else if (Math.Abs(playerDirection.X) < 28 && 0 < playerDirection.Y && playerDirection.Y < 48) _aiComponent.ChangeState("walk"); else _aiComponent.ChangeState("preJump"); } private void InitPreJump() { var playerDirection = MapManager.ObjLink.EntityPosition.Position - EntityPosition.Position; _direction = playerDirection.X < 0 ? -1 : 1; _animator.Play("preJump" + _direction); } private void InitJump() { Game1.GameManager.PlaySoundEffect("D360-36-24"); _animator.Play("stand" + _direction); var playerDirection = MapManager.ObjLink.EntityPosition.Position - EntityPosition.Position; if (playerDirection != Vector2.Zero) { // try to jump at a spot where the player can be easily hit if (playerDirection.Y < 0) playerDirection.Y *= 2; playerDirection.Normalize(); _body.VelocityTarget = playerDirection * 1.5f; _body.Velocity.Z = 3; } } private void UpdateJump() { // reached the ground if (_body.IsGrounded && _body.Velocity.Z <= 0) { _aiComponent.ChangeState("postJump"); _body.VelocityTarget = Vector2.Zero; } } private void InitPostJump() { _animator.Play("preJump" + _direction); } private void InitWalk() { // look at the player while moving var playerDirection = MapManager.ObjLink.EntityPosition.Position - EntityPosition.Position; _direction = playerDirection.X < 0 ? -1 : 1; _animator.Play("walk" + _direction); // move towards the player if (playerDirection != Vector2.Zero) { playerDirection.Normalize(); _body.VelocityTarget = playerDirection * MovementSpeed; } } private void UpdateWalking() { var playerDirection = MapManager.ObjLink.EntityPosition.Position - EntityPosition.Position; // attack if we are close enough if (Math.Abs(playerDirection.X) < 24 && 0 < playerDirection.Y && playerDirection.Y < 18) _aiComponent.ChangeState("attack"); } private void EndWalking() { _aiComponent.ChangeState("idle"); } private void InitPreDamageState() { _body.Velocity = Vector3.Zero; _body.VelocityTarget = Vector2.Zero; } private void UpdatePreDamaged() { if (_body.IsGrounded && !_aiDamageState.IsInDamageState() && _body.Velocity.Length() < 0.1f) _aiComponent.ChangeState("damaged"); } private void InitDamageState() { _body.Velocity = Vector3.Zero; _body.VelocityTarget = Vector2.Zero; _damageField.IsActive = false; Game1.GameManager.PlaySoundEffect("D360-40-28"); for (var i = 0; i < _partVelocity.Length; i++) { _damageCounters[i] = DamageTime[i]; _partVelocity[i] = 0; } } private void UpdateDamaged() { _damageState = true; for (var i = 1; i < _partVelocity.Length; i++) { _damageCounters[i] -= Game1.DeltaTime; // fall onto the ground if (_damageCounters[i] < 0) { _partVelocity[i] += _partGravity * Game1.TimeMultiplier; var velocityOffset = _partVelocity[i] * Game1.TimeMultiplier; if (_positions[i].Z - velocityOffset > _sprites[i].SourceRectangle.Height - _partOffset[i]) _positions[i].Z -= velocityOffset; else _positions[i].Z = _sprites[i].SourceRectangle.Height - _partOffset[i]; } } } private void WobbleTick(double counter) { // move the upper body up and down // 4 frames to go up/down _positions[1].Z = _sprites[1].SourceRectangle.Height + 1 - MathF.Sin(MathF.PI / 2 + MathF.PI * ((WobbleTime - (float)counter) / 1000 * (60 / 4f))); } private void WobbleEnd() { _aiComponent.ChangeState("standUp"); } private void InitStandingUp() { for (var i = 0; i < _partVelocity.Length; i++) { _standCounters[i] = DamageTime[i]; } } private void StandUpTick(double counter) { for (var i = 0; i < _partVelocity.Length; i++) { _standCounters[i] -= Game1.DeltaTime; if (_standCounters[i] < 0) { var speed = 0.5f * Game1.TimeMultiplier; if (_positions[i].Z + speed < -_animator.CurrentFrame.Sprites[i].Offset.Y) _positions[i].Z += speed; else _positions[i].Z = -_animator.CurrentFrame.Sprites[i].Offset.Y; } } } private void StandUpEnd() { _damageCooldown.Reset(); _damageField.IsActive = true; _damageState = false; _aiComponent.ChangeState("idle"); } private void OnDeath(bool pieceOfPower) { _flee = true; _damageField.IsActive = false; } private void OnBossDeath() { if (!string.IsNullOrEmpty(_saveKey)) Game1.GameManager.SaveManager.SetString(_saveKey, (_encounterNumber + 1).ToString()); // stop the music Game1.GameManager.SetMusic(-1, 2); SpawnHookshot(); Map.Objects.DeleteObjects.Add(this); } private void SpawnHookshot() { var objItem = new ObjItem(Map, (int)EntityPosition.X - 8, (int)EntityPosition.Y - 16, "j", "hookshot_collected", "hookshot", null); Map.Objects.SpawnObject(objItem); } private void Draw(SpriteBatch spriteBatch) { // change the draw effect if (_sprite.SpriteShader != null) { spriteBatch.End(); ObjectManager.SpriteBatchBegin(spriteBatch, _sprite.SpriteShader); } // do not draw the legs if the upper part is laying on the floor _sprites[0].IsVisible = _sprites[1].SourceRectangle.Height + 2 < _positions[1].Z; for (var i = 0; i < _positions.Length; i++) { var spriteIndex = _drawOrder[i]; if (_animator.CurrentFrame.Sprites[spriteIndex] == null) continue; _positions[spriteIndex].X = EntityPosition.X + _animator.CurrentFrame.Sprites[spriteIndex].Offset.X; _positions[spriteIndex].Y = EntityPosition.Y; // only update the animation if the boss is not in the damaged state if (!_damageState) { _positions[spriteIndex].Z = EntityPosition.Z - _animator.CurrentFrame.Sprites[spriteIndex].Offset.Y; _sprites[spriteIndex].SpriteEffect = (_animator.CurrentFrame.Sprites[spriteIndex].MirroredV ? SpriteEffects.FlipVertically : 0) | (_animator.CurrentFrame.Sprites[spriteIndex].MirroredH ? SpriteEffects.FlipHorizontally : 0); } _sprites[spriteIndex].Color = Color.White * _transparency; _sprites[spriteIndex].Draw(spriteBatch); } // change the draw effect if (_sprite.SpriteShader != null) { spriteBatch.End(); ObjectManager.SpriteBatchBegin(spriteBatch, null); } } private bool OnPush(Vector2 direction, PushableComponent.PushType type) { if (!_damageField.IsActive) return false; if (type == PushableComponent.PushType.Impact) _body.Velocity = new Vector3(direction.X, direction.Y, _body.Velocity.Z); return true; } public Values.HitCollision OnHit(GameObject gameObject, Vector2 direction, HitType damageType, int damage, bool pieceOfPower) { if (_aiDamageState.CurrentLives <= 0 || _aiDamageState.IsInDamageState()) return Values.HitCollision.None; // switch to the damaged state if (_damageCooldown.State && _aiComponent.CurrentStateId != "preDamaged" && _aiComponent.CurrentStateId != "damaged" && _aiComponent.CurrentStateId != "attack" && _aiComponent.CurrentStateId != "wobble" && _aiComponent.CurrentStateId != "standUp") { _aiComponent.ChangeState("preDamaged"); _aiDamageState.OnHit(gameObject, direction, damageType, 0, pieceOfPower); return Values.HitCollision.Repelling; } if (_body.Velocity.Length() < 0.01f) { _body.Velocity.X = direction.X; _body.Velocity.Y = direction.Y; } // can only be damaged while lying on the floor while being hit by a bomb if ((_aiComponent.CurrentStateId == "damaged" || _aiComponent.CurrentStateId == "wobble") && damageType == HitType.Bomb) { _aiDamageState.OnHit(gameObject, direction, damageType, damage, pieceOfPower); } return Values.HitCollision.RepellingParticle; } } }