mixedcards/Assets/Battle/BattleController.cs
2020-10-13 01:00:49 -04:00

588 lines
20 KiB
C#

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<HeroCard> heroHandPlayability;
private HandPlayability<MonsterCard> monsterHandPlayability;
private TurnIntentions<MonsterCard> 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<HeroCard> heroDeck = new BattleDeck<HeroCard>(
this.heroCardDatabase, parameters.HCardIds.Cast<int>().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<MonsterCard> monsterDeck = new BattleDeck<MonsterCard>(
this.monsterCardDatabase, parameters.MCardIds.Cast<int>().ToArray(), mCardOwners);
this.state.SetDecks(heroDeck, monsterDeck);
this.participantActivity = new ParticipantActivity();
this.heroHandPlayability = new HandPlayability<HeroCard>(
this.state, this.state.HeroEnergy, TeamChoice.Heroes, this.state.HeroDeck);
this.OnBattleStateChanged += this.heroHandPlayability.NotifyBattleStateChanged;
this.monsterHandPlayability = new HandPlayability<MonsterCard>(
this.state, this.state.MonsterEnergy, TeamChoice.Monsters, this.state.MonsterDeck);
this.OnBattleStateChanged += this.monsterHandPlayability.NotifyBattleStateChanged;
this.monsterIntentions = new TurnIntentions<MonsterCard>(
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<CheatBattle>();
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<MonsterCard> 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<MonsterCard> 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<BattleParticipant>().ToArray()
: this.monsters.Cast<BattleParticipant>().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<BaseCardT>(
AcquireCardsEffect effect,
BattleDeck<BaseCardT> deck,
CardDatabase<BaseCardT> 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<Trigger> activatedTriggers = new List<Trigger>();
private List<DamageTrigger> damageTriggers = new List<DamageTrigger>();
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;}
}