using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.SceneManagement; public class BattleController : MonoBehaviour, IBattleEffectApplier, IBattleEffectResolver { private bool STABLE_HAND_STYLE = true; // Inspector public public GameObject uiCanvas; public BattlePlayerControls battlePlayerControls; public EnergyUI heroEnergyUI; public EnergyUI monsterEnergyUI; public BattleSprites battleSprites; // Private private BattleState state; private HandPlayability heroHandPlayability; private HandPlayability monsterHandPlayability; private TurnIntentions monsterIntentions; private delegate void BattleStateChangedAction(); private event BattleStateChangedAction OnBattleStateChanged; void Awake() { BattleParameters parameters = BattleSetup.RetrieveParameters(); if (parameters == null) { Debug.Log("Using default battle"); parameters = CheatBattle.CreateParameters(); } this.state = new BattleState(parameters); this.heroEnergyUI.Init(this.state.HeroEnergy); this.monsterEnergyUI.Init(this.state.MonsterEnergy); this.battleSprites.Init(this.state.Heroes, this.state.Monsters, this.state); this.heroHandPlayability = new HandPlayability( this.state, this.state.HeroEnergy, TeamChoice.Heroes, this.state.HeroDeck); this.OnBattleStateChanged += this.heroHandPlayability.NotifyBattleStateChanged; this.monsterHandPlayability = new HandPlayability( this.state, this.state.MonsterEnergy, TeamChoice.Monsters, this.state.MonsterDeck); this.OnBattleStateChanged += this.monsterHandPlayability.NotifyBattleStateChanged; this.monsterIntentions = new TurnIntentions( this.state.MonsterDeck, this.state.MonsterEnergy, this.monsterHandPlayability); this.battlePlayerControls.Init( this.state.HeroDeck, this.state.MonsterDeck, this.state.PlayArea, this.state.InsightLevel, this.heroHandPlayability, this.monsterHandPlayability, this.monsterIntentions, this.uiCanvas.transform.localScale.x); } // Start is called before the first frame update void Start() { StartCoroutine(RunBattleRounds()); CheatBattle cheat = new GameObject("CheatBattle").AddComponent(); cheat.OnWinCode += delegate { BattleResult.FinishBattle(new BattleResult(true)); }; cheat.OnInsightCode += delegate { this.state.InsightLevel.Modify(+5); }; cheat.OnOneHPCode += delegate { foreach (BattleMonster m in this.state.Monsters) { if (m.IsAlive()) { m.DealDamage(m.GetHP() - 1); } } OnBattleStateChanged(); }; } // Update is called once per frame void Update() { } private IEnumerator RunBattleRounds() { if (STABLE_HAND_STYLE) { this.state.HeroDeck.DrawCards(6); this.state.MonsterDeck.DrawCards(6); } while (true) { yield return RunBattleRound(); } } private IEnumerator RunBattleRound() { this.state.Clock.AdvanceRound(); this.state.HeroEnergy.Replenish(); this.state.MonsterEnergy.Replenish(); if (STABLE_HAND_STYLE) { if (this.state.Clock.Now.RoundNumber > 1) { this.state.HeroDeck.DrawCards(3); this.state.MonsterDeck.DrawCards(3); } } else { this.state.HeroDeck.DrawCards(5); this.state.MonsterDeck.DrawCards(5); } yield return RunHeroTurn(); yield return RunMonsterTurn(); this.state.Clock.AdvanceToStep(RoundStep.EndOfRound); } private IEnumerator RunHeroTurn() { // Start of hero turn this.state.Clock.AdvanceToStep(RoundStep.StartOfHeroTurn); this.state.ClearEmotionPlayed(); OnBattleStateChanged(); this.monsterIntentions.ResumeIntentions(); foreach (BattleHero hero in this.state.Heroes) { hero.ResetBlock(); } OnBattleStateChanged(); // Heroes act until they pass while (true) { PlayerAction playerAction = this.battlePlayerControls.RetrievePlayerAction(); if (playerAction != null) { if (playerAction.IsTurnPassed()) { break; } else { HeroCard card = playerAction.GetCardUsed(); yield return UseHeroCard(card); // TODO: Implement a stack/queue to resolve triggers in order. // Do this after the card resolution is over to avoid reentraint effects. this.state.TriggerManager.RunActivatedTriggers(this); CheckState(); OnBattleStateChanged(); } } else { yield return null; } } // End of hero turn yield return this.state.PlayArea.UseHeroDefaults(this.state.Heroes, this.state.ParticipantActivity, UseHeroDefault); // TODO: Implement a stack/queue to resolve triggers in order. // Do this after the card resolution is over to avoid reentraint effects. this.state.TriggerManager.RunActivatedTriggers(this); CheckState(); OnBattleStateChanged(); if (STABLE_HAND_STYLE) { this.state.HeroDeck.DiscardInExcessOf(3); } else { this.state.HeroDeck.DiscardHand(); } foreach (BattleHero hero in this.state.Heroes) { hero.TickStatuses(); } this.state.Clock.AdvanceToStep(RoundStep.EndOfHeroTurn); OnBattleStateChanged(); this.monsterIntentions.PauseIntentions(); } private IEnumerator RunMonsterTurn() { yield return new WaitForSeconds(0.5f); // Start of monster turn this.state.Clock.AdvanceToStep(RoundStep.StartOfMonsterTurn); this.state.ClearEmotionPlayed(); foreach (BattleMonster monster in this.state.Monsters) { monster.ResetBlock(); } OnBattleStateChanged(); yield return new WaitForSeconds(0.5f); // Monsters play an emotion List monsterHand = this.state.MonsterDeck.GetHandPile(); for (int i = 0; i < monsterHand.Count; i++) { MonsterCard monsterCard = monsterHand[i]; if (monsterCard.Type == CardType.Emotion && CanUseMonsterCard(monsterCard)) { yield return UseMonsterCard(monsterCard); // TODO: Implement a stack/queue to resolve triggers in order. // Do this after the card resolution is over to avoid reentraint effects. this.state.TriggerManager.RunActivatedTriggers(this); CheckState(); OnBattleStateChanged(); } } // Monsters play as many actions from the top as possible bool anyCardPlayed = true; while (anyCardPlayed) { anyCardPlayed = false; MonsterCard monsterCard = FirstMonsterActionToPlay(); if (monsterCard != null) { yield return UseMonsterCard(monsterCard); anyCardPlayed = true; // TODO: Implement a stack/queue to resolve triggers in order. // Do this after the card resolution is over to avoid reentraint effects. this.state.TriggerManager.RunActivatedTriggers(this); CheckState(); OnBattleStateChanged(); } } // End of monster turn if (STABLE_HAND_STYLE) { this.state.MonsterDeck.DiscardInExcessOf(3); } else { this.state.MonsterDeck.DiscardHand(); } foreach (BattleMonster monster in this.state.Monsters) { monster.TickStatuses(); } this.state.Clock.AdvanceToStep(RoundStep.EndOfMonsterTurn); OnBattleStateChanged(); } private MonsterCard FirstMonsterActionToPlay() { List monsterHand = monsterHand = this.state.MonsterDeck.GetHandPile(); for (int i = 0; i < monsterHand.Count; i++) { MonsterCard monsterCard = monsterHand[i]; if (monsterCard.Type != CardType.Emotion && CanUseMonsterCard(monsterCard)) { return monsterCard; } } return null; } public IEnumerator UseHeroCard(HeroCard card) { if (!CanUseHeroCard(card)) { throw new System.InvalidOperationException("Cannot use card " + card.GetDisplayName()); } this.state.HeroEnergy.PayForCard(card); this.state.HeroDeck.RemoveCardFromHand(card); this.state.ParticipantActivity.NotifyParticipantUsedCard(card.Owner, card); yield return card.Use(this, this.state, this.battlePlayerControls); if (card.Type == CardType.Default) { // Already added to playArea by effect } else if (card.IsPermanent()) { this.state.PlayArea.AddPlayedHeroPermanent(card); } else if (card.IsEphemeral()) { this.state.HeroDeck.MoveCardToOblivion(card); } else { this.state.HeroDeck.MoveCardToDiscard(card); } CheckState(); OnBattleStateChanged(); } public IEnumerator UseHeroDefault(DefaultHeroCard card) { if (!card.Owner.CanUseDefault()) { yield break; } yield return card.UseDefault(this, this.state, this.battlePlayerControls); CheckState(); OnBattleStateChanged(); } public IEnumerator UseMonsterCard(MonsterCard card) { this.state.MonsterEnergy.PayForCard(card); this.state.MonsterDeck.RemoveCardFromHand(card); this.state.ParticipantActivity.NotifyParticipantUsedCard(card.Owner, card); yield return card.Use(this, this.state); if (card.IsPermanent()) { this.state.PlayArea.AddPlayedMonsterPermanent(card); } else if (card.IsEphemeral()) { this.state.MonsterDeck.MoveCardToOblivion(card); } else { this.state.MonsterDeck.MoveCardToDiscard(card); } yield return new WaitForSeconds(0.5f); CheckState(); OnBattleStateChanged(); } public bool CanUseHeroCard(HeroCard card) { return this.heroHandPlayability.IsPlayableImmediately(card); } public bool CanUseMonsterCard(MonsterCard card) { return this.monsterHandPlayability.IsPlayableImmediately(card); } private void CheckState() { foreach (BattleParticipant p in this.state.Heroes) { p.CheckState(); } foreach (BattleParticipant p in this.state.Monsters) { p.CheckState(); } if (System.Array.TrueForAll(this.state.Heroes, h => !h.IsAlive())) { BattleResult.FinishBattle(new BattleResult(false)); } else if (System.Array.TrueForAll(this.state.Monsters, m => !m.IsAlive())) { BattleResult.FinishBattle(new BattleResult(true)); } } private BattleParticipant[] GetTeam(TeamChoice team) { return (team == TeamChoice.Heroes ? this.state.Heroes.Cast().ToArray() : this.state.Monsters.Cast().ToArray()); } public BattleState State {get => this.state;} // IBattleEffectApplier public void Apply(IBattleEffect effect) { effect.Apply(this); } // IBattleEffectResolver public void DropEmotion(DropEmotionEffect effect) { if (!this.state.CanPlayEmotion(effect.Team)) { throw new System.InvalidOperationException("Cannot play emotion"); } this.state.NotifyPlayedEmotion(effect.Team); EnergyPool pool = (effect.Team == TeamChoice.Heroes ? this.state.HeroEnergy : this.state.MonsterEnergy); pool.MaxEnergy += effect.Amount; pool.Energy += effect.Amount; } public void IncreaseHeroMaxEnergy(IncreaseMaxEnergyEffect effect) { this.state.HeroEnergy.MaxEnergy += effect.Amount; if (effect.Immediate) { this.state.HeroEnergy.Energy += effect.Amount; } } public void IncreaseMaxHP(IncreaseMaxHPEffect effect) { if (effect.Target == null) return; effect.Target.IncreaseMaxHP(effect.Amount); } public void ModifyInsight(ModifyInsightEffect effect) { this.state.InsightLevel.Modify(effect.Amount); } public void DrawCards(DrawCardsEffect effect) { if (effect.Team == TeamChoice.Heroes) { for (int i = 0; i < effect.Amount; i++) { this.state.HeroDeck.DrawCard(); } } else { for (int i = 0; i < effect.Amount; i++) { this.state.MonsterDeck.DrawCard(); } } } public void AcquireCards(AcquireCardsEffect effect) { if (effect.Participant == null) return; if (effect.Participant.Team == TeamChoice.Heroes) { this.AcquireCardsTyped(effect, this.state.HeroDeck, new HeroCardDatabase()); } else { this.AcquireCardsTyped(effect, this.state.MonsterDeck, new MonsterCardDatabase()); } } private void AcquireCardsTyped( AcquireCardsEffect effect, BattleDeck deck, CardDatabase cardDatabase) where BaseCardT : BaseCard { foreach (int cardId in effect.CardIds) { BaseCardT acquiredCard = cardDatabase.CreateCard(cardId, effect.Participant); switch (effect.Zone) { case BattleZone.DrawPile: deck.PutCardOnTopOfDrawPile(acquiredCard); break; case BattleZone.DiscardPile: deck.MoveCardToDiscard(acquiredCard); break; default: throw new System.NotImplementedException(); } } if (effect.Zone == BattleZone.DrawPile) { deck.ShuffleDrawPile(); } } public int DealDamage(DealDamageEffect effect) { if (effect.Target == null) return 0; int amount = effect.Amount; if (effect.Source != null) { amount += this.state.BuffManager.GetTotalBuffAmountOfType(effect.Source, BuffType.Power); } int damageDealt = effect.Target.DealDamage(amount); this.state.TriggerManager.NotifyResolvedDamageEffect(effect); return damageDealt; } public int DealMassDamage(MassDamageEffect effect) { int damageDealt = 0; BattleParticipant[] affectedTeam = GetTeam(effect.Team); foreach (BattleParticipant bp in affectedTeam) { damageDealt += bp.DealDamage(effect.Amount); } this.state.TriggerManager.NotifyResolvedMassDamageEffect(effect); return damageDealt; } public int Heal(HealEffect effect) { if (effect.Target == null) return 0; int healedAmount = effect.Target.Heal(effect.Amount); return healedAmount; } public void AddBlock(BlockEffect effect) { if (effect.Target == null) return; effect.Target.AddBlock(effect.Amount); } public void AddMassBlock(MassBlockEffect effect) { BattleParticipant[] affectedTeam = GetTeam(effect.Team); foreach (BattleParticipant bp in affectedTeam) { bp.AddBlock(effect.Amount); } } public void AddStatus(StatusEffect effect) { if (effect.Target == null) return; effect.Target.AddStatus(effect.Status, effect.Amount); } public void BuffParticipant(BuffParticipantEffect effect) { this.state.BuffManager.AddBuff(effect, this.state.Clock.Now); } public void Rotate(RotateEffect effect) { if (effect.Positions == 0) return; BattleParticipant[] currentPositions = GetTeam(effect.Team); int start = int.MaxValue; int end = int.MinValue; for (int i = 0; i < currentPositions.Length; i++) { BattleParticipant bp = currentPositions[i]; if (bp != null) { start = System.Math.Min(start, i); end = System.Math.Max(end, i); } } int[] newPositions = new int[currentPositions.Length]; for (int i = start; i <= end; i++) { if (i < start || i > end) { newPositions[i] = i; } else { int newI = start + MathUtil.TrueMod( i - start - effect.Positions, end - start + 1); newPositions[newI] = i; } } this.state.MoveTeamToNewPositions(effect.Team, newPositions); } public void SetupDefault(DefaultEffect effect) { DefaultHeroCard previousDefault = this.state.PlayArea.SetupDefault(effect); if (previousDefault != null) { this.state.HeroDeck.MoveCardToDiscard(previousDefault); } } public void ArmDamageTrigger(DamageTrigger damageTrigger) { this.state.TriggerManager.ArmDamageTrigger(damageTrigger); } }