mixedcards/Assets/Battle/BattleController.cs

611 lines
21 KiB
C#

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
public class BattleController : MonoBehaviour, IBattleEffectApplier, IBattleEffectResolver, MonsterViewpoint
{
// 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<HeroCard> heroDeck;
private BattleDeck<MonsterCard> monsterDeck;
private PlayArea playArea;
private BattleHero[] heroes;
private Dictionary<BattleHero, GameObject> heroSprites;
private Dictionary<BattleHero, HeroInfoUI> heroUIs;
private BattleMonster[] monsters;
private Dictionary<BattleMonster, GameObject> monsterSprites;
private Dictionary<BattleMonster, HeroInfoUI> monsterUIs;
private BattleState state;
private EnergyPool heroEnergy;
private EnergyPool monsterEnergy;
private InsightLevel insightLevel;
private HandPlayability<HeroCard> heroHandPlayability;
private delegate void BattleStateChangedAction();
private event BattleStateChangedAction OnBattleStateChanged;
void Awake() {
BattleParameters parameters = BattleSetup.RetrieveParameters();
if (parameters == null) {
Debug.Log("Using default battle");
MonsterId[] monsterIds = new MonsterId[3] {
MonsterId.Golem,
MonsterId.Bat,
MonsterId.Bat,
};
MCardId[] mCardIds = new MCardId[10] {
MCardId.GraniteDefense,
MCardId.GraniteDefense,
MCardId.GolemCharge,
MCardId.GolemFist,
MCardId.VampireFangs,
MCardId.VampireFangs,
MCardId.Whirlwind,
MCardId.VampireFangs,
MCardId.VampireFangs,
MCardId.Whirlwind,
};
int[] mCardOwnerIndexes = new int[10] {
0, 0, 0, 0,
1, 1, 1,
2, 2, 2,
};
parameters = new BattleParameters();
parameters.SetMonsters(monsterIds, mCardIds, mCardOwnerIndexes);
}
HeroId[] heroIds = new HeroId[3] {
HeroId.Alchemist,
HeroId.NPCGuard,
HeroId.NPCGuard,
};
HCardId[] cardIds = new HCardId[11] {
HCardId.Eagerness,
HCardId.Eagerness,
HCardId.LeadShot,
HCardId.LeadShot,
HCardId.SmokeBomb,
HCardId.Eagerness,
HCardId.SwordSlash,
HCardId.ShieldBlock,
HCardId.Eagerness,
HCardId.SwordSlash,
HCardId.ShieldBlock,
};
// HeroId[] heroIds = new HeroId[3] {
// HeroId.Brute,
// HeroId.Alchemist,
// HeroId.Shaman,
// };
// HCardId[] cardIds = new HCardId[10] {
// HCardId.CrashingPunch,
// HCardId.LightningHatchet,
// HCardId.BlindFury,
// HCardId.Cover,
// HCardId.Cover,
// HCardId.Thunderstorm,
// HCardId.Scarring,
// HCardId.Scarring,
// HCardId.Meditate,
// HCardId.IronskinDraught,
// };
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(1);
this.heroes = new BattleHero[heroIds.Length];
this.heroSprites = new Dictionary<BattleHero, GameObject>();
this.heroUIs = new Dictionary<BattleHero, HeroInfoUI>();
for (int i = 0; i < this.heroes.Length; i++) {
HeroId id = 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<BattleParticipantSprite>();
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<BattleMonster, GameObject>();
this.monsterUIs = new Dictionary<BattleMonster, HeroInfoUI>();
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<BattleParticipantSprite>();
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[cardIds.Length];
for (int i = 0; i < cardIds.Length; i++) {
cardOwners[i] = FindHeroByClass(HeroCardDatabase.GetHeroIdOfCard(cardIds[i]));
}
this.heroDeck = new BattleDeck<HeroCard>(
this.heroCardDatabase, cardIds.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]];
}
this.monsterDeck = new BattleDeck<MonsterCard>(
this.monsterCardDatabase, parameters.MCardIds.Cast<int>().ToArray(), mCardOwners);
this.playArea = new PlayArea();
this.heroHandPlayability = new HandPlayability<HeroCard>(
this.state, this.heroEnergy, TeamChoice.Heroes, this.heroDeck);
this.OnBattleStateChanged += this.heroHandPlayability.NotifyBattleStateChanged;
this.battlePlayerControls.Init(
this.heroDeck, this.monsterDeck, this.playArea, this.insightLevel,
this.heroHandPlayability, 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(-1 + -3 + 3 * i, -1.8f, 0);
} else {
pos = new Vector3(+1 + -3 + 3 * i, 1.2f, 0);
}
sprite.transform.position = pos;
}
private BattleParticipant FindHeroByClass(HeroId heroId) {
foreach (BattleHero battleHero in this.heroes) {
if (battleHero.GetId() == heroId) return battleHero;
}
return null;
}
// Start is called before the first frame update
void Start()
{
StartCoroutine(RunBattleRounds());
}
// Update is called once per frame
void Update()
{
}
private IEnumerator RunBattleRounds() {
while (true) {
yield return RunBattleRound();
}
}
private IEnumerator RunBattleRound() {
this.state.HeroTurn = true;
this.damageTriggers.RemoveAll(t => t.Expiration == TriggerExpiration.StartOfNextHeroTurn);
heroEnergy.Replenish();
monsterEnergy.Replenish();
this.heroDeck.DrawHand();
this.monsterDeck.DrawHand();
yield return RunHeroTurn();
this.state.HeroTurn = false;
yield return RunMonsterTurn();
}
private IEnumerator RunHeroTurn() {
// Start of hero turn
OnBattleStateChanged();
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
this.heroDeck.DiscardHand();
foreach (BattleHero hero in this.heroes) {
hero.TickStatuses();
}
OnBattleStateChanged();
}
private IEnumerator RunMonsterTurn() {
OnBattleStateChanged();
yield return new WaitForSeconds(0.5f);
// Start of monster turn
// TODO: Play emotions from hand
if (this.monsterEnergy.MaxEnergy < 9) {
this.monsterEnergy.DropOneEmotion();
}
foreach (BattleMonster monster in this.monsters) {
monster.ResetBlock();
}
OnBattleStateChanged();
// Monsters act until they pass
List<MonsterCard> monsterHand = this.monsterDeck.GetHandPile();
for (int i = 0; i < monsterHand.Count; i++) {
MonsterCard monsterCard = monsterHand[i];
if (CanUseMonsterCard(monsterCard)) {
yield return UseMonsterCard(monsterCard);
OnBattleStateChanged();
}
}
// End of monster turn
this.monsterDeck.DiscardHand();
foreach (BattleMonster monster in this.monsters) {
monster.TickStatuses();
}
OnBattleStateChanged();
}
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);
}
OnBattleStateChanged();
CheckState();
}
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();
}
public bool CanUseHeroCard(HeroCard card) {
return this.heroHandPlayability.IsPlayable(card);
}
public bool CanUseMonsterCard(MonsterCard card) {
return !this.state.HeroTurn && card.Owner.CanUseCard(card) && this.monsterEnergy.CanPayForCard(card);
}
public BattleDeck<HeroCard> 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())) {
Debug.Log("You lost!");
}
if (System.Array.TrueForAll(this.monsters, m => !m.IsAlive())) {
Debug.Log("You won!");
}
}
private BattleParticipant[] GetTeam(TeamChoice team) {
return (team == TeamChoice.Heroes
? this.heroes.Cast<BattleParticipant>().ToArray()
: this.monsters.Cast<BattleParticipant>().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<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 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;}
}