using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Events; using UnityEngine.Profiling; namespace TcgEngine.Gameplay { /// /// Execute and resolves game rules and logic /// public class GameLogic { public UnityAction onGameStart; public UnityAction onGameEnd; //Winner public UnityAction onTurnStart; public UnityAction onTurnPlay; public UnityAction onTurnEnd; public UnityAction onCardPlayed; public UnityAction onCardSummoned; public UnityAction onCardMoved; public UnityAction onCardTransformed; public UnityAction onCardDiscarded; public UnityAction onCardDrawn; public UnityAction onRollValue; public UnityAction onAbilityStart; public UnityAction onAbilityTargetCard; //Ability, Caster, Target public UnityAction onAbilityTargetPlayer; public UnityAction onAbilityTargetSlot; public UnityAction onAbilityEnd; public UnityAction onAttackStart; //Attacker, Defender public UnityAction onAttackEnd; //Attacker, Defender public UnityAction onAttackPlayerStart; public UnityAction onAttackPlayerEnd; public UnityAction onCardDamaged; public UnityAction onCardHealed; public UnityAction onPlayerDamaged; public UnityAction onPlayerHealed; public UnityAction onSecretTrigger; //Secret, Triggerer public UnityAction onSecretResolve; //Secret, Triggerer public UnityAction onRefresh; private Game game_data; private ResolveQueue resolve_queue; private bool is_ai_predict = false; private System.Random random = new System.Random(); private ListSwap card_array = new ListSwap(); private ListSwap player_array = new ListSwap(); private ListSwap slot_array = new ListSwap(); private ListSwap card_data_array = new ListSwap(); private List cards_to_clear = new List(); public GameLogic(bool is_ai) { //is_instant ignores all gameplay delays and process everything immediately, needed for AI prediction resolve_queue = new ResolveQueue(null, is_ai); is_ai_predict = is_ai; } public GameLogic(Game game) { game_data = game; resolve_queue = new ResolveQueue(game, false); } public virtual void SetData(Game game) { game_data = game; resolve_queue.SetData(game); } public virtual void Update(float delta) { resolve_queue.Update(delta); } //----- Turn Phases ---------- public virtual void StartGame() { if (game_data.state == GameState.GameEnded) return; //Choose first player game_data.state = GameState.Play; game_data.first_player = random.NextDouble() < 0.5 ? 0 : 1; game_data.current_player = game_data.first_player; game_data.turn_count = 1; //Adventure settings bool should_mulligan = GameplayData.Get().mulligan; LevelData level = game_data.settings.GetLevel(); if (level != null) { if (level != null && level.first_player == LevelFirst.Player) game_data.first_player = 0; if (level != null && level.first_player == LevelFirst.AI) game_data.first_player = 1; game_data.current_player = game_data.first_player; should_mulligan = level.mulligan; } //Init each players foreach (Player player in game_data.players) { //Puzzle level deck DeckPuzzleData pdeck = DeckPuzzleData.Get(player.deck); //Hp / mana player.hp_max = pdeck != null ? pdeck.start_hp : GameplayData.Get().hp_start; player.hp = player.hp_max; player.mana_max = pdeck != null ? pdeck.start_mana : GameplayData.Get().mana_start; player.mana = player.mana_max; //Draw starting cards 开始抽卡 int dcards = pdeck != null ? pdeck.start_cards : GameplayData.Get().cards_start; DrawCard(player, dcards); //Add coin second player bool is_random = level == null || level.first_player == LevelFirst.Random; if (is_random && player.player_id != game_data.first_player && GameplayData.Get().second_bonus != null) { Card card = Card.Create(GameplayData.Get().second_bonus, VariantData.GetDefault(), player); player.cards_hand.Add(card); } } //Start state RefreshData(); onGameStart?.Invoke(); if(should_mulligan) GoToMulligan(); else StartTurn(); } public virtual void StartTurn() { if (game_data.state == GameState.GameEnded) return; ClearTurnData(); game_data.phase = GamePhase.StartTurn; RefreshData(); onTurnStart?.Invoke(); Player player = game_data.GetActivePlayer(); //Cards draw if (game_data.turn_count > 1 || player.player_id != game_data.first_player) { DrawCard(player, GameplayData.Get().cards_per_turn); } //Mana player.mana_max += GameplayData.Get().mana_per_turn; player.mana_max = Mathf.Min(player.mana_max, GameplayData.Get().mana_max); player.mana = player.mana_max; //Turn timer and history game_data.turn_timer = GameplayData.Get().turn_duration; player.history_list.Clear(); //Player poison if (player.HasStatus(StatusType.Poisoned)) player.hp -= player.GetStatusValue(StatusType.Poisoned); if (player.hero != null) player.hero.Refresh(); //Refresh Cards and Status Effects for (int i = player.cards_board.Count - 1; i >= 0; i--) { Card card = player.cards_board[i]; if (!card.HasStatus(StatusType.Sleep)) card.Refresh(); if (card.HasStatus(StatusType.Poisoned)) DamageCard(card, card.GetStatusValue(StatusType.Poisoned)); } //Ongoing Abilities UpdateOngoing(); //StartTurn Abilities TriggerPlayerCardsAbilityType(player, AbilityTrigger.StartOfTurn); TriggerPlayerSecrets(player, AbilityTrigger.StartOfTurn); resolve_queue.AddCallback(StartMainPhase); resolve_queue.ResolveAll(0.2f); } public virtual void StartNextTurn() { if (game_data.state == GameState.GameEnded) return; game_data.current_player = (game_data.current_player + 1) % game_data.settings.nb_players; if (game_data.current_player == game_data.first_player) game_data.turn_count++; CheckForWinner(); StartTurn(); } public virtual void StartMainPhase() { if (game_data.state == GameState.GameEnded) return; game_data.phase = GamePhase.Main; onTurnPlay?.Invoke(); RefreshData(); } public virtual void EndTurn() { if (game_data.state == GameState.GameEnded) return; if (game_data.phase != GamePhase.Main) return; game_data.selector = SelectorType.None; game_data.phase = GamePhase.EndTurn; //Reduce status effects with duration foreach (Player aplayer in game_data.players) { aplayer.ReduceStatusDurations(); foreach (Card card in aplayer.cards_board) card.ReduceStatusDurations(); foreach (Card card in aplayer.cards_equip) card.ReduceStatusDurations(); } //End of turn abilities Player player = game_data.GetActivePlayer(); TriggerPlayerCardsAbilityType(player, AbilityTrigger.EndOfTurn); onTurnEnd?.Invoke(); RefreshData(); resolve_queue.AddCallback(StartNextTurn); resolve_queue.ResolveAll(0.2f); } //End game with winner public virtual void EndGame(int winner) { if (game_data.state != GameState.GameEnded) { game_data.state = GameState.GameEnded; game_data.phase = GamePhase.None; game_data.selector = SelectorType.None; game_data.current_player = winner; //Winner player resolve_queue.Clear(); Player player = game_data.GetPlayer(winner); onGameEnd?.Invoke(player); RefreshData(); } } //Progress to the next step/phase public virtual void NextStep() { if (game_data.state == GameState.GameEnded) return; if (game_data.phase == GamePhase.Mulligan) { StartTurn(); return; } CancelSelection(); //Add to resolve queue in case its still resolving resolve_queue.AddCallback(EndTurn); resolve_queue.ResolveAll(); } //Check if a player is winning the game, if so end the game //Change or edit this function for a new win condition protected virtual void CheckForWinner() { int count_alive = 0; Player alive = null; foreach (Player player in game_data.players) { if (!player.IsDead()) { alive = player; count_alive++; } } if (count_alive == 0) { EndGame(-1); //Everyone is dead, Draw } else if (count_alive == 1) { EndGame(alive.player_id); //Player win } } protected virtual void ClearTurnData() { game_data.selector = SelectorType.None; resolve_queue.Clear(); card_array.Clear(); player_array.Clear(); slot_array.Clear(); card_data_array.Clear(); game_data.last_played = null; game_data.last_destroyed = null; game_data.last_target = null; game_data.last_summoned = null; game_data.ability_triggerer = null; game_data.selected_value = 0; game_data.ability_played.Clear(); game_data.cards_attacked.Clear(); } //--- Setup ------ //Set deck using a Deck in Resources 使用玩家卡组作为卡组 public virtual void SetPlayerDeck(Player player, DeckData deck) { player.cards_all.Clear(); player.cards_deck.Clear(); player.deck = deck.id; player.hero = null; VariantData variant = VariantData.GetDefault(); if (deck.hero != null) { player.hero = Card.Create(deck.hero, variant, player); } foreach (CardData card in deck.cards) { if (card != null) { Card acard = Card.Create(card, variant, player); player.cards_deck.Add(acard); } } DeckPuzzleData puzzle = deck as DeckPuzzleData; //Board cards 公共牌 if (puzzle != null) { foreach (DeckCardSlot card in puzzle.board_cards) { Card acard = Card.Create(card.card, variant, player); acard.slot = new Slot(card.slot, Slot.GetP(player.player_id)); player.cards_board.Add(acard); } } //Shuffle deck if (puzzle == null || !puzzle.dont_shuffle_deck) ShuffleDeck(player.cards_deck); } //Set deck using custom deck in save file or database public virtual void SetPlayerDeck(Player player, UserDeckData deck) { player.cards_all.Clear(); player.cards_deck.Clear(); player.deck = deck.tid; player.hero = null; if (deck.hero != null) { CardData hdata = CardData.Get(deck.hero.tid); VariantData hvariant = VariantData.Get(deck.hero.variant); if (hdata != null && hvariant != null) player.hero = Card.Create(hdata, hvariant, player); } foreach (UserCardData card in deck.cards) { CardData icard = CardData.Get(card.tid); VariantData variant = VariantData.Get(card.variant); if (icard != null && variant != null) { for (int i = 0; i < card.quantity; i++) { Card acard = Card.Create(icard, variant, player); player.cards_deck.Add(acard); } } } //Shuffle deck ShuffleDeck(player.cards_deck); } //---- Gameplay Actions -------------- public virtual void PlayCard(Card card, Slot slot, bool skip_cost = false) { if (game_data.CanPlayCard(card, slot, skip_cost)) { Player player = game_data.GetPlayer(card.player_id); //Cost if (!skip_cost) player.PayMana(card); //Play card player.RemoveCardFromAllGroups(card); //Add to board CardData icard = card.CardData; if (icard.IsBoardCard()) { player.cards_board.Add(card); card.slot = slot; card.exhausted = true; //Cant attack first turn } else if (icard.IsEquipment()) { Card bearer = game_data.GetSlotCard(slot); EquipCard(bearer, card); card.exhausted = true; } else if (icard.IsSecret()) { player.cards_secret.Add(card); } else { player.cards_discard.Add(card); card.slot = slot; //Save slot in case spell has PlayTarget } //History if (!is_ai_predict && !icard.IsSecret()) player.AddHistory(GameAction.PlayCard, card); //Update ongoing effects game_data.last_played = card.uid; UpdateOngoing(); //Trigger abilities if (card.CardData.IsDynamicManaCost()) { GoToSelectorCost(card); } else { TriggerSecrets(AbilityTrigger.OnPlayOther, card); //After playing card TriggerCardAbilityType(AbilityTrigger.OnPlay, card); TriggerOtherCardsAbilityType(AbilityTrigger.OnPlayOther, card); } RefreshData(); onCardPlayed?.Invoke(card, slot); resolve_queue.ResolveAll(0.3f); } } public virtual void MoveCard(Card card, Slot slot, bool skip_cost = false) { if (game_data.CanMoveCard(card, slot, skip_cost)) { card.slot = slot; //Moving doesn't really have any effect in demo so can be done indefinitely //if(!skip_cost) //card.exhausted = true; //card.RemoveStatus(StatusEffect.Stealth); //player.AddHistory(GameAction.Move, card); //Also move the equipment Card equip = game_data.GetEquipCard(card.equipped_uid); if (equip != null) equip.slot = slot; UpdateOngoing(); RefreshData(); onCardMoved?.Invoke(card, slot); resolve_queue.ResolveAll(0.2f); } } public virtual void CastAbility(Card card, AbilityData iability) { if (game_data.CanCastAbility(card, iability)) { Player player = game_data.GetPlayer(card.player_id); if (!is_ai_predict && iability.target != AbilityTarget.SelectTarget) player.AddHistory(GameAction.CastAbility, card, iability); card.RemoveStatus(StatusType.Stealth); TriggerCardAbility(iability, card); resolve_queue.ResolveAll(); } } public virtual void AttackTarget(Card attacker, Card target, bool skip_cost = false) { if (game_data.CanAttackTarget(attacker, target, skip_cost)) { Player player = game_data.GetPlayer(attacker.player_id); if (!is_ai_predict) player.AddHistory(GameAction.Attack, attacker, target); game_data.last_target = target.uid; //Trigger before attack abilities TriggerCardAbilityType(AbilityTrigger.OnBeforeAttack, attacker, target); TriggerCardAbilityType(AbilityTrigger.OnBeforeDefend, target, attacker); TriggerSecrets(AbilityTrigger.OnBeforeAttack, attacker); TriggerSecrets(AbilityTrigger.OnBeforeDefend, target); //Resolve attack resolve_queue.AddAttack(attacker, target, ResolveAttack, skip_cost); resolve_queue.ResolveAll(); } } protected virtual void ResolveAttack(Card attacker, Card target, bool skip_cost) { if (!game_data.IsOnBoard(attacker) || !game_data.IsOnBoard(target)) return; onAttackStart?.Invoke(attacker, target); attacker.RemoveStatus(StatusType.Stealth); UpdateOngoing(); resolve_queue.AddAttack(attacker, target, ResolveAttackHit, skip_cost); resolve_queue.ResolveAll(0.3f); } protected virtual void ResolveAttackHit(Card attacker, Card target, bool skip_cost) { //Count attack damage int datt1 = attacker.GetAttack(); int datt2 = target.GetAttack(); //Damage Cards DamageCard(attacker, target, datt1); //Counter Damage if (!attacker.HasStatus(StatusType.Intimidate)) DamageCard(target, attacker, datt2); //Save attack and exhaust if (!skip_cost) ExhaustBattle(attacker); //Recalculate bonus UpdateOngoing(); //Abilities bool att_board = game_data.IsOnBoard(attacker); bool def_board = game_data.IsOnBoard(target); if (att_board) TriggerCardAbilityType(AbilityTrigger.OnAfterAttack, attacker, target); if (def_board) TriggerCardAbilityType(AbilityTrigger.OnAfterDefend, target, attacker); if (att_board) TriggerSecrets(AbilityTrigger.OnAfterAttack, attacker); if (def_board) TriggerSecrets(AbilityTrigger.OnAfterDefend, target); onAttackEnd?.Invoke(attacker, target); RefreshData(); CheckForWinner(); resolve_queue.ResolveAll(0.2f); } public virtual void AttackPlayer(Card attacker, Player target, bool skip_cost = false) { if (attacker == null || target == null) return; if (!game_data.CanAttackTarget(attacker, target, skip_cost)) return; Player player = game_data.GetPlayer(attacker.player_id); if (!is_ai_predict) player.AddHistory(GameAction.AttackPlayer, attacker, target); //Resolve abilities TriggerSecrets(AbilityTrigger.OnBeforeAttack, attacker); TriggerCardAbilityType(AbilityTrigger.OnBeforeAttack, attacker, target); //Resolve attack resolve_queue.AddAttack(attacker, target, ResolveAttackPlayer, skip_cost); resolve_queue.ResolveAll(); } protected virtual void ResolveAttackPlayer(Card attacker, Player target, bool skip_cost) { if (!game_data.IsOnBoard(attacker)) return; onAttackPlayerStart?.Invoke(attacker, target); attacker.RemoveStatus(StatusType.Stealth); UpdateOngoing(); resolve_queue.AddAttack(attacker, target, ResolveAttackPlayerHit, skip_cost); resolve_queue.ResolveAll(0.3f); } protected virtual void ResolveAttackPlayerHit(Card attacker, Player target, bool skip_cost) { DamagePlayer(attacker, target, attacker.GetAttack()); //Save attack and exhaust if (!skip_cost) ExhaustBattle(attacker); //Recalculate bonus UpdateOngoing(); if (game_data.IsOnBoard(attacker)) TriggerCardAbilityType(AbilityTrigger.OnAfterAttack, attacker, target); TriggerSecrets(AbilityTrigger.OnAfterAttack, attacker); onAttackPlayerEnd?.Invoke(attacker, target); RefreshData(); CheckForWinner(); resolve_queue.ResolveAll(0.2f); } //Exhaust after battle public virtual void ExhaustBattle(Card attacker) { bool attacked_before = game_data.cards_attacked.Contains(attacker.uid); game_data.cards_attacked.Add(attacker.uid); bool attack_again = attacker.HasStatus(StatusType.Fury) && !attacked_before; attacker.exhausted = !attack_again; } //Redirect attack to a new target public virtual void RedirectAttack(Card attacker, Card new_target) { foreach (AttackQueueElement att in resolve_queue.GetAttackQueue()) { if (att.attacker.uid == attacker.uid) { att.target = new_target; att.ptarget = null; att.callback = ResolveAttack; att.pcallback = null; } } } public virtual void RedirectAttack(Card attacker, Player new_target) { foreach (AttackQueueElement att in resolve_queue.GetAttackQueue()) { if (att.attacker.uid == attacker.uid) { att.ptarget = new_target; att.target = null; att.pcallback = ResolveAttackPlayer; att.callback = null; } } } public virtual void ShuffleDeck(List cards) { for (int i = 0; i < cards.Count; i++) { Card temp = cards[i]; int randomIndex = random.Next(i, cards.Count); cards[i] = cards[randomIndex]; cards[randomIndex] = temp; } } //Draw card 抽卡 public virtual void DrawCard(Player player, int nb = 1) { for (int i = 0; i < nb; i++) { if (player.cards_deck.Count > 0 && player.cards_hand.Count < GameplayData.Get().cards_max) { Card card = player.cards_deck[0]; player.cards_deck.RemoveAt(0); player.cards_hand.Add(card); } } onCardDrawn?.Invoke(nb); } //Put a card from deck into discard public virtual void DrawDiscardCard(Player player, int nb = 1) { for (int i = 0; i < nb; i++) { if (player.cards_deck.Count > 0) { Card card = player.cards_deck[0]; player.cards_deck.RemoveAt(0); player.cards_discard.Add(card); } } } //Summon copy of an exiting card public virtual Card SummonCopy(Player player, Card copy, Slot slot) { CardData icard = copy.CardData; return SummonCard(player, icard, copy.VariantData, slot); } //Summon copy of an exiting card into hand public virtual Card SummonCopyHand(Player player, Card copy) { CardData icard = copy.CardData; return SummonCardHand(player, icard, copy.VariantData); } //Create a new card and send it to the board public virtual Card SummonCard(Player player, CardData card, VariantData variant, Slot slot) { if (!slot.IsValid()) return null; if (game_data.GetSlotCard(slot) != null) return null; Card acard = SummonCardHand(player, card, variant); PlayCard(acard, slot, true); onCardSummoned?.Invoke(acard, slot); return acard; } //Create a new card and send it to your hand public virtual Card SummonCardHand(Player player, CardData card, VariantData variant) { Card acard = Card.Create(card, variant, player); player.cards_hand.Add(acard); game_data.last_summoned = acard.uid; return acard; } //Transform card into another one public virtual Card TransformCard(Card card, CardData transform_to) { card.SetCard(transform_to, card.VariantData); onCardTransformed?.Invoke(card); return card; } public virtual void EquipCard(Card card, Card equipment) { if (card != null && equipment != null && card.player_id == equipment.player_id) { if (!card.CardData.IsEquipment() && equipment.CardData.IsEquipment()) { UnequipAll(card); //Unequip previous cards, only 1 equip at a time Player player = game_data.GetPlayer(card.player_id); player.RemoveCardFromAllGroups(equipment); player.cards_equip.Add(equipment); card.equipped_uid = equipment.uid; equipment.slot = card.slot; } } } public virtual void UnequipAll(Card card) { if (card != null && card.equipped_uid != null) { Player player = game_data.GetPlayer(card.player_id); Card equip = player.GetEquipCard(card.equipped_uid); if (equip != null) { card.equipped_uid = null; DiscardCard(equip); } } } //Change owner of a card public virtual void ChangeOwner(Card card, Player owner) { if (card.player_id != owner.player_id) { Player powner = game_data.GetPlayer(card.player_id); powner.RemoveCardFromAllGroups(card); powner.cards_all.Remove(card.uid); owner.cards_all[card.uid] = card; card.player_id = owner.player_id; } } //Damage a player public virtual void DamagePlayer(Card attacker, Player target, int value) { //Damage player target.hp -= value; target.hp = Mathf.Clamp(target.hp, 0, target.hp_max); //Lifesteal Player aplayer = game_data.GetPlayer(attacker.player_id); if (attacker.HasStatus(StatusType.LifeSteal)) aplayer.hp += value; onPlayerDamaged?.Invoke(target, value); } //Heal a card public virtual void HealCard(Card target, int value) { if (target == null) return; if (target.HasStatus(StatusType.Invincibility)) return; target.damage -= value; target.damage = Mathf.Max(target.damage, 0); onCardHealed?.Invoke(target, value); } public virtual void HealPlayer(Player target, int value) { if (target == null) return; target.hp += value; target.hp = Mathf.Clamp(target.hp, 0, target.hp_max); onPlayerHealed?.Invoke(target, value); } //Generic damage that doesnt come from another card public virtual void DamageCard(Card target, int value) { if (target == null) return; if (target.HasStatus(StatusType.Invincibility)) return; //Invincible if (target.HasStatus(StatusType.SpellImmunity)) return; //Spell immunity target.damage += value; onCardDamaged?.Invoke(target, value); if (target.GetHP() <= 0) DiscardCard(target); } //Damage a card with attacker/caster public virtual void DamageCard(Card attacker, Card target, int value, bool spell_damage = false) { if (attacker == null || target == null) return; if (target.HasStatus(StatusType.Invincibility)) return; //Invincible if (target.HasStatus(StatusType.SpellImmunity) && attacker.CardData.type != CardType.Character) return; //Spell immunity //Shell bool doublelife = target.HasStatus(StatusType.Shell); if (doublelife && value > 0) { target.RemoveStatus(StatusType.Shell); return; } //Armor if (!spell_damage && target.HasStatus(StatusType.Armor)) value = Mathf.Max(value - target.GetStatusValue(StatusType.Armor), 0); //Damage int damage_max = Mathf.Min(value, target.GetHP()); int extra = value - target.GetHP(); target.damage += value; //Trample Player tplayer = game_data.GetPlayer(target.player_id); if (!spell_damage && extra > 0 && attacker.player_id == game_data.current_player && attacker.HasStatus(StatusType.Trample)) tplayer.hp -= extra; //Lifesteal Player player = game_data.GetPlayer(attacker.player_id); if (!spell_damage && attacker.HasStatus(StatusType.LifeSteal)) player.hp += damage_max; //Remove sleep on damage target.RemoveStatus(StatusType.Sleep); //Callback onCardDamaged?.Invoke(target, value); //Deathtouch if (value > 0 && attacker.HasStatus(StatusType.Deathtouch) && target.CardData.type == CardType.Character) KillCard(attacker, target); //Kill card if no hp if (target.GetHP() <= 0) KillCard(attacker, target); } //A card that kills another card public virtual void KillCard(Card attacker, Card target) { if (attacker == null || target == null) return; if (!game_data.IsOnBoard(target) && !game_data.IsEquipped(target)) return; //Already killed if (target.HasStatus(StatusType.Invincibility)) return; //Cant be killed Player pattacker = game_data.GetPlayer(attacker.player_id); if (attacker.player_id != target.player_id) pattacker.kill_count++; DiscardCard(target); TriggerCardAbilityType(AbilityTrigger.OnKill, attacker, target); } //Send card into discard public virtual void DiscardCard(Card card) { if (card == null) return; if (game_data.IsInDiscard(card)) return; //Already discarded CardData icard = card.CardData; Player player = game_data.GetPlayer(card.player_id); bool was_on_board = game_data.IsOnBoard(card) || game_data.IsEquipped(card); //Unequip card UnequipAll(card); //Remove card from board and add to discard player.RemoveCardFromAllGroups(card); player.cards_discard.Add(card); game_data.last_destroyed = card.uid; //Remove from bearer Card bearer = player.GetBearerCard(card); if (bearer != null) bearer.equipped_uid = null; if (was_on_board) { //Trigger on death abilities TriggerCardAbilityType(AbilityTrigger.OnDeath, card); TriggerOtherCardsAbilityType(AbilityTrigger.OnDeathOther, card); TriggerSecrets(AbilityTrigger.OnDeathOther, card); UpdateOngoingCards(); //Not UpdateOngoing() here to avoid recursive calls in UpdateOngoingKills } cards_to_clear.Add(card); //Will be Clear() in the next UpdateOngoing, so that simultaneous damage effects work onCardDiscarded?.Invoke(card); } public int RollRandomValue(int dice) { return RollRandomValue(1, dice + 1); } public virtual int RollRandomValue(int min, int max) { game_data.rolled_value = random.Next(min, max); onRollValue?.Invoke(game_data.rolled_value); resolve_queue.SetDelay(1f); return game_data.rolled_value; } //--- Abilities -- public virtual void TriggerCardAbilityType(AbilityTrigger type, Card caster, Card triggerer = null) { foreach (AbilityData iability in caster.GetAbilities()) { if (iability && iability.trigger == type) { TriggerCardAbility(iability, caster, triggerer); } } Card equipped = game_data.GetEquipCard(caster.equipped_uid); if (equipped != null) TriggerCardAbilityType(type, equipped, triggerer); } public virtual void TriggerCardAbilityType(AbilityTrigger type, Card caster, Player triggerer) { foreach (AbilityData iability in caster.GetAbilities()) { if (iability && iability.trigger == type) { TriggerCardAbility(iability, caster, triggerer); } } Card equipped = game_data.GetEquipCard(caster.equipped_uid); if (equipped != null) TriggerCardAbilityType(type, equipped, triggerer); } public virtual void TriggerOtherCardsAbilityType(AbilityTrigger type, Card triggerer) { foreach (Player oplayer in game_data.players) { if (oplayer.hero != null) TriggerCardAbilityType(type, oplayer.hero, triggerer); foreach (Card card in oplayer.cards_board) TriggerCardAbilityType(type, card, triggerer); } } public virtual void TriggerPlayerCardsAbilityType(Player player, AbilityTrigger type) { if (player.hero != null) TriggerCardAbilityType(type, player.hero, player.hero); foreach (Card card in player.cards_board) TriggerCardAbilityType(type, card, card); } public virtual void TriggerCardAbility(AbilityData iability, Card caster) { TriggerCardAbility(iability, caster, caster); } public virtual void TriggerCardAbility(AbilityData iability, Card caster, Card triggerer) { Card trigger_card = triggerer != null ? triggerer : caster; //Triggerer is the caster if not set if (!caster.HasStatus(StatusType.Silenced) && iability.AreTriggerConditionsMet(game_data, caster, trigger_card)) { resolve_queue.AddAbility(iability, caster, trigger_card, ResolveCardAbility); } } public virtual void TriggerCardAbility(AbilityData iability, Card caster, Player triggerer) { if (!caster.HasStatus(StatusType.Silenced) && iability.AreTriggerConditionsMet(game_data, caster, triggerer)) { resolve_queue.AddAbility(iability, caster, caster, ResolveCardAbility); } } public virtual void TriggerAbilityDelayed(AbilityData iability, Card caster) { resolve_queue.AddAbility(iability, caster, caster, TriggerCardAbility); } public virtual void TriggerAbilityDelayed(AbilityData iability, Card caster, Card triggerer) { Card trigger_card = triggerer != null ? triggerer : caster; //Triggerer is the caster if not set resolve_queue.AddAbility(iability, caster, trigger_card, TriggerCardAbility); } //Resolve a card ability, may stop to ask for target protected virtual void ResolveCardAbility(AbilityData iability, Card caster, Card triggerer) { if (!caster.CanDoAbilities()) return; //Silenced card cant cast //Debug.Log("Trigger Ability " + iability.id + " : " + caster.card_id); onAbilityStart?.Invoke(iability, caster); game_data.ability_triggerer = triggerer.uid; game_data.ability_played.Add(iability.id); bool is_selector = ResolveCardAbilitySelector(iability, caster); if (is_selector) return; //Wait for player to select ResolveCardAbilityPlayTarget(iability, caster); ResolveCardAbilityPlayers(iability, caster); ResolveCardAbilityCards(iability, caster); ResolveCardAbilitySlots(iability, caster); ResolveCardAbilityCardData(iability, caster); ResolveCardAbilityNoTarget(iability, caster); AfterAbilityResolved(iability, caster); } protected virtual bool ResolveCardAbilitySelector(AbilityData iability, Card caster) { if (iability.target == AbilityTarget.SelectTarget) { //Wait for target GoToSelectTarget(iability, caster); return true; } else if (iability.target == AbilityTarget.CardSelector) { GoToSelectorCard(iability, caster); return true; } else if (iability.target == AbilityTarget.ChoiceSelector) { GoToSelectorChoice(iability, caster); return true; } return false; } protected virtual void ResolveCardAbilityPlayTarget(AbilityData iability, Card caster) { if (iability.target == AbilityTarget.PlayTarget) { Slot slot = caster.slot; Card slot_card = game_data.GetSlotCard(slot); if (slot.IsPlayerSlot()) { Player tplayer = game_data.GetPlayer(slot.p); if (iability.CanTarget(game_data, caster, tplayer)) ResolveEffectTarget(iability, caster, tplayer); } else if (slot_card != null) { if (iability.CanTarget(game_data, caster, slot_card)) { game_data.last_target = slot_card.uid; ResolveEffectTarget(iability, caster, slot_card); } } else { if (iability.CanTarget(game_data, caster, slot)) ResolveEffectTarget(iability, caster, slot); } } } protected virtual void ResolveCardAbilityPlayers(AbilityData iability, Card caster) { //Get Player Targets based on conditions List targets = iability.GetPlayerTargets(game_data, caster, player_array); //Resolve effects foreach (Player target in targets) { ResolveEffectTarget(iability, caster, target); } } protected virtual void ResolveCardAbilityCards(AbilityData iability, Card caster) { //Get Cards Targets based on conditions List targets = iability.GetCardTargets(game_data, caster, card_array); //Resolve effects foreach (Card target in targets) { ResolveEffectTarget(iability, caster, target); } } protected virtual void ResolveCardAbilitySlots(AbilityData iability, Card caster) { //Get Slot Targets based on conditions List targets = iability.GetSlotTargets(game_data, caster, slot_array); //Resolve effects foreach (Slot target in targets) { ResolveEffectTarget(iability, caster, target); } } protected virtual void ResolveCardAbilityCardData(AbilityData iability, Card caster) { //Get Cards Targets based on conditions List targets = iability.GetCardDataTargets(game_data, caster, card_data_array); //Resolve effects foreach (CardData target in targets) { ResolveEffectTarget(iability, caster, target); } } protected virtual void ResolveCardAbilityNoTarget(AbilityData iability, Card caster) { if (iability.target == AbilityTarget.None) iability.DoEffects(this, caster); } protected virtual void ResolveEffectTarget(AbilityData iability, Card caster, Player target) { iability.DoEffects(this, caster, target); onAbilityTargetPlayer?.Invoke(iability, caster, target); } protected virtual void ResolveEffectTarget(AbilityData iability, Card caster, Card target) { iability.DoEffects(this, caster, target); onAbilityTargetCard?.Invoke(iability, caster, target); } protected virtual void ResolveEffectTarget(AbilityData iability, Card caster, Slot target) { iability.DoEffects(this, caster, target); onAbilityTargetSlot?.Invoke(iability, caster, target); } protected virtual void ResolveEffectTarget(AbilityData iability, Card caster, CardData target) { iability.DoEffects(this, caster, target); } protected virtual void AfterAbilityResolved(AbilityData iability, Card caster) { Player player = game_data.GetPlayer(caster.player_id); //Pay cost if (iability.trigger == AbilityTrigger.Activate || iability.trigger == AbilityTrigger.None) { player.mana -= iability.mana_cost; caster.exhausted = caster.exhausted || iability.exhaust; } //Recalculate and clear UpdateOngoing(); CheckForWinner(); //Chain ability if (iability.target != AbilityTarget.ChoiceSelector && game_data.state != GameState.GameEnded) { foreach (AbilityData chain_ability in iability.chain_abilities) { if (chain_ability != null) { TriggerCardAbility(chain_ability, caster); } } } onAbilityEnd?.Invoke(iability, caster); resolve_queue.ResolveAll(0.5f); RefreshData(); } //This function is called often to update status/stats affected by ongoing abilities //It basically first reset the bonus to 0 (CleanOngoing) and then recalculate it to make sure it it still present //Only cards in hand and on board are updated in this way public virtual void UpdateOngoing() { Profiler.BeginSample("Update Ongoing"); UpdateOngoingCards(); //Update status and stats UpdateOngoingKills(); //Kill cards with 0 HP Profiler.EndSample(); } protected virtual void UpdateOngoingCards() { for (int p = 0; p < game_data.players.Length; p++) { Player player = game_data.players[p]; player.ClearOngoing(); for (int c = 0; c < player.cards_board.Count; c++) player.cards_board[c].ClearOngoing(); for (int c = 0; c < player.cards_equip.Count; c++) player.cards_equip[c].ClearOngoing(); for (int c = 0; c < player.cards_hand.Count; c++) player.cards_hand[c].ClearOngoing(); } for (int p = 0; p < game_data.players.Length; p++) { Player player = game_data.players[p]; UpdateOngoingAbilities(player, player.hero); //Remove this line if hero is on the board for (int c = 0; c < player.cards_board.Count; c++) { Card card = player.cards_board[c]; UpdateOngoingAbilities(player, card); } for (int c = 0; c < player.cards_equip.Count; c++) { Card card = player.cards_equip[c]; UpdateOngoingAbilities(player, card); } } //Stats bonus for (int p = 0; p < game_data.players.Length; p++) { Player player = game_data.players[p]; for (int c = 0; c < player.cards_board.Count; c++) { Card card = player.cards_board[c]; //Taunt effect if (card.HasStatus(StatusType.Protection) && !card.HasStatus(StatusType.Stealth)) { player.AddOngoingStatus(StatusType.Protected, 0); for (int tc = 0; tc < player.cards_board.Count; tc++) { Card tcard = player.cards_board[tc]; if (!tcard.HasStatus(StatusType.Protection) && !tcard.HasStatus(StatusType.Protected)) { tcard.AddOngoingStatus(StatusType.Protected, 0); } } } //Status bonus foreach (CardStatus status in card.status) AddOngoingStatusBonus(card, status); foreach (CardStatus status in card.ongoing_status) AddOngoingStatusBonus(card, status); } for (int c = 0; c < player.cards_hand.Count; c++) { Card card = player.cards_hand[c]; //Status bonus foreach (CardStatus status in card.status) AddOngoingStatusBonus(card, status); foreach (CardStatus status in card.ongoing_status) AddOngoingStatusBonus(card, status); } } } protected virtual void UpdateOngoingKills() { //Kill stuff with 0 hp for (int p = 0; p < game_data.players.Length; p++) { Player player = game_data.players[p]; for (int i = player.cards_board.Count - 1; i >= 0; i--) { if (i < player.cards_board.Count) { Card card = player.cards_board[i]; if (card.GetHP() <= 0) DiscardCard(card); } } for (int i = player.cards_equip.Count - 1; i >= 0; i--) { if (i < player.cards_equip.Count) { Card card = player.cards_equip[i]; if (card.GetHP() <= 0) DiscardCard(card); Card bearer = player.GetBearerCard(card); if (bearer == null) DiscardCard(card); } } } //Clear cards for (int c = 0; c < cards_to_clear.Count; c++) cards_to_clear[c].Clear(); cards_to_clear.Clear(); } protected virtual void UpdateOngoingAbilities(Player player, Card card) { if (card == null || !card.CanDoAbilities()) return; List cabilities = card.GetAbilities(); for (int a = 0; a < cabilities.Count; a++) { AbilityData ability = cabilities[a]; if (ability != null && ability.trigger == AbilityTrigger.Ongoing && ability.AreTriggerConditionsMet(game_data, card)) { if (ability.target == AbilityTarget.Self) { if (ability.AreTargetConditionsMet(game_data, card, card)) { ability.DoOngoingEffects(this, card, card); } } if (ability.target == AbilityTarget.PlayerSelf) { if (ability.AreTargetConditionsMet(game_data, card, player)) { ability.DoOngoingEffects(this, card, player); } } if (ability.target == AbilityTarget.AllPlayers || ability.target == AbilityTarget.PlayerOpponent) { for (int tp = 0; tp < game_data.players.Length; tp++) { if (ability.target == AbilityTarget.AllPlayers || tp != player.player_id) { Player oplayer = game_data.players[tp]; if (ability.AreTargetConditionsMet(game_data, card, oplayer)) { ability.DoOngoingEffects(this, card, oplayer); } } } } if (ability.target == AbilityTarget.EquippedCard) { if (card.CardData.IsEquipment()) { //Get bearer of the equipment Card target = player.GetBearerCard(card); if (target != null && ability.AreTargetConditionsMet(game_data, card, target)) { ability.DoOngoingEffects(this, card, target); } } else if (card.equipped_uid != null) { //Get equipped card Card target = game_data.GetCard(card.equipped_uid); if (target != null && ability.AreTargetConditionsMet(game_data, card, target)) { ability.DoOngoingEffects(this, card, target); } } } if (ability.target == AbilityTarget.AllCardsAllPiles || ability.target == AbilityTarget.AllCardsHand || ability.target == AbilityTarget.AllCardsBoard) { for (int tp = 0; tp < game_data.players.Length; tp++) { //Looping on all cards is very slow, since there are no ongoing effects that works out of board/hand we loop on those only Player tplayer = game_data.players[tp]; //Hand Cards if (ability.target == AbilityTarget.AllCardsAllPiles || ability.target == AbilityTarget.AllCardsHand) { for (int tc = 0; tc < tplayer.cards_hand.Count; tc++) { Card tcard = tplayer.cards_hand[tc]; if (ability.AreTargetConditionsMet(game_data, card, tcard)) { ability.DoOngoingEffects(this, card, tcard); } } } //Board Cards if (ability.target == AbilityTarget.AllCardsAllPiles || ability.target == AbilityTarget.AllCardsBoard) { for (int tc = 0; tc < tplayer.cards_board.Count; tc++) { Card tcard = tplayer.cards_board[tc]; if (ability.AreTargetConditionsMet(game_data, card, tcard)) { ability.DoOngoingEffects(this, card, tcard); } } } //Equip Cards if (ability.target == AbilityTarget.AllCardsAllPiles) { for (int tc = 0; tc < tplayer.cards_equip.Count; tc++) { Card tcard = tplayer.cards_equip[tc]; if (ability.AreTargetConditionsMet(game_data, card, tcard)) { ability.DoOngoingEffects(this, card, tcard); } } } } } } } } protected virtual void AddOngoingStatusBonus(Card card, CardStatus status) { if (status.type == StatusType.AddAttack) card.attack_ongoing += status.value; if (status.type == StatusType.AddHP) card.hp_ongoing += status.value; if (status.type == StatusType.AddManaCost) card.mana_ongoing += status.value; } //---- Secrets ------------ public virtual bool TriggerPlayerSecrets(Player player, AbilityTrigger secret_trigger) { for (int i = player.cards_secret.Count - 1; i >= 0; i--) { Card card = player.cards_secret[i]; CardData icard = card.CardData; if (icard.type == CardType.Secret && !card.exhausted) { if (card.AreAbilityConditionsMet(secret_trigger, game_data, card, card)) { resolve_queue.AddSecret(secret_trigger, card, card, ResolveSecret); resolve_queue.SetDelay(0.5f); card.exhausted = true; if (onSecretTrigger != null) onSecretTrigger.Invoke(card, card); return true; //Trigger only 1 secret per trigger } } } return false; } public virtual bool TriggerSecrets(AbilityTrigger secret_trigger, Card trigger_card) { if (trigger_card != null && trigger_card.HasStatus(StatusType.SpellImmunity)) return false; //Spell Immunity, triggerer is the one that trigger the trap, target is the one attacked, so usually the player who played the trap, so we dont check the target for (int p = 0; p < game_data.players.Length; p++) { if (p != game_data.current_player) { Player other_player = game_data.players[p]; for (int i = other_player.cards_secret.Count - 1; i >= 0; i--) { Card card = other_player.cards_secret[i]; CardData icard = card.CardData; if (icard.type == CardType.Secret && !card.exhausted) { Card trigger = trigger_card != null ? trigger_card : card; if (card.AreAbilityConditionsMet(secret_trigger, game_data, card, trigger)) { resolve_queue.AddSecret(secret_trigger, card, trigger, ResolveSecret); resolve_queue.SetDelay(0.5f); card.exhausted = true; if (onSecretTrigger != null) onSecretTrigger.Invoke(card, trigger); return true; //Trigger only 1 secret per trigger } } } } } return false; } protected virtual void ResolveSecret(AbilityTrigger secret_trigger, Card secret_card, Card trigger) { CardData icard = secret_card.CardData; Player player = game_data.GetPlayer(secret_card.player_id); if (icard.type == CardType.Secret) { Player tplayer = game_data.GetPlayer(trigger.player_id); if (!is_ai_predict) tplayer.AddHistory(GameAction.SecretTriggered, secret_card, trigger); TriggerCardAbilityType(secret_trigger, secret_card, trigger); DiscardCard(secret_card); if (onSecretResolve != null) onSecretResolve.Invoke(secret_card, trigger); } } //---- Resolve Selector ----- public virtual void SelectCard(Card target) { if (game_data.selector == SelectorType.None) return; Card caster = game_data.GetCard(game_data.selector_caster_uid); AbilityData ability = AbilityData.Get(game_data.selector_ability_id); if (caster == null || target == null || ability == null) return; if (game_data.selector == SelectorType.SelectTarget) { if (!ability.CanTarget(game_data, caster, target)) return; //Can't target that target Player player = game_data.GetPlayer(caster.player_id); if (!is_ai_predict) player.AddHistory(GameAction.CastAbility, caster, ability, target); game_data.selector = SelectorType.None; game_data.last_target = target.uid; ResolveEffectTarget(ability, caster, target); AfterAbilityResolved(ability, caster); resolve_queue.ResolveAll(); } if (game_data.selector == SelectorType.SelectorCard) { if (!ability.IsCardSelectionValid(game_data, caster, target, card_array)) return; //Supports conditions and filters game_data.selector = SelectorType.None; game_data.last_target = target.uid; ResolveEffectTarget(ability, caster, target); AfterAbilityResolved(ability, caster); resolve_queue.ResolveAll(); } } public virtual void SelectPlayer(Player target) { if (game_data.selector == SelectorType.None) return; Card caster = game_data.GetCard(game_data.selector_caster_uid); AbilityData ability = AbilityData.Get(game_data.selector_ability_id); if (caster == null || target == null || ability == null) return; if (game_data.selector == SelectorType.SelectTarget) { if (!ability.CanTarget(game_data, caster, target)) return; //Can't target that target Player player = game_data.GetPlayer(caster.player_id); if (!is_ai_predict) player.AddHistory(GameAction.CastAbility, caster, ability, target); game_data.selector = SelectorType.None; ResolveEffectTarget(ability, caster, target); AfterAbilityResolved(ability, caster); resolve_queue.ResolveAll(); } } public virtual void SelectSlot(Slot target) { if (game_data.selector == SelectorType.None) return; Card caster = game_data.GetCard(game_data.selector_caster_uid); AbilityData ability = AbilityData.Get(game_data.selector_ability_id); if (caster == null || ability == null || !target.IsValid()) return; if (game_data.selector == SelectorType.SelectTarget) { if (!ability.CanTarget(game_data, caster, target)) return; //Conditions not met Player player = game_data.GetPlayer(caster.player_id); if (!is_ai_predict) player.AddHistory(GameAction.CastAbility, caster, ability, target); game_data.selector = SelectorType.None; ResolveEffectTarget(ability, caster, target); AfterAbilityResolved(ability, caster); resolve_queue.ResolveAll(); } } public virtual void SelectChoice(int choice) { if (game_data.selector == SelectorType.None) return; Card caster = game_data.GetCard(game_data.selector_caster_uid); AbilityData ability = AbilityData.Get(game_data.selector_ability_id); if (caster == null || ability == null || choice < 0) return; if (game_data.selector == SelectorType.SelectorChoice && ability.target == AbilityTarget.ChoiceSelector) { if (choice >= 0 && choice < ability.chain_abilities.Length) { AbilityData achoice = ability.chain_abilities[choice]; if (achoice != null && game_data.CanSelectAbility(caster, achoice)) { game_data.selector = SelectorType.None; AfterAbilityResolved(ability, caster); ResolveCardAbility(achoice, caster, caster); resolve_queue.ResolveAll(); } } } } public virtual void SelectCost(int select_cost) { if (game_data.selector == SelectorType.None) return; Player player = game_data.GetPlayer(game_data.selector_player_id); Card caster = game_data.GetCard(game_data.selector_caster_uid); if (player == null || caster == null || select_cost < 0) return; if (game_data.selector == SelectorType.SelectorCost) { if (select_cost >= 0 && select_cost < 10 && select_cost <= player.mana) { game_data.selector = SelectorType.None; game_data.selected_value = select_cost; player.mana -= select_cost; RefreshData(); TriggerSecrets(AbilityTrigger.OnPlayOther, caster); TriggerCardAbilityType(AbilityTrigger.OnPlay, caster); TriggerOtherCardsAbilityType(AbilityTrigger.OnPlayOther, caster); resolve_queue.ResolveAll(); } } } public virtual void CancelSelection() { if (game_data.selector != SelectorType.None) { //Return card to hand if was selecting cost if (game_data.selector == SelectorType.SelectorCost) CancelPlayCard(); //End selection game_data.selector = SelectorType.None; RefreshData(); } } public void CancelPlayCard() { Card card = game_data.GetCard(game_data.selector_caster_uid); if (card != null) { Player player = game_data.GetPlayer(card.player_id); if (card.CardData.IsDynamicManaCost()) player.mana += game_data.selected_value; else player.mana += card.CardData.cost; player.RemoveCardFromAllGroups(card); player.AddCard(player.cards_hand, card); card.Clear(); } } // 换牌 public virtual void Mulligan(Player player, string[] cards) { if (game_data.phase == GamePhase.Mulligan && !player.ready) { int count = 0; List remove_list = new List(); foreach (Card card in player.cards_hand) { if (cards.Contains(card.uid)) { remove_list.Add(card); count++; } } // 将换掉的卡牌临时存储,不放入弃牌堆 List mulligan_cards = new List(); foreach (Card card in remove_list) { player.RemoveCardFromAllGroups(card); mulligan_cards.Add(card); } player.ready = true; DrawCard(player, count); // 将换掉的卡牌重新加入牌组并洗牌 foreach (Card card in mulligan_cards) { player.cards_deck.Add(card); } ShuffleDeck(player.cards_deck); RefreshData(); if (game_data.AreAllPlayersReady()) { StartTurn(); } } } //-----Trigger Selector----- protected virtual void GoToSelectTarget(AbilityData iability, Card caster) { game_data.selector = SelectorType.SelectTarget; game_data.selector_player_id = caster.player_id; game_data.selector_ability_id = iability.id; game_data.selector_caster_uid = caster.uid; RefreshData(); } protected virtual void GoToSelectorCard(AbilityData iability, Card caster) { game_data.selector = SelectorType.SelectorCard; game_data.selector_player_id = caster.player_id; game_data.selector_ability_id = iability.id; game_data.selector_caster_uid = caster.uid; RefreshData(); } protected virtual void GoToSelectorChoice(AbilityData iability, Card caster) { game_data.selector = SelectorType.SelectorChoice; game_data.selector_player_id = caster.player_id; game_data.selector_ability_id = iability.id; game_data.selector_caster_uid = caster.uid; RefreshData(); } protected virtual void GoToSelectorCost(Card caster) { game_data.selector = SelectorType.SelectorCost; game_data.selector_player_id = caster.player_id; game_data.selector_ability_id = ""; game_data.selector_caster_uid = caster.uid; game_data.selected_value = 0; RefreshData(); } protected virtual void GoToMulligan() { game_data.phase = GamePhase.Mulligan; game_data.turn_timer = GameplayData.Get().turn_duration; foreach (Player player in game_data.players) player.ready = false; RefreshData(); } //------------- public virtual void RefreshData() { onRefresh?.Invoke(); } public virtual void ClearResolve() { resolve_queue.Clear(); } public virtual bool IsResolving() { return resolve_queue.IsResolving(); } public virtual bool IsGameStarted() { return game_data.HasStarted(); } public virtual bool IsGameEnded() { return game_data.HasEnded(); } public virtual Game GetGameData() { return game_data; } public System.Random GetRandom() { return random; } public Game GameData { get { return game_data; } } public ResolveQueue ResolveQueue { get { return resolve_queue; } } } }