using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.SceneManagement; public class BattleController : MonoBehaviour, IBattleEffectApplier, IBattleEffectResolver, MonsterViewpoint { private bool STABLE_HAND_STYLE = true; private int STARTING_INSIGHT = 3; // Inspector public public GameObject uiCanvas; public BattlePlayerControls battlePlayerControls; public EnergyUI heroEnergyUI; public EnergyUI monsterEnergyUI; public BattleSprites battleSprites; // Private private HeroCardDatabase heroCardDatabase; private MonsterCardDatabase monsterCardDatabase; private BattleHero[] heroes; private BattleMonster[] monsters; private BattleState state; private InsightLevel insightLevel; private HandPlayability heroHandPlayability; private HandPlayability monsterHandPlayability; private TurnIntentions monsterIntentions; private ParticipantActivity participantActivity; 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.heroCardDatabase = new HeroCardDatabase(); this.monsterCardDatabase = new MonsterCardDatabase(); this.state = new BattleState(); this.heroEnergyUI.Init(this.state.HeroEnergy); this.monsterEnergyUI.Init(this.state.MonsterEnergy); this.insightLevel = new InsightLevel(STARTING_INSIGHT); this.heroes = new BattleHero[parameters.HeroIds.Length]; for (int i = 0; i < this.heroes.Length; i++) { HeroId id = parameters.HeroIds[i]; BattleHero hero = new BattleHero(id); hero.Init(); this.heroes[i] = hero; } this.monsters = new BattleMonster[parameters.MonsterIds.Length]; for (int i = 0; i < this.monsters.Length; i++) { MonsterId id = parameters.MonsterIds[i]; BattleMonster monster = MonsterDatabase.createMonsterScript(id); monster.Init(); this.monsters[i] = monster; } this.battleSprites.Init(this.heroes, this.monsters, this.state); BattleParticipant[] cardOwners = new BattleParticipant[parameters.HCardIds.Length]; for (int i = 0; i < parameters.HCardIds.Length; i++) { cardOwners[i] = this.heroes[parameters.HCardOwnerIndexes[i]]; } BattleDeck heroDeck = new BattleDeck( this.heroCardDatabase, parameters.HCardIds.Cast().ToArray(), cardOwners); BattleParticipant[] mCardOwners = new BattleParticipant[parameters.MCardIds.Length]; for (int i = 0; i < parameters.MCardIds.Length; i++) { mCardOwners[i] = this.monsters[parameters.MCardOwnerIndexes[i]]; } BattleDeck monsterDeck = new BattleDeck( this.monsterCardDatabase, parameters.MCardIds.Cast().ToArray(), mCardOwners); this.state.SetDecks(heroDeck, monsterDeck); this.participantActivity = new ParticipantActivity(); 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.insightLevel, this.heroHandPlayability, this.monsterHandPlayability, this.monsterIntentions, this.uiCanvas.transform.localScale.x); this.insightLevel.OnChanged += this.battlePlayerControls.NotifyInsightLevelChanged; } // 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.insightLevel.Modify(+5); }; cheat.OnOneHPCode += delegate { foreach (BattleMonster m in this.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.damageTriggers.RemoveAll(t => t.Expiration == TriggerExpiration.StartOfNextHeroTurn); 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); this.participantActivity.NextRound(); } 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.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); } } else { yield return null; } } // End of hero turn yield return this.state.PlayArea.UseHeroDefaults(this.heroes, this.participantActivity, UseHeroDefault); if (STABLE_HAND_STYLE) { this.state.HeroDeck.DiscardInExcessOf(3); } else { this.state.HeroDeck.DiscardHand(); } foreach (BattleHero hero in this.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.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); } } // 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); OnBattleStateChanged(); anyCardPlayed = true; } } // End of monster turn if (STABLE_HAND_STYLE) { this.state.MonsterDeck.DiscardInExcessOf(3); } else { this.state.MonsterDeck.DiscardHand(); } foreach (BattleMonster monster in this.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.participantActivity.NotifyParticipantUsedCard(card.Owner, card); yield return card.Use(this, 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, battlePlayerControls); CheckState(); OnBattleStateChanged(); } public IEnumerator UseMonsterCard(MonsterCard card) { this.state.MonsterEnergy.PayForCard(card); this.state.MonsterDeck.RemoveCardFromHand(card); yield return card.Use(this, this); 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 EnqueueActivatedTrigger(Trigger trigger) { this.activatedTriggers.Add(trigger); } private void RunActivatedTriggers() { foreach (Trigger t in this.activatedTriggers) { t.Run(this); } this.activatedTriggers.Clear(); // Cleanup expired triggers this.damageTriggers.RemoveAll(t => t.Expired); } private void CheckState() { foreach (BattleParticipant p in this.heroes) { p.CheckState(); } foreach (BattleParticipant p in this.monsters) { p.CheckState(); } if (System.Array.TrueForAll(this.heroes, h => !h.IsAlive())) { BattleResult.FinishBattle(new BattleResult(false)); } else if (System.Array.TrueForAll(this.monsters, m => !m.IsAlive())) { BattleResult.FinishBattle(new BattleResult(true)); } } private BattleParticipant[] GetTeam(TeamChoice team) { return (team == TeamChoice.Heroes ? this.heroes.Cast().ToArray() : this.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.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, this.heroCardDatabase); } else { this.AcquireCardsTyped(effect, this.state.MonsterDeck, this.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(); } } private List activatedTriggers = new List(); private List damageTriggers = new List(); 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); foreach (DamageTrigger dt in this.damageTriggers) { if (dt.IsTriggeredBy(effect)) { EnqueueActivatedTrigger(dt); } } // Do this after the card resolution is over to avoid reentraint effects. RunActivatedTriggers(); 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); foreach (DamageTrigger dt in this.damageTriggers) { if (dt.IsTriggeredBy(effect)) { EnqueueActivatedTrigger(dt); } } } // Do this after the card resolution is over to avoid reentraint effects. RunActivatedTriggers(); 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 + TrueMod( i - start - effect.Positions, end - start + 1); newPositions[newI] = i; } } MoveTeamToNewPositions(effect.Team, newPositions); } private static int TrueMod(int a, int b) { return (a % b + b) % b; } private void MoveTeamToNewPositions(TeamChoice team, int[] newPositions) { int n = newPositions.Length; if (team == TeamChoice.Heroes) { BattleHero[] newHeroes = new BattleHero[n]; for (int i = 0; i < n; i++) { BattleHero hero = this.heroes[i]; newHeroes[newPositions[i]] = hero; this.battleSprites.PositionHero(hero, newPositions[i]); } this.heroes = newHeroes; } else { BattleMonster[] newMonsters = new BattleMonster[n]; for (int i = 0; i < n; i++) { BattleMonster monster = this.monsters[i]; newMonsters[newPositions[i]] = monster; this.battleSprites.PositionMonster(monster, newPositions[i]); } this.monsters = newMonsters; } } 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.damageTriggers.Add(damageTrigger); } // MonsterViewpoint public BattleHero[] Heroes {get => this.heroes;} public BattleMonster[] Monsters {get => this.monsters;} }