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 HeroInfoUI heroUIPrefab; public GameObject uiCanvas; public BattlePlayerControls battlePlayerControls; public EnergyUI heroEnergyUI; public EnergyUI monsterEnergyUI; public Material hitEffectMaterial; public RuntimeAnimatorController runtimeAnimatorController; // Private private HeroCardDatabase heroCardDatabase; private MonsterCardDatabase monsterCardDatabase; private BattleDeck heroDeck; private BattleDeck monsterDeck; private PlayArea playArea; private BattleHero[] heroes; private Dictionary heroSprites; private Dictionary heroUIs; private BattleMonster[] monsters; private Dictionary monsterSprites; private Dictionary monsterUIs; private BattleState state; private EnergyPool heroEnergy; private EnergyPool monsterEnergy; private InsightLevel insightLevel; 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.heroCardDatabase = new HeroCardDatabase(); this.monsterCardDatabase = new MonsterCardDatabase(); this.state = new BattleState(); this.heroEnergy = new EnergyPool(); this.heroEnergyUI.Init(this.heroEnergy); this.heroEnergy.OnEnergyChanged += this.heroEnergyUI.NotifyEnergyChanged; this.monsterEnergy = new EnergyPool(); this.monsterEnergyUI.Init(this.monsterEnergy); this.monsterEnergy.OnEnergyChanged += this.monsterEnergyUI.NotifyEnergyChanged; this.insightLevel = new InsightLevel(STARTING_INSIGHT); this.heroes = new BattleHero[parameters.HeroIds.Length]; this.heroSprites = new Dictionary(); this.heroUIs = new Dictionary(); 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; GameObject sprite = HeroDatabase.createHeroSprite(id); this.heroSprites[hero] = sprite; sprite.name = "Hero" + i + "_Sprite"; PositionSprite(TeamChoice.Heroes, sprite, i); BattleParticipantSprite bps = sprite.AddComponent(); bps.Init(battlePlayerControls, hero, this.hitEffectMaterial, this.runtimeAnimatorController); bps.OnSpriteClicked += battlePlayerControls.NotifyParticipantSpriteClicked; hero.OnHit += bps.NotifyHit; hero.OnDeath += bps.NotifyDeath; HeroInfoUI heroUI = (HeroInfoUI) Instantiate(this.heroUIPrefab.gameObject, this.uiCanvas.transform).GetComponent(typeof(HeroInfoUI)); heroUI.Init(sprite, hero); this.heroUIs[hero] = heroUI; } this.monsters = new BattleMonster[parameters.MonsterIds.Length]; this.monsterSprites = new Dictionary(); this.monsterUIs = new Dictionary(); for (int i = 0; i < 3; i++) { MonsterId id = parameters.MonsterIds[i]; BattleMonster monster = MonsterDatabase.createMonsterScript(id); monster.Init(); this.monsters[i] = monster; GameObject sprite = MonsterDatabase.createMonsterSprite(id); this.monsterSprites[monster] = sprite; sprite.name = "Monster" + i + "_Sprite"; PositionSprite(TeamChoice.Monsters, sprite, i); BattleParticipantSprite bps = sprite.AddComponent(); bps.Init(battlePlayerControls, monster, this.hitEffectMaterial, this.runtimeAnimatorController); bps.OnSpriteClicked += battlePlayerControls.NotifyParticipantSpriteClicked; monster.OnHit += bps.NotifyHit; monster.OnDeath += bps.NotifyDeath; HeroInfoUI monsterUI = (HeroInfoUI) Instantiate(this.heroUIPrefab.gameObject, this.uiCanvas.transform).GetComponent(typeof(HeroInfoUI)); monsterUI.Init(sprite, monster); this.monsterUIs[monster] = monsterUI; } BattleParticipant[] cardOwners = new BattleParticipant[parameters.HCardIds.Length]; for (int i = 0; i < parameters.HCardIds.Length; i++) { cardOwners[i] = this.heroes[parameters.HCardOwnerIndexes[i]]; } this.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]]; } this.monsterDeck = new BattleDeck( this.monsterCardDatabase, parameters.MCardIds.Cast().ToArray(), mCardOwners); this.playArea = new PlayArea(); this.heroHandPlayability = new HandPlayability( this.state, this.heroEnergy, TeamChoice.Heroes, this.heroDeck); this.OnBattleStateChanged += this.heroHandPlayability.NotifyBattleStateChanged; this.monsterHandPlayability = new HandPlayability( this.state, this.monsterEnergy, TeamChoice.Monsters, this.monsterDeck); this.OnBattleStateChanged += this.monsterHandPlayability.NotifyBattleStateChanged; this.monsterIntentions = new TurnIntentions( this.monsterDeck, this.monsterEnergy, this.monsterHandPlayability); this.battlePlayerControls.Init( this.heroDeck, this.monsterDeck, this.playArea, this.insightLevel, this.heroHandPlayability, this.monsterHandPlayability, this.monsterIntentions, this.uiCanvas.transform.localScale.x); this.insightLevel.OnChanged += this.battlePlayerControls.NotifyInsightLevelChanged; } private void PositionSprite(TeamChoice team, GameObject sprite, int i) { Vector3 pos; if (team == TeamChoice.Heroes) { pos = new Vector3(-0.8f + -3 + 3 * i, -1.8f, 0); } else { pos = new Vector3(+0.8f + -3 + 3 * i, 1.2f, 0); } sprite.transform.position = pos; } // 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.heroDeck.DrawCards(7); this.monsterDeck.DrawCards(7); } while (true) { yield return RunBattleRound(); } } private IEnumerator RunBattleRound() { this.state.AdvanceToTeamTurn(TeamChoice.Heroes); this.damageTriggers.RemoveAll(t => t.Expiration == TriggerExpiration.StartOfNextHeroTurn); heroEnergy.Replenish(); monsterEnergy.Replenish(); if (STABLE_HAND_STYLE) { if (this.state.Round > 1) { this.heroDeck.DrawCards(2); this.monsterDeck.DrawCards(2); } } else { this.heroDeck.DrawCards(5); this.monsterDeck.DrawCards(5); } yield return RunHeroTurn(); this.state.AdvanceToTeamTurn(TeamChoice.Monsters); yield return RunMonsterTurn(); } private IEnumerator RunHeroTurn() { // Start of hero turn 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 if (STABLE_HAND_STYLE) { this.heroDeck.DiscardInExcessOf(7); } else { this.heroDeck.DiscardHand(); } foreach (BattleHero hero in this.heroes) { hero.TickStatuses(); } OnBattleStateChanged(); this.monsterIntentions.PauseIntentions(); } private IEnumerator RunMonsterTurn() { OnBattleStateChanged(); yield return new WaitForSeconds(0.5f); // Start of monster turn foreach (BattleMonster monster in this.monsters) { monster.ResetBlock(); } OnBattleStateChanged(); // Monsters play an emotion List monsterHand = this.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.monsterDeck.DiscardInExcessOf(7); } else { this.monsterDeck.DiscardHand(); } foreach (BattleMonster monster in this.monsters) { monster.TickStatuses(); } OnBattleStateChanged(); } private MonsterCard FirstMonsterActionToPlay() { List monsterHand = monsterHand = this.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.heroEnergy.PayForCard(card); this.heroDeck.RemoveCardFromHand(card); yield return card.Use(this, this, battlePlayerControls); if (card.IsPermanent()) { this.playArea.AddPlayedHeroPermanent(card); } else if (card.IsEphemeral()) { this.heroDeck.MoveCardToOblivion(card); } else { this.heroDeck.MoveCardToDiscard(card); } CheckState(); OnBattleStateChanged(); } public IEnumerator UseMonsterCard(MonsterCard card) { this.monsterEnergy.PayForCard(card); this.monsterDeck.RemoveCardFromHand(card); yield return card.Use(this, this); if (card.IsPermanent()) { this.playArea.AddPlayedMonsterPermanent(card); } else if (card.IsEphemeral()) { this.monsterDeck.MoveCardToOblivion(card); } else { this.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); } public BattleDeck GetHeroDeck() { return this.heroDeck; } public PlayArea GetPlayArea() { return this.playArea; } 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 EnergyPool HeroEnergy {get => this.heroEnergy;} public EnergyPool MonsterEnergy {get => this.monsterEnergy;} // 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.heroEnergy : this.monsterEnergy); pool.MaxEnergy += effect.Amount; pool.Energy += effect.Amount; } public void IncreaseHeroMaxEnergy(IncreaseMaxEnergyEffect effect) { this.heroEnergy.MaxEnergy += effect.Amount; if (effect.Immediate) { this.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.heroDeck.DrawCard(); } } else { for (int i = 0; i < effect.Amount; i++) { this.monsterDeck.DrawCard(); } } } public void AcquireCards(AcquireCardsEffect effect) { if (effect.Participant == null) return; if (effect.Participant.Team == TeamChoice.Heroes) { this.AcquireCardsTyped(effect, this.heroDeck, this.heroCardDatabase); } else { this.AcquireCardsTyped(effect, this.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 damageDealt = effect.Target.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 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 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; PositionSprite(team, heroSprites[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; PositionSprite(team, monsterSprites[monster], newPositions[i]); } this.monsters = newMonsters; } } public void ArmDamageTrigger(DamageTrigger damageTrigger) { this.damageTriggers.Add(damageTrigger); } // MonsterViewpoint public BattleHero[] Heroes {get => this.heroes;} public BattleMonster[] Monsters {get => this.monsters;} }