This commit is contained in:
yaoyanwei
2025-08-04 16:45:48 +08:00
parent 565aa16389
commit 2f2a601227
2296 changed files with 522745 additions and 93 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 34ed9bd9838a40c4d9755bc0fbba9a8c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,225 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine.AI
{
/// <summary>
/// Values and calculations for various values of the AI decision-making, adjusting these can improve your AI
/// Heuristic: Represent the score of a board state, high score favor AI, low score favor the opponent
/// Action Score: Represent the score of an individual action, to proritize actions if too many in a single node
/// Action Sort Order: Value to determine the order actions should be executed in a single turn to avoid searching same things in different order, executed in ascending order
/// </summary>
public class AIHeuristic
{
//---------- Heuristic PARAMS -------------
public int board_card_value = 20; //Score of having cards on board
public int secret_card_value = 10; //Score of having cards in secret zone
public int hand_card_value = 5; //Score of having cards in hand
public int kill_value = 5; //Score of killing a card
public int player_hp_value = 4; //Score per player hp
public int card_attack_value = 3; //Score per board card attack
public int card_hp_value = 2; //Score per board card hp
public int card_status_value = 15; //Score per status on card (multiplied by hvalue of StatusData)
//-----------
private int ai_player_id; //ID of this AI, usually the human is 0 and AI is 1
private int ai_level; //ai level (level 10 is the best, level 1 is the worst)
private int heuristic_modifier; //Randomize heuristic for lower level ai
private System.Random random_gen;
public AIHeuristic(int player_id, int level)
{
ai_player_id = player_id;
ai_level = level;
heuristic_modifier = GetHeuristicModifier();
random_gen = new System.Random();
}
public int CalculateHeuristic(Game data, NodeState node)
{
Player aiplayer = data.GetPlayer(ai_player_id);
Player oplayer = data.GetOpponentPlayer(ai_player_id);
return CalculateHeuristic(data, node, aiplayer, oplayer);
}
//Calculate full heuristic
//Should return a value between -10000 and 10000 (unless its a win)
public int CalculateHeuristic(Game data, NodeState node, Player aiplayer, Player oplayer)
{
int score = 0;
//Victories
if (aiplayer.IsDead())
score += -100000 + node.tdepth * 1000; //Add node depth to seek surviving longest
if (oplayer.IsDead())
score += 100000 - node.tdepth * 1000; //Reduce node depth to seek fastest win
//Board state
score += aiplayer.cards_board.Count * board_card_value;
score += aiplayer.cards_equip.Count * board_card_value;
score += aiplayer.cards_secret.Count * secret_card_value;
score += aiplayer.cards_hand.Count * hand_card_value;
score += aiplayer.kill_count * kill_value;
score += aiplayer.hp * player_hp_value;
score -= oplayer.cards_board.Count * board_card_value;
score -= oplayer.cards_equip.Count * board_card_value;
score -= oplayer.cards_secret.Count * secret_card_value;
score -= oplayer.cards_hand.Count * hand_card_value;
score -= oplayer.kill_count * kill_value;
score -= oplayer.hp * player_hp_value;
foreach (Card card in aiplayer.cards_board)
{
score += card.GetAttack() * card_attack_value;
score += card.GetHP() * card_hp_value;
foreach (CardStatus status in card.status)
score += status.StatusData.hvalue * card_status_value;
foreach (CardStatus status in card.ongoing_status)
score += status.StatusData.hvalue * card_status_value;
}
foreach (Card card in oplayer.cards_board)
{
score -= card.GetAttack() * card_attack_value;
score -= card.GetHP() * card_hp_value;
foreach (CardStatus status in card.status)
score -= status.StatusData.hvalue * card_status_value;
foreach (CardStatus status in card.ongoing_status)
score -= status.StatusData.hvalue * card_status_value;
}
if (heuristic_modifier > 0)
score += random_gen.Next(-heuristic_modifier, heuristic_modifier);
return score;
}
//This calculates the score of an individual action, instead of the board state
//When too many actions are possible in a single node, only the ones with best action score will be evaluated
//Make sure to return a positive value
public int CalculateActionScore(Game data, AIAction order)
{
if (order.type == GameAction.EndTurn)
return 0; //Other orders are better
if (order.type == GameAction.CancelSelect)
return 0; //Other orders are better
if (order.type == GameAction.CastAbility)
{
return 200;
}
if (order.type == GameAction.Attack)
{
Card card = data.GetCard(order.card_uid);
Card target = data.GetCard(order.target_uid);
int ascore = card.GetAttack() >= target.GetHP() ? 300 : 100; //Are you killing the card?
int oscore = target.GetAttack() >= card.GetHP() ? -200 : 0; //Are you getting killed?
return ascore + oscore + target.GetAttack() * 5; //Always better to get rid of high-attack cards
}
if (order.type == GameAction.AttackPlayer)
{
Card card = data.GetCard(order.card_uid);
Player player = data.GetPlayer(order.target_player_id);
int ascore = card.GetAttack() >= player.hp ? 500 : 200; //Are you killing the player?
return ascore + (card.GetAttack() * 10) - player.hp; //Always better to inflict more damage
}
if (order.type == GameAction.PlayCard)
{
Player player = data.GetPlayer(ai_player_id);
Card card = data.GetCard(order.card_uid);
if (card.CardData.IsBoardCard())
return 200 + (card.GetMana() * 5) - (30 * player.cards_board.Count); //High cost cards are better to play, better to play when not a lot of cards in play
else if (card.CardData.IsEquipment())
return 200 + (card.GetMana() * 5) - (30 * player.cards_equip.Count);
else
return 200 + (card.GetMana() * 5);
}
if (order.type == GameAction.Move)
{
return 100;
}
return 100; //Other actions are better than End/Cancel
}
//Within the same turn, actions can only be executed in sorting order, make sure it returns positive value higher than 0 or it wont be sorted
//This prevents calculating all possibilities of A->B->C B->C->A C->A->B etc..
//If two AIActions with same sorting value, or if sorting value is 0, ai will test all ordering variations (slower)
//This would not be necessary in a game with only 1 action per turn (such as chess) but is useful for AI that can perform multiple actions in 1 turn
//Ordering could be improved, pretty much random now
public int CalculateActionSort(Game data, AIAction order)
{
if (order.type == GameAction.EndTurn)
return 0; //End turn can always be performed, 0 means any order
if (data.selector != SelectorType.None)
return 0; //Selector actions not affected by sorting
Card card = data.GetCard(order.card_uid);
Card target = order.target_uid != null ? data.GetCard(order.target_uid) : null;
bool is_spell = card != null && !card.CardData.IsBoardCard();
int type_sort = 0;
if (order.type == GameAction.PlayCard && is_spell)
type_sort = 1; //Play Spells first
if (order.type == GameAction.CastAbility)
type_sort = 2; //Card Abilities second
if (order.type == GameAction.Move)
type_sort = 3; //Move third
if (order.type == GameAction.Attack)
type_sort = 4; //Attacks fourth
if (order.type == GameAction.AttackPlayer)
type_sort = 5; //Player attacks fifth
if (order.type == GameAction.PlayCard && !is_spell)
type_sort = 7; //Play Characters last
int card_sort = card != null ? (card.Hash % 100) : 0;
int target_sort = target != null ? (target.Hash % 100) : 0;
int sort = type_sort * 10000 + card_sort * 100 + target_sort + 1;
return sort;
}
//Lower level AI add a random number to their heuristic
private int GetHeuristicModifier()
{
if (ai_level >= 10)
return 0;
if (ai_level == 9)
return 5;
if (ai_level == 8)
return 10;
if (ai_level == 7)
return 20;
if (ai_level == 6)
return 30;
if (ai_level == 5)
return 40;
if (ai_level == 4)
return 50;
if (ai_level == 3)
return 75;
if (ai_level == 2)
return 100;
if (ai_level <= 1)
return 200;
return 0;
}
//Check if this node represent one of the players winning
public bool IsWin(NodeState node)
{
return node.hvalue > 50000 || node.hvalue < -50000;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a435ad5e46007f5449dc8d627fbe175f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,825 @@
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using UnityEngine.Profiling;
using TcgEngine.Gameplay;
namespace TcgEngine.AI
{
/// <summary>
/// Minimax algorithm for AI.
/// </summary>
public class AILogic
{
//-------- AI Logic Params ------------------
public int ai_depth = 3; //How many turns in advance does it check, higher number takes exponentially longer
public int ai_depth_wide = 1; //For these first few turns, will consider more options, slow!
public int actions_per_turn = 2; //AI wont predict more than this number of sequential actions per turn, if more than that will EndTurn (Do A, then do B, then do C, then end turn)
public int actions_per_turn_wide = 3; //Same but in wide depth
public int nodes_per_action = 4; //For a turn action (1st, 2nd, or 3rd...), cannot evaluate more than this number of child nodes, if more, will only process the AIActions with with best score
public int nodes_per_action_wide = 7; //Same but in wide depth
//Example: for the first turn, AI will predict 3 sequential actions (I play a card, then attack with this one, then play a spell),
//for each of those actions, it will look at 7 possibilities, if more will cut based on score, keeping the actions with highest score
//At depth 2 and 3 it will only try to perform 2 actions but for each one will evaluate 4 possibilities. Depth 2 is the opponent's turn and depth 3 is the AI's next turn.
//For the nodes that are evaluated, will go down to depth 3 and calculate heuristic at the max depth, and then propagate the heuristic up in the node tree.
//AI will choose the move that has a path leading to the best heuristic.
//-----
public int ai_player_id; //AI player_id (usually its 1)
public int ai_level; //AI level
private GameLogic game_logic; //Game logic used to calculate moves
private Game original_data; //Original game data when start calculating possibilities
private AIHeuristic heuristic;
private Thread ai_thread;
private NodeState first_node = null;
private NodeState best_move = null;
private bool running = false;
private int nb_calculated = 0;
private int reached_depth = 0;
private System.Random random_gen;
private Pool<NodeState> node_pool = new Pool<NodeState>();
private Pool<Game> data_pool = new Pool<Game>();
private Pool<AIAction> action_pool = new Pool<AIAction>();
private Pool<List<AIAction>> list_pool = new Pool<List<AIAction>>();
private ListSwap<Card> card_array = new ListSwap<Card>();
private ListSwap<Slot> slot_array = new ListSwap<Slot>();
public static AILogic Create(int player_id, int level)
{
AILogic job = new AILogic();
job.ai_player_id = player_id;
job.ai_level = level;
job.heuristic = new AIHeuristic(player_id, level);
job.game_logic = new GameLogic(true); //Skip all delays for the AI calculations
return job;
}
public void RunAI(Game data)
{
if (running)
return;
original_data = Game.CloneNew(data); //Clone game data to keep original data unaffected
game_logic.ClearResolve(); //Clear temp memory
game_logic.SetData(original_data); //Assign data to game logic
random_gen = new System.Random(); //Reset random seed
first_node = null;
reached_depth = 0;
nb_calculated = 0;
running = true;
//Uncomment these lines to run on separate thread (and comment Execute()), better for production so it doesn't freeze the UI while calculating the AI
ai_thread = new Thread(Execute);
ai_thread.Start();
//Uncomment this line to run on main thread (and comment the thread one), better for debuging since you will be able to use breakpoints, profiler and Debug.Log
//Execute();
}
public void Stop()
{
running = false;
if (ai_thread != null && ai_thread.IsAlive)
ai_thread.Abort();
}
private void Execute()
{
//Create first node
first_node = CreateNode(null, null, ai_player_id, 0, 0);
first_node.hvalue = heuristic.CalculateHeuristic(original_data, first_node);
first_node.alpha = int.MinValue;
first_node.beta = int.MaxValue;
Profiler.BeginSample("AI");
System.Diagnostics.Stopwatch watch = System.Diagnostics.Stopwatch.StartNew();
//Calculate first node
CalculateNode(original_data, first_node);
Debug.Log("AI: Time " + watch.ElapsedMilliseconds + "ms Depth " + reached_depth + " Nodes " + nb_calculated);
Profiler.EndSample();
//Save best move
best_move = first_node.best_child;
running = false;
}
//Add list of all possible orders and search in all of them
private void CalculateNode(Game data, NodeState node)
{
Profiler.BeginSample("Add Actions");
Player player = data.GetPlayer(data.current_player);
List<AIAction> action_list = list_pool.Create();
int max_actions = node.tdepth < ai_depth_wide ? actions_per_turn_wide : actions_per_turn;
if (node.taction < max_actions)
{
if (data.selector == SelectorType.None)
{
//Play card
for (int c = 0; c < player.cards_hand.Count; c++)
{
Card card = player.cards_hand[c];
AddActions(action_list, data, node, GameAction.PlayCard, card);
}
//Action on board
for (int c = 0; c < player.cards_board.Count; c++)
{
Card card = player.cards_board[c];
AddActions(action_list, data, node, GameAction.Attack, card);
AddActions(action_list, data, node, GameAction.AttackPlayer, card);
AddActions(action_list, data, node, GameAction.CastAbility, card);
//AddActions(action_list, data, node, GameAction.Move, card); //Uncomment to consider move actions
}
if (player.hero != null)
AddActions(action_list, data, node, GameAction.CastAbility, player.hero);
}
else
{
AddSelectActions(action_list, data, node);
}
}
//End Turn (dont add action if ai can still attack player, or ai hasnt spent any mana)
bool is_full_mana = HasAction(action_list, GameAction.PlayCard) && player.mana >= player.mana_max;
bool can_attack_player = HasAction(action_list, GameAction.AttackPlayer);
bool can_end = !can_attack_player && !is_full_mana && data.selector == SelectorType.None;
if (action_list.Count == 0 || can_end)
{
AIAction actiont = CreateAction(GameAction.EndTurn);
action_list.Add(actiont);
}
//Remove actions with low score
FilterActions(data, node, action_list);
Profiler.EndSample();
//Execute valid action and search child node
for (int o = 0; o < action_list.Count; o++)
{
AIAction action = action_list[o];
if (action.valid && node.alpha < node.beta)
{
CalculateChildNode(data, node, action);
}
}
action_list.Clear();
list_pool.Dispose(action_list);
}
//Mark valid/invalid on each action, if too many actions, will keep only the ones with best score
private void FilterActions(Game data, NodeState node, List<AIAction> action_list)
{
int count_valid = 0;
for (int o = 0; o < action_list.Count; o++)
{
AIAction action = action_list[o];
action.sort = heuristic.CalculateActionSort(data, action);
action.valid = action.sort <= 0 || action.sort >= node.sort_min;
if (action.valid)
count_valid++;
}
int max_actions = node.tdepth < ai_depth_wide ? nodes_per_action_wide : nodes_per_action;
int max_actions_skip = max_actions + 2; //No need to calculate all scores if its just to remove 1-2 actions
if (count_valid <= max_actions_skip)
return; //No filtering needed
//Calculate scores
for (int o = 0; o < action_list.Count; o++)
{
AIAction action = action_list[o];
if (action.valid)
{
action.score = heuristic.CalculateActionScore(data, action);
}
}
//Sort, and invalidate actions with low score
action_list.Sort((AIAction a, AIAction b) => { return b.score.CompareTo(a.score); });
for (int o = 0; o < action_list.Count; o++)
{
AIAction action = action_list[o];
action.valid = action.valid && o < max_actions;
}
}
//Create a child node for parent, and calculate it
private void CalculateChildNode(Game data, NodeState parent, AIAction action)
{
if (action.type == GameAction.None)
return;
int player_id = data.current_player;
//Clone data so we can update it in a new node
Profiler.BeginSample("Clone Data");
Game ndata = data_pool.Create();
Game.Clone(data, ndata); //Clone
game_logic.ClearResolve();
game_logic.SetData(ndata);
Profiler.EndSample();
//Execute move and update data
Profiler.BeginSample("Execute AIAction");
DoAIAction(ndata, action, player_id);
Profiler.EndSample();
//Update depth
bool new_turn = action.type == GameAction.EndTurn;
int next_tdepth = parent.tdepth;
int next_taction = parent.taction + 1;
if (new_turn)
{
next_tdepth = parent.tdepth + 1;
next_taction = 0;
}
//Create node
Profiler.BeginSample("Create Node");
NodeState child_node = CreateNode(parent, action, player_id, next_tdepth, next_taction);
parent.childs.Add(child_node);
Profiler.EndSample();
//Set minimum sort for next AIActions, if new turn, reset to 0
child_node.sort_min = new_turn ? 0 : Mathf.Max(action.sort, child_node.sort_min);
//If win or reached max depth, stop searching deeper
if (!ndata.HasEnded() && child_node.tdepth < ai_depth)
{
//Calculate child
CalculateNode(ndata, child_node);
}
else
{
//End of tree, calculate full Heuristic
child_node.hvalue = heuristic.CalculateHeuristic(ndata, child_node);
}
//Update parents hvalue, alpha, beta, and best child
if (player_id == ai_player_id)
{
//AI player
if (parent.best_child == null || child_node.hvalue > parent.hvalue)
{
parent.best_child = child_node;
parent.hvalue = child_node.hvalue;
parent.alpha = Mathf.Max(parent.alpha, parent.hvalue);
}
}
else
{
//Opponent player
if (parent.best_child == null || child_node.hvalue < parent.hvalue)
{
parent.best_child = child_node;
parent.hvalue = child_node.hvalue;
parent.beta = Mathf.Min(parent.beta, parent.hvalue);
}
}
//Just for debug, keep track of node/depth count
nb_calculated++;
if (child_node.tdepth > reached_depth)
reached_depth = child_node.tdepth;
//We are done with this game data, dispose it.
//Dont dispose NodeState here (node_pool) since we want to retrieve the full tree path later
data_pool.Dispose(ndata);
}
private NodeState CreateNode(NodeState parent, AIAction action, int player_id, int turn_depth, int turn_action)
{
NodeState nnode = node_pool.Create();
nnode.current_player = player_id;
nnode.tdepth = turn_depth;
nnode.taction = turn_action;
nnode.parent = parent;
nnode.last_action = action;
nnode.alpha = parent != null ? parent.alpha : int.MinValue;
nnode.beta = parent != null ? parent.beta : int.MaxValue;
nnode.hvalue = 0;
nnode.sort_min = 0;
return nnode;
}
//Add all possible moves for card to list of actions
private void AddActions(List<AIAction> actions, Game data, NodeState node, ushort type, Card card)
{
Player player = data.GetPlayer(data.current_player);
if (data.selector != SelectorType.None)
return;
if (card.HasStatus(StatusType.Paralysed))
return;
if (type == GameAction.PlayCard)
{
if (card.CardData.IsBoardCard())
{
//Doesn't matter where the card is played
Slot slot = player.GetRandomEmptySlot(random_gen, slot_array.Get());
if (data.CanPlayCard(card, slot))
{
AIAction action = CreateAction(type, card);
action.slot = slot;
actions.Add(action);
}
}
else if (card.CardData.IsEquipment())
{
Player tplayer = data.GetPlayer(card.player_id);
for (int c = 0; c < tplayer.cards_board.Count; c++)
{
Card tcard = tplayer.cards_board[c];
if (data.CanPlayCard(card, tcard.slot))
{
AIAction action = CreateAction(type, card);
action.slot = tcard.slot;
action.target_player_id = tplayer.player_id;
actions.Add(action);
}
}
}
else if (card.CardData.IsRequireTargetSpell())
{
for (int p = 0; p < data.players.Length; p++)
{
Player tplayer = data.players[p];
Slot tslot = new Slot(tplayer.player_id);
if (data.CanPlayCard(card, tslot))
{
AIAction action = CreateAction(type, card);
action.slot = tslot;
action.target_player_id = tplayer.player_id;
actions.Add(action);
}
}
foreach (Slot slot in Slot.GetAll())
{
if (data.CanPlayCard(card, slot))
{
Card slot_card = data.GetSlotCard(slot);
AIAction action = CreateAction(type, card);
action.slot = slot;
action.target_uid = slot_card != null ? slot_card.uid : null;
actions.Add(action);
}
}
}
else if (data.CanPlayCard(card, Slot.None))
{
AIAction action = CreateAction(type, card);
actions.Add(action);
}
}
if (type == GameAction.Attack)
{
if (card.CanAttack())
{
for (int p = 0; p < data.players.Length; p++)
{
if (p != player.player_id)
{
Player oplayer = data.players[p];
for (int tc = 0; tc < oplayer.cards_board.Count; tc++)
{
Card target = oplayer.cards_board[tc];
if (data.CanAttackTarget(card, target))
{
AIAction action = CreateAction(type, card);
action.target_uid = target.uid;
actions.Add(action);
}
}
}
}
}
}
if (type == GameAction.AttackPlayer)
{
if (card.CanAttack())
{
for (int p = 0; p < data.players.Length; p++)
{
if (p != player.player_id)
{
Player oplayer = data.players[p];
if (data.CanAttackTarget(card, oplayer))
{
AIAction action = CreateAction(type, card);
action.target_player_id = oplayer.player_id;
actions.Add(action);
}
}
}
}
}
if (type == GameAction.CastAbility)
{
List<AbilityData> abilities = card.GetAbilities();
for (int a = 0; a < abilities.Count; a++)
{
AbilityData ability = abilities[a];
if (ability.trigger == AbilityTrigger.Activate && data.CanCastAbility(card, ability) && ability.HasValidSelectTarget(data, card))
{
AIAction action = CreateAction(type, card);
action.ability_id = ability.id;
actions.Add(action);
}
}
}
if (type == GameAction.Move)
{
foreach (Slot slot in Slot.GetAll(player.player_id))
{
if (data.CanMoveCard(card, slot))
{
AIAction action = CreateAction(type, card);
action.slot = slot;
actions.Add(action);
}
}
}
}
//Add all possible moves for a selection
private void AddSelectActions(List<AIAction> actions, Game data, NodeState node)
{
if (data.selector == SelectorType.None)
return;
Player player = data.GetPlayer(data.selector_player_id);
Card caster = data.GetCard(data.selector_caster_uid);
AbilityData ability = AbilityData.Get(data.selector_ability_id);
if (player == null || caster == null)
return;
if (data.selector == SelectorType.SelectTarget && ability != null)
{
for (int p = 0; p < data.players.Length; p++)
{
Player tplayer = data.players[p];
if (ability.CanTarget(data, caster, tplayer))
{
AIAction action = CreateAction(GameAction.SelectPlayer, caster);
action.target_player_id = tplayer.player_id;
actions.Add(action);
}
}
foreach (Slot slot in Slot.GetAll())
{
Card tcard = data.GetSlotCard(slot);
if (tcard != null && ability.CanTarget(data, caster, tcard))
{
AIAction action = CreateAction(GameAction.SelectCard, caster);
action.target_uid = tcard.uid;
actions.Add(action);
}
else if (tcard == null && ability.CanTarget(data, caster, slot))
{
AIAction action = CreateAction(GameAction.SelectSlot, caster);
action.slot = slot;
actions.Add(action);
}
}
}
if (data.selector == SelectorType.SelectorCard && ability != null)
{
for (int p = 0; p < data.players.Length; p++)
{
List<Card> cards = ability.GetCardTargets(data, caster, card_array);
foreach (Card tcard in cards)
{
AIAction action = CreateAction(GameAction.SelectCard, caster);
action.target_uid = tcard.uid;
actions.Add(action);
}
}
}
if (data.selector == SelectorType.SelectorChoice && ability != null)
{
for (int i = 0; i < ability.chain_abilities.Length; i++)
{
AbilityData choice = ability.chain_abilities[i];
if (choice != null && data.CanSelectAbility(caster, choice))
{
AIAction action = CreateAction(GameAction.SelectChoice, caster);
action.value = i;
actions.Add(action);
}
}
}
if (data.selector == SelectorType.SelectorCost)
{
for (int i = 1; i <= player.mana; i++)
{
AIAction action = CreateAction(GameAction.SelectCost, caster);
action.value = i;
actions.Add(action);
}
}
//Add option to cancel, if no valid options
if (actions.Count == 0)
{
AIAction caction = CreateAction(GameAction.CancelSelect, caster);
actions.Add(caction);
}
}
private AIAction CreateAction(ushort type)
{
AIAction action = action_pool.Create();
action.Clear();
action.type = type;
action.valid = true;
return action;
}
private AIAction CreateAction(ushort type, Card card)
{
AIAction action = action_pool.Create();
action.Clear();
action.type = type;
action.card_uid = card.uid;
action.valid = true;
return action;
}
//Simulate AI action
private void DoAIAction(Game data, AIAction action, int player_id)
{
Player player = data.GetPlayer(player_id);
if (action.type == GameAction.PlayCard)
{
Card card = player.GetHandCard(action.card_uid);
game_logic.PlayCard(card, action.slot);
}
if (action.type == GameAction.Move)
{
Card card = player.GetBoardCard(action.card_uid);
game_logic.MoveCard(card, action.slot);
}
if (action.type == GameAction.Attack)
{
Card card = player.GetBoardCard(action.card_uid);
Card target = data.GetBoardCard(action.target_uid);
game_logic.AttackTarget(card, target);
}
if (action.type == GameAction.AttackPlayer)
{
Card card = player.GetBoardCard(action.card_uid);
Player tplayer = data.GetPlayer(action.target_player_id);
game_logic.AttackPlayer(card, tplayer);
}
if (action.type == GameAction.CastAbility)
{
Card card = player.GetCard(action.card_uid);
AbilityData ability = AbilityData.Get(action.ability_id);
game_logic.CastAbility(card, ability);
}
if (action.type == GameAction.SelectCard)
{
Card target = data.GetCard(action.target_uid);
game_logic.SelectCard(target);
}
if (action.type == GameAction.SelectPlayer)
{
Player target = data.GetPlayer(action.target_player_id);
game_logic.SelectPlayer(target);
}
if (action.type == GameAction.SelectSlot)
{
game_logic.SelectSlot(action.slot);
}
if (action.type == GameAction.SelectChoice)
{
game_logic.SelectChoice(action.value);
}
if (action.type == GameAction.CancelSelect)
{
game_logic.CancelSelection();
}
if (action.type == GameAction.EndTurn)
{
game_logic.EndTurn();
}
}
private bool HasAction(List<AIAction> list, ushort type)
{
for (int i = 0; i < list.Count; i++)
{
if (list[i].type == type)
return true;
}
return false;
}
//----Return values----
public bool IsRunning()
{
return running;
}
public string GetNodePath()
{
return GetNodePath(first_node);
}
public string GetNodePath(NodeState node)
{
string path = "Prediction: HValue: " + node.hvalue + "\n";
NodeState current = node;
AIAction move;
while (current != null)
{
move = current.last_action;
if (move != null)
path += "Player " + current.current_player + ": " + move.GetText(original_data) + "\n";
current = current.best_child;
}
return path;
}
public void ClearMemory()
{
original_data = null;
first_node = null;
best_move = null;
foreach (NodeState node in node_pool.GetAllActive())
node.Clear();
foreach (AIAction order in action_pool.GetAllActive())
order.Clear();
data_pool.DisposeAll();
node_pool.DisposeAll();
action_pool.DisposeAll();
list_pool.DisposeAll();
System.GC.Collect(); //Free memory from AI
}
public int GetNbNodesCalculated()
{
return nb_calculated;
}
public int GetDepthReached()
{
return reached_depth;
}
public NodeState GetBest()
{
return best_move;
}
public NodeState GetFirst()
{
return first_node;
}
public AIAction GetBestAction()
{
return best_move != null ? best_move.last_action : null;
}
public bool IsBestFound()
{
return best_move != null;
}
}
public class NodeState
{
public int tdepth; //Depth in number of turns
public int taction; //How many orders in current turn
public int sort_min; //Sorting minimum value, orders below this value will be ignored to avoid calculate both path A -> B and path B -> A
public int hvalue; //Heuristic value, this AI tries to maximize it, opponent tries to minimize it
public int alpha; //Highest heuristic reached by the AI player, used for optimization and ignore some tree branch
public int beta; //Lowest heuristic reached by the opponent player, used for optimization and ignore some tree branch
public AIAction last_action = null;
public int current_player;
public NodeState parent;
public NodeState best_child = null;
public List<NodeState> childs = new List<NodeState>();
public NodeState() { }
public NodeState(NodeState parent, int player_id, int turn_depth, int turn_action, int turn_sort)
{
this.parent = parent;
this.current_player = player_id;
this.tdepth = turn_depth;
this.taction = turn_action;
this.sort_min = turn_sort;
}
public void Clear()
{
last_action = null;
best_child = null;
parent = null;
childs.Clear();
}
}
public class AIAction
{
public ushort type;
public string card_uid;
public string target_uid;
public int target_player_id;
public string ability_id;
public Slot slot;
public int value;
public int score; //Score to determine which orders get cut and ignored
public int sort; //Orders must be executed in sort order
public bool valid; //If false, this order will be ignored
public AIAction() { }
public AIAction(ushort t) { type = t; }
public string GetText(Game data)
{
string txt = GameAction.GetString(type);
Card card = data.GetCard(card_uid);
Card target = data.GetCard(target_uid);
if (card != null)
txt += " card " + card.card_id;
if (target != null)
txt += " target " + target.card_id;
if (slot != Slot.None)
txt += " slot " + slot.x + "-" + slot.p;
if (ability_id != null)
txt += " ability " + ability_id;
if (value > 0)
txt += " value " + value;
return txt;
}
public void Clear()
{
type = 0;
valid = false;
card_uid = null;
target_uid = null;
ability_id = null;
target_player_id = -1;
slot = Slot.None;
value = -1;
score = 0;
sort = 0;
}
public static AIAction None { get { AIAction a = new AIAction(); a.type = 0; return a; } }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3ab13916fa774b3419a43439fc50ec1c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,48 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TcgEngine.Gameplay;
namespace TcgEngine.AI
{
/// <summary>
/// AI player base class, other AI inherit from this
/// </summary>
public abstract class AIPlayer
{
public int player_id;
public int ai_level;
protected GameLogic gameplay;
public virtual void Update()
{
//Script called by game server to update AI
//Override this to let the AI play
}
public bool CanPlay()
{
Game game_data = gameplay.GetGameData();
Player player = game_data.GetPlayer(player_id);
bool can_play = game_data.IsPlayerTurn(player) || game_data.IsPlayerMulliganTurn(player);
return can_play && !gameplay.IsResolving();
}
public static AIPlayer Create(AIType type, GameLogic gameplay, int id, int level = 0)
{
if (type == AIType.Random)
return new AIPlayerRandom(gameplay, id, level);
if (type == AIType.MiniMax)
return new AIPlayerMM(gameplay, id, level);
return null;
}
}
public enum AIType
{
Random = 0, //Dumb AI that just do random moves, useful for testing cards without getting destroyed
MiniMax = 10, //Stronger AI using Minimax algo with alpha-beta pruning
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0354b727a964c654787c7597d4091569
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,287 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TcgEngine.Gameplay;
namespace TcgEngine.AI
{
/// <summary>
/// AI player using the MinMax AI algorithm
/// </summary>
public class AIPlayerMM : AIPlayer
{
private AILogic ai_logic;
private bool is_playing = false;
public AIPlayerMM(GameLogic gameplay, int id, int level)
{
this.gameplay = gameplay;
player_id = id;
ai_level = Mathf.Clamp(level, 1, 10);
ai_logic = AILogic.Create(id, ai_level);
}
public override void Update()
{
Game game_data = gameplay.GetGameData();
Player player = game_data.GetPlayer(player_id);
if (!is_playing && game_data.IsPlayerTurn(player))
{
is_playing = true;
TimeTool.StartCoroutine(AiTurn());
}
if (!is_playing && game_data.IsPlayerMulliganTurn(player))
{
SkipMulligan();
}
if (!game_data.IsPlayerTurn(player) && ai_logic.IsRunning())
Stop();
}
private IEnumerator AiTurn()
{
yield return new WaitForSeconds(1f);
Game game_data = gameplay.GetGameData();
ai_logic.RunAI(game_data);
while (ai_logic.IsRunning())
{
yield return new WaitForSeconds(0.1f);
}
AIAction best = ai_logic.GetBestAction();
if (best != null)
{
Debug.Log("Execute AI Action: " + best.GetText(game_data) + "\n" + ai_logic.GetNodePath());
//foreach (NodeState node in ai_logic.GetFirst().childs)
// Debug.Log(ai_logic.GetNodePath(node));
ExecuteAction(best);
}
ai_logic.ClearMemory();
yield return new WaitForSeconds(0.5f);
is_playing = false;
}
private void Stop()
{
ai_logic.Stop();
is_playing = false;
}
//----------
private void ExecuteAction(AIAction action)
{
if (!CanPlay())
return;
if (action.type == GameAction.PlayCard)
{
PlayCard(action.card_uid, action.slot);
}
if (action.type == GameAction.Attack)
{
AttackCard(action.card_uid, action.target_uid);
}
if (action.type == GameAction.AttackPlayer)
{
AttackPlayer(action.card_uid, action.target_player_id);
}
if (action.type == GameAction.Move)
{
MoveCard(action.card_uid, action.slot);
}
if (action.type == GameAction.CastAbility)
{
CastAbility(action.card_uid, action.ability_id);
}
if (action.type == GameAction.SelectCard)
{
SelectCard(action.target_uid);
}
if (action.type == GameAction.SelectPlayer)
{
SelectPlayer(action.target_player_id);
}
if (action.type == GameAction.SelectSlot)
{
SelectSlot(action.slot);
}
if (action.type == GameAction.SelectChoice)
{
SelectChoice(action.value);
}
if (action.type == GameAction.SelectCost)
{
SelectCost(action.value);
}
if (action.type == GameAction.SelectMulligan)
{
SkipMulligan();
}
if (action.type == GameAction.CancelSelect)
{
CancelSelect();
}
if (action.type == GameAction.EndTurn)
{
EndTurn();
}
if (action.type == GameAction.Resign)
{
Resign();
}
}
private void PlayCard(string card_uid, Slot slot)
{
Game game_data = gameplay.GetGameData();
Card card = game_data.GetCard(card_uid);
if (card != null)
{
gameplay.PlayCard(card, slot);
}
}
private void MoveCard(string card_uid, Slot slot)
{
Game game_data = gameplay.GetGameData();
Card card = game_data.GetCard(card_uid);
if (card != null)
{
gameplay.MoveCard(card, slot);
}
}
private void AttackCard(string attacker_uid, string target_uid)
{
Game game_data = gameplay.GetGameData();
Card card = game_data.GetCard(attacker_uid);
Card target = game_data.GetCard(target_uid);
if (card != null && target != null)
{
gameplay.AttackTarget(card, target);
}
}
private void AttackPlayer(string attacker_uid, int target_player_id)
{
Game game_data = gameplay.GetGameData();
Card card = game_data.GetCard(attacker_uid);
if (card != null)
{
Player oplayer = game_data.GetPlayer(target_player_id);
gameplay.AttackPlayer(card, oplayer);
}
}
private void CastAbility(string caster_uid, string ability_id)
{
Game game_data = gameplay.GetGameData();
Card caster = game_data.GetCard(caster_uid);
AbilityData iability = AbilityData.Get(ability_id);
if (caster != null && iability != null)
{
gameplay.CastAbility(caster, iability);
}
}
private void SelectCard(string target_uid)
{
Game game_data = gameplay.GetGameData();
Card target = game_data.GetCard(target_uid);
if (target != null)
{
gameplay.SelectCard(target);
}
}
private void SelectPlayer(int tplayer_id)
{
Game game_data = gameplay.GetGameData();
Player target = game_data.GetPlayer(tplayer_id);
if (target != null)
{
gameplay.SelectPlayer(target);
}
}
private void SelectSlot(Slot slot)
{
if (slot != Slot.None)
{
gameplay.SelectSlot(slot);
}
}
private void SelectChoice(int choice)
{
gameplay.SelectChoice(choice);
}
private void SelectCost(int cost)
{
gameplay.SelectCost(cost);
}
private void CancelSelect()
{
if (CanPlay())
{
gameplay.CancelSelection();
}
}
private void SkipMulligan()
{
string[] cards = new string[0]; //Don't mulligan
SelectMulligan(cards);
}
private void SelectMulligan(string[] cards)
{
Game game_data = gameplay.GetGameData();
Player player = game_data.GetPlayer(player_id);
gameplay.Mulligan(player, cards);
}
private void EndTurn()
{
if (CanPlay())
{
gameplay.EndTurn();
}
}
private void Resign()
{
int other = player_id == 0 ? 1 : 0;
gameplay.EndGame(other);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ee38d0542562f9543b095360388a2867
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,340 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TcgEngine.Client;
using TcgEngine.Gameplay;
namespace TcgEngine.AI
{
/// <summary>
/// AI player making completely random decisions, really bad AI but useful for testing
/// </summary>
public class AIPlayerRandom : AIPlayer
{
private bool is_playing = false;
private bool is_selecting = false;
private System.Random rand = new System.Random();
public AIPlayerRandom(GameLogic gameplay, int id, int level)
{
this.gameplay = gameplay;
player_id = id;
}
public override void Update()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
Player player = game_data.GetPlayer(player_id);
if (game_data.IsPlayerTurn(player) && !gameplay.IsResolving())
{
if(!is_playing && game_data.selector == SelectorType.None && game_data.current_player == player_id)
{
is_playing = true;
TimeTool.StartCoroutine(AiTurn());
}
if (!is_selecting && game_data.selector != SelectorType.None && game_data.selector_player_id == player_id)
{
if (game_data.selector == SelectorType.SelectTarget)
{
//AI select target
is_selecting = true;
TimeTool.StartCoroutine(AiSelectTarget());
}
if (game_data.selector == SelectorType.SelectorCard)
{
//AI select target
is_selecting = true;
TimeTool.StartCoroutine(AiSelectCard());
}
if (game_data.selector == SelectorType.SelectorChoice)
{
//AI select target
is_selecting = true;
TimeTool.StartCoroutine(AiSelectChoice());
}
if (game_data.selector == SelectorType.SelectorCost)
{
//AI select target
is_selecting = true;
TimeTool.StartCoroutine(AiSelectCost());
}
}
}
if (!is_selecting && game_data.IsPlayerMulliganTurn(player))
{
is_selecting = true;
TimeTool.StartCoroutine(AiSelectMulligan());
}
}
private IEnumerator AiTurn()
{
yield return new WaitForSeconds(1f);
PlayCard();
yield return new WaitForSeconds(0.5f);
PlayCard();
yield return new WaitForSeconds(0.5f);
PlayCard();
yield return new WaitForSeconds(0.5f);
Attack();
yield return new WaitForSeconds(0.5f);
Attack();
yield return new WaitForSeconds(0.5f);
AttackPlayer();
yield return new WaitForSeconds(0.5f);
EndTurn();
is_playing = false;
}
private IEnumerator AiSelectCard()
{
yield return new WaitForSeconds(0.5f);
SelectCard();
yield return new WaitForSeconds(0.5f);
CancelSelect();
is_selecting = false;
}
private IEnumerator AiSelectTarget()
{
yield return new WaitForSeconds(0.5f);
SelectTarget();
yield return new WaitForSeconds(0.5f);
CancelSelect();
is_selecting = false;
}
private IEnumerator AiSelectChoice()
{
yield return new WaitForSeconds(0.5f);
SelectChoice();
yield return new WaitForSeconds(0.5f);
CancelSelect();
is_selecting = false;
}
private IEnumerator AiSelectCost()
{
yield return new WaitForSeconds(0.5f);
SelectCost();
yield return new WaitForSeconds(0.5f);
CancelSelect();
is_selecting = false;
}
private IEnumerator AiSelectMulligan()
{
yield return new WaitForSeconds(0.5f);
SelectMulligan();
yield return new WaitForSeconds(0.5f);
is_selecting = false;
}
//----------
public void PlayCard()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
Player player = game_data.GetPlayer(player_id);
if (player.cards_hand.Count > 0 && game_data.IsPlayerActionTurn(player))
{
Card random = player.GetRandomCard(player.cards_hand, rand);
Slot slot = player.GetRandomEmptySlot(rand);
if (random != null && random.CardData.IsRequireTargetSpell())
slot = game_data.GetRandomSlot(rand); //Spell can target any slot, not just your side
if(random != null && random.CardData.IsEquipment())
slot = player.GetRandomOccupiedSlot(rand);
if (random != null)
gameplay.PlayCard(random, slot);
}
}
public void Attack()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
Player player = game_data.GetPlayer(player_id);
if (player.cards_board.Count > 0 && game_data.IsPlayerActionTurn(player))
{
Card random = player.GetRandomCard(player.cards_board, rand);
Card rtarget = game_data.GetRandomBoardCard(rand);
if (random != null && rtarget != null)
gameplay.AttackTarget(random, rtarget);
}
}
public void AttackPlayer()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
Player player = game_data.GetPlayer(player_id);
Player oplayer = game_data.GetRandomPlayer(rand);
if (player.cards_board.Count > 0 && game_data.IsPlayerActionTurn(player))
{
Card random = player.GetRandomCard(player.cards_board, rand);
if (random != null && oplayer != null && oplayer != player)
gameplay.AttackPlayer(random, oplayer);
}
}
public void SelectCard()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
Player player = game_data.GetPlayer(player_id);
AbilityData ability = AbilityData.Get(game_data.selector_ability_id);
Card caster = game_data.GetCard(game_data.selector_caster_uid);
if (player != null && ability != null && caster != null)
{
List<Card> card_list = ability.GetCardTargets(game_data, caster);
if (card_list.Count > 0)
{
Card card = card_list[rand.Next(0, card_list.Count)];
gameplay.SelectCard(card);
}
}
}
public void SelectTarget()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
if (game_data.selector != SelectorType.None)
{
int target_player = player_id;
AbilityData ability = AbilityData.Get(game_data.selector_ability_id);
if (ability != null && ability.target == AbilityTarget.SelectTarget)
target_player = (player_id == 0 ? 1 : 0);
Player tplayer = game_data.GetPlayer(target_player);
if (tplayer.cards_board.Count > 0)
{
Card random = tplayer.GetRandomCard(tplayer.cards_board, rand);
if (random != null)
gameplay.SelectCard(random);
}
}
}
public void SelectChoice()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
if (game_data.selector != SelectorType.None)
{
AbilityData ability = AbilityData.Get(game_data.selector_ability_id);
if (ability != null && ability.chain_abilities.Length > 0)
{
int choice = rand.Next(0, ability.chain_abilities.Length);
gameplay.SelectChoice(choice);
}
}
}
public void SelectCost()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
if (game_data.selector != SelectorType.None)
{
Player player = game_data.GetPlayer(player_id);
Card card = game_data.GetCard(game_data.selector_caster_uid);
if (player != null && card != null)
{
int max = Mathf.Clamp(player.mana, 0, 9);
int choice = rand.Next(0, max + 1);
gameplay.SelectCost(choice);
}
}
}
public void CancelSelect()
{
if (CanPlay())
{
gameplay.CancelSelection();
}
}
public void SelectMulligan()
{
if (!CanPlay())
return;
Game game_data = gameplay.GetGameData();
if (game_data.phase == GamePhase.Mulligan)
{
Player player = game_data.GetPlayer(player_id);
string[] cards = new string[0]; //Don't mulligan
gameplay.Mulligan(player, cards);
}
}
public void EndTurn()
{
if (CanPlay())
{
gameplay.EndTurn();
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4f327588bb7c02141baa6fbeb0081e84
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4288928c4a58cb946b7eff592956d5d1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,439 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Threading.Tasks;
using UnityEngine.Networking;
using UnityEngine.Events;
namespace TcgEngine
{
/// <summary>
/// API client communicates with the NodeJS web api
/// Can send requests and receive responses
/// </summary>
public class ApiClient : MonoBehaviour
{
public bool is_server;
public UnityAction<RegisterResponse> onRegister; //Triggered after register, even if failed
public UnityAction<LoginResponse> onLogin; //Triggered after login, even if failed
public UnityAction<LoginResponse> onRefresh; //Triggered after login refresh, even if failed
public UnityAction onLogout; //Triggered after logout
private string user_id = "";
private string username = "";
private string access_token = "";
private string refresh_token = "";
private string api_version = "";
private bool logged_in = false;
private bool expired = false;
private UserData udata = null;
private int sending = 0;
private string last_error = "";
private float refresh_timer = 0f;
private float online_timer = 0f;
private long expiration_timestamp = 0;
private const float online_duration = 60f * 5f; //5 min
private static ApiClient instance;
void Awake()
{
//API client should be on OnDestroyOnLoad
//dont assign here if already assigned cause new one will be destroyed in TheNetwork Awake
if(instance == null)
instance = this;
LoadTokens();
}
private void Update()
{
//Refresh access token or online status
Refresh();
}
private void LoadTokens()
{
if (!is_server && string.IsNullOrEmpty(user_id))
{
access_token = PlayerPrefs.GetString("tcg_access_token");
refresh_token = PlayerPrefs.GetString("tcg_refresh_token");
}
}
private void SaveTokens()
{
if (!is_server)
{
PlayerPrefs.SetString("tcg_access_token", access_token);
PlayerPrefs.SetString("tcg_refresh_token", refresh_token);
}
}
private async void Refresh()
{
if (!logged_in)
return;
//Check expiration
if (!expired)
{
long current = GetTimestamp();
expired = current > (expiration_timestamp - 10);
}
//Refresh access token when expired
refresh_timer += Time.deltaTime;
if (expired && refresh_timer > 5f)
{
refresh_timer = 0f;
await RefreshLogin(); //Try to relogin
}
//Refresh online status
online_timer += Time.deltaTime;
if (!expired && online_timer > online_duration)
{
online_timer = 0f;
await KeepOnline();
}
}
public async Task<RegisterResponse> Register(string email, string user, string password)
{
RegisterRequest data = new RegisterRequest();
data.email = email;
data.username = user;
data.password = password;
data.avatar = "";
return await Register(data);
}
public async Task<RegisterResponse> Register(RegisterRequest data)
{
Logout(); //Disconnect
string url = ServerURL + "/users/register";
string json = ApiTool.ToJson(data);
WebResponse res = await SendPostRequest(url, json);
RegisterResponse regist_res = ApiTool.JsonToObject<RegisterResponse>(res.data);
regist_res.success = res.success;
regist_res.error = res.error;
onRegister?.Invoke(regist_res);
return regist_res;
}
public async Task<LoginResponse> Login(string user, string password)
{
Logout(); //Disconnect
LoginRequest data = new LoginRequest();
data.password = password;
if (user.Contains("@"))
data.email = user;
else
data.username = user;
string url = ServerURL + "/auth";
string json = ApiTool.ToJson(data);
WebResponse res = await SendPostRequest(url, json);
LoginResponse login_res = GetLoginRes(res);
AfterLogin(login_res);
onLogin?.Invoke(login_res);
return login_res;
}
public async Task<LoginResponse> RefreshLogin()
{
string url = ServerURL + "/auth/refresh";
AutoLoginRequest data = new AutoLoginRequest();
data.refresh_token = refresh_token;
string json = ApiTool.ToJson(data);
WebResponse res = await SendPostRequest(url, json);
LoginResponse login_res = GetLoginRes(res);
AfterLogin(login_res);
onRefresh?.Invoke(login_res);
return login_res;
}
private LoginResponse GetLoginRes(WebResponse res)
{
LoginResponse login_res = ApiTool.JsonToObject<LoginResponse>(res.data);
login_res.success = res.success;
login_res.error = res.error;
//Uncomment to force having same client version as api
/*if (!is_server && !IsVersionValid())
{
login_res.error = "Invalid Version";
login_res.success = false;
}*/
return login_res;
}
private void AfterLogin(LoginResponse login_res)
{
last_error = login_res.error;
if (login_res.success)
{
user_id = login_res.id;
username = login_res.username;
access_token = login_res.access_token;
refresh_token = login_res.refresh_token;
api_version = login_res.version;
expiration_timestamp = GetTimestamp() + login_res.duration;
refresh_timer = 0f;
online_timer = 0f;
logged_in = true;
expired = false;
SaveTokens();
}
}
public async Task<UserData> LoadUserData()
{
udata = await LoadUserData(this.username);
return udata;
}
public async Task<UserData> LoadUserData(string username)
{
if (!IsConnected())
return null;
string url = ServerURL + "/users/" + username;
WebResponse res = await SendGetRequest(url);
UserData udata = null;
if (res.success)
{
udata = ApiTool.JsonToObject<UserData>(res.data);
}
return udata;
}
public async Task<bool> KeepOnline()
{
if (!IsConnected())
return false;
//Keep player online
string url = ServerURL + "/auth/keep";
WebResponse res = await SendGetRequest(url);
expired = !res.success;
return res.success;
}
public async Task<bool> Validate()
{
if (!IsConnected())
return false;
//Check if connection is still valid
string url = ServerURL + "/auth/validate";
WebResponse res = await SendGetRequest(url);
expired = !res.success;
return res.success;
}
public void Logout()
{
user_id = "";
username = "";
access_token = "";
refresh_token = "";
api_version = "";
last_error = "";
logged_in = false;
onLogout?.Invoke();
SaveTokens();
}
public async void CreateMatch(Game game_data)
{
if (game_data.settings.game_type != GameType.Multiplayer)
return;
AddMatchRequest req = new AddMatchRequest();
req.players = new string[2];
req.players[0] = game_data.players[0].username;
req.players[1] = game_data.players[1].username;
req.tid = game_data.game_uid;
req.ranked = game_data.settings.IsRanked();
req.mode = game_data.settings.GetGameModeId();
string url = ServerURL + "/matches/add";
string json = ApiTool.ToJson(req);
WebResponse res = await SendPostRequest(url, json);
Debug.Log("Match Started! " + res.success);
}
public async void EndMatch(Game game_data, int winner_id)
{
if (game_data.settings.game_type != GameType.Multiplayer)
return;
Player player = game_data.GetPlayer(winner_id);
CompleteMatchRequest req = new CompleteMatchRequest();
req.tid = game_data.game_uid;
req.winner = player != null ? player.username : "";
string url = ServerURL + "/matches/complete";
string json = ApiTool.ToJson(req);
WebResponse res = await SendPostRequest(url, json);
Debug.Log("Match Completed! " + res.success);
}
public async Task<string> SendGetVersion()
{
string url = ServerURL + "/version";
WebResponse res = await SendGetRequest(url);
if (res.success)
{
VersionResponse version_data = ApiTool.JsonToObject<VersionResponse>(res.data);
api_version = version_data.version;
return api_version;
}
return null;
}
public async Task<WebResponse> SendGetRequest(string url)
{
return await SendRequest(url, WebRequest.METHOD_GET);
}
public async Task<WebResponse> SendPostRequest(string url, string json_data)
{
return await SendRequest(url, WebRequest.METHOD_POST, json_data);
}
public async Task<WebResponse> SendRequest(string url, string method, string json_data = "")
{
UnityWebRequest request = WebRequest.Create(url, method, json_data, access_token);
return await SendRequest(request);
}
private async Task<WebResponse> SendRequest(UnityWebRequest request)
{
int wait = 0;
int wait_max = request.timeout * 1000;
request.timeout += 1; //Add offset to make sure it aborts first
sending++;
var async_oper = request.SendWebRequest();
while (!async_oper.isDone)
{
await TimeTool.Delay(200);
wait += 200;
if (wait >= wait_max)
request.Abort(); //Abort to avoid unity errors on timeout
}
WebResponse response = WebRequest.GetResponse(request);
response.error = GetError(response);
last_error = response.error;
request.Dispose();
sending--;
return response;
}
private string GetError(WebResponse res)
{
if (res.success)
return "";
ErrorResponse err = ApiTool.JsonToObject<ErrorResponse>(res.data);
if (err != null)
return err.error;
else
return res.error;
}
public bool IsConnected()
{
return logged_in && !expired;
}
public bool IsLoggedIn()
{
return logged_in;
}
public bool IsExpired()
{
return expired;
}
public bool IsBusy()
{
return sending > 0;
}
public long GetTimestamp()
{
return System.DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
public string GetLastRequest()
{
return last_error;
}
public string GetLastError()
{
return last_error;
}
//Use this function if you want to prevent players to login with an outdated client
//Call it inside the login and loginrefresh functions after the api_version is set and return error if invalid
public bool IsVersionValid()
{
return ClientVersion == ServerVersion;
}
public UserData UserData { get { return udata; } }
public string UserID { get { return user_id; } set { user_id = value; } }
public string Username { get { return username; } set { username = value; } }
public string AccessToken { get { return access_token; } set { access_token = value; } }
public string RefreshToken { get { return refresh_token; } set { refresh_token = value; } }
public string ServerVersion { get { return api_version; } }
public string ClientVersion { get { return Application.version; } }
public static string ServerURL
{
get
{
NetworkData data = NetworkData.Get();
string protocol = data.api_https ? "https://" : "http://";
return protocol + data.api_url;
}
}
public static ApiClient Get()
{
if (instance == null)
instance = FindObjectOfType<ApiClient>();
return instance;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bbcd533fe8d4d0a4aaa364f2ae5e4077
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,211 @@
using System;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// List of data structure used by the ApiClient requests and responses
/// </summary>
//--------- Requests -----------
[Serializable]
public struct LoginRequest
{
public string email;
public string username;
public string password;
}
[Serializable]
public struct AutoLoginRequest
{
public string refresh_token;
}
[Serializable]
public struct RegisterRequest
{
public string email;
public string username;
public string password;
public string avatar;
}
[Serializable]
public struct EditUserRequest
{
public string avatar;
public string cardback;
}
[Serializable]
public struct EditEmailRequest
{
public string email;
}
[Serializable]
public struct EditPasswordRequest
{
public string password_previous;
public string password_new;
}
[Serializable]
public struct FriendAddRequest
{
public string username;
}
[Serializable]
public struct AddMatchRequest
{
public string tid;
public string[] players;
public string mode;
public bool ranked;
}
[Serializable]
public struct CompleteMatchRequest
{
public string tid;
public string winner;
}
[Serializable]
public struct RewardGainRequest
{
public string reward;
}
[Serializable]
public struct BuyPackRequest
{
public string pack;
public int quantity;
}
[Serializable]
public struct BuyCardRequest
{
public string card;
public string variant;
public int quantity;
}
[Serializable]
public struct SellDuplicateRequest
{
//public string variant;
//public string rarity;
public int keep;
}
[Serializable]
public struct OpenPackRequest
{
public string pack;
}
//--------- Response -----------
[Serializable]
public struct VersionResponse
{
public string version;
}
[Serializable]
public struct RegisterResponse
{
public string id;
public string username;
public string version;
public bool success;
public string error;
}
[Serializable]
public struct LoginResponse
{
public string id;
public string username;
public string refresh_token;
public string access_token;
public int permission_level;
public int validation_level;
public int duration;
public string version;
public string error;
public bool success;
}
[Serializable]
public struct UserIdResponse
{
public string id;
public string username;
public string error;
}
[Serializable]
public struct MatchResponse
{
public string tid;
public string[] players;
public DateTime start;
public DateTime end;
public string winner;
public bool completed;
public MatchDataResponse[] udata;
}
[Serializable]
public struct MatchDataResponse
{
public string username;
public int rank;
public DeckData deck;
public RewardResponse reward;
}
[Serializable]
public struct RewardResponse
{
public string tid;
public int coins;
public int elo;
public int xp;
public string[] cards;
public string[] decks;
}
[Serializable]
public struct MarketResponse
{
public string seller;
public string card;
public int price;
public int quantity;
}
[Serializable]
public struct FriendResponse
{
public string username;
public string server_time;
public FriendData[] friends;
public FriendData[] friends_requests;
}
[System.Serializable]
public struct FriendData
{
public string username;
public string avatar;
public string last_online_time;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3d5b5a0543605d2459b2da189f0c814b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,54 @@
using UnityEngine;
using System;
using System.Text;
using System.Security.Cryptography;
using UnityEngine.Events;
namespace TcgEngine
{
/// <summary>
/// Useful tool static functions for the ApiClient
/// </summary>
public class ApiTool : MonoBehaviour
{
// ----- Convertions ------
public static T JsonToObject<T>(string json)
{
try
{
T value = JsonUtility.FromJson<T>(json);
return value;
}
catch (Exception) { }
return (T)Activator.CreateInstance(typeof(T));
}
public static T[] JsonToArray<T>(string json)
{
ListJson<T> list = new ListJson<T>();
list.list = new T[0];
try
{
string wrap_json = "{ \"list\": " + json + "}";
list = JsonUtility.FromJson<ListJson<T>>(wrap_json);
return list.list;
}
catch (Exception) { }
return new T[0];
}
public static string ToJson(object data)
{
return JsonUtility.ToJson(data);
}
public static int ParseInt(string int_str, int default_val = 0)
{
bool success = int.TryParse(int_str, out int val);
return success ? val : default_val;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3a80590b3916cb84fb73b14768bdf168
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,425 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Unity.Netcode;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Contain UserData retrieved from the web api database
/// </summary>
[System.Serializable]
public class UserData
{
public string id;
public string username;
public string email;
public string avatar;
public string cardback;
public int permission_level = 1;
public int validation_level = 1;
public int coins;
public int xp;
public int elo;
public int matches;
public int victories;
public int defeats;
public UserCardData[] cards;
public UserCardData[] packs;
public UserDeckData[] decks;
public string[] rewards;
public string[] avatars;
public string[] cardbacks;
public string[] friends;
public UserData()
{
cards = new UserCardData[0];
packs = new UserCardData[0];
decks = new UserDeckData[0];
rewards = new string[0];
avatars = new string[0];
cardbacks = new string[0];
friends = new string[0];
permission_level = 1;
coins = 10000;
elo = 1000;
}
public int GetLevel()
{
return Mathf.FloorToInt(xp / 1000) + 1;
}
public string GetAvatar()
{
if (avatar != null)
return avatar;
return "";
}
public string GetCardback()
{
if (cardback != null)
return cardback;
return "";
}
public void SetDeck(UserDeckData deck)
{
for(int i=0; i<decks.Length; i++)
{
if (decks[i].tid == deck.tid)
{
decks[i] = deck;
return;
}
}
//Not found
List<UserDeckData> ldecks = new List<UserDeckData>(decks);
ldecks.Add(deck);
this.decks = ldecks.ToArray();
}
public UserDeckData GetDeck(string tid)
{
foreach (UserDeckData deck in decks)
{
if (deck.tid == tid)
return deck;
}
return null;
}
public UserCardData GetCard(string tid, string variant)
{
foreach (UserCardData card in cards)
{
if (card.tid == tid && card.variant == variant)
return card;
}
return null;
}
public int GetCardQuantity(CardData card, VariantData variant)
{
return GetCardQuantity(card.id, variant.id, variant.is_default);
}
public int GetCardQuantity(string tid, string variant, bool default_variant = false)
{
if (cards == null)
return 0;
foreach (UserCardData card in cards)
{
if (card.tid == tid && card.variant == variant)
return card.quantity;
if (card.tid == tid && card.variant == "" && default_variant)
return card.quantity;
}
return 0;
}
public UserCardData GetPack(string tid)
{
foreach (UserCardData pack in packs)
{
if (pack.tid == tid)
return pack;
}
return null;
}
public int GetPackQuantity(string tid)
{
if (packs == null)
return 0;
foreach (UserCardData pack in packs)
{
if (pack.tid == tid)
return pack.quantity;
}
return 0;
}
public int CountUniqueCards()
{
if (cards == null)
return 0;
HashSet<string> unique_cards = new HashSet<string>();
foreach (UserCardData card in cards)
{
if (!unique_cards.Contains(card.tid))
unique_cards.Add(card.tid);
}
return unique_cards.Count;
}
public int CountCardType(VariantData variant)
{
int value = 0;
foreach (UserCardData card in cards)
{
if (card.variant == variant.id)
value += 1;
}
return value;
}
public bool HasDeckCards(UserDeckData deck)
{
foreach (UserCardData card in deck.cards)
{
bool default_variant = true; //Count "" variant as valid for compatibilty with older vers
if (GetCardQuantity(card.tid, card.variant, default_variant) < card.quantity)
return false;
}
return true;
}
public bool IsDeckValid(UserDeckData deck)
{
if (Authenticator.Get().IsApi())
return HasDeckCards(deck) && deck.IsValid();
return deck.IsValid();
}
public void AddDeck(UserDeckData deck)
{
List<UserDeckData> udecks = new List<UserDeckData>(decks);
udecks.Add(deck);
decks = udecks.ToArray();
foreach (UserCardData card in deck.cards)
{
AddCard(card.tid, card.variant, 1);
}
}
public void AddPack(string tid, int quantity)
{
bool found = false;
foreach (UserCardData pack in packs)
{
if (pack.tid == tid)
{
found = true;
pack.quantity += quantity;
}
}
if (!found)
{
UserCardData npack = new UserCardData();
npack.tid = tid;
npack.quantity = quantity;
List<UserCardData> apacks = new List<UserCardData>(packs);
apacks.Add(npack);
packs = apacks.ToArray();
}
}
public void AddCard(string tid, string variant, int quantity)
{
bool found = false;
foreach (UserCardData card in cards)
{
if (card.tid == tid && card.variant == variant)
{
found = true;
card.quantity += quantity;
}
}
if (!found)
{
UserCardData ncard = new UserCardData();
ncard.tid = tid;
ncard.variant = variant;
ncard.quantity = quantity;
List<UserCardData> acards = new List<UserCardData>(cards);
acards.Add(ncard);
cards = acards.ToArray();
}
}
public void AddReward(string tid)
{
if (!HasReward(tid))
{
List<string> arewards = new List<string>(rewards);
arewards.Add(tid);
rewards = arewards.ToArray();
}
}
public bool HasCard(string card_tid, string variant, int quantity = 1)
{
foreach (UserCardData card in cards)
{
if (card.tid == card_tid && card.variant == variant && card.quantity >= quantity)
return true;
}
return false;
}
public bool HasPack(string pack_tid, int quantity=1)
{
foreach (UserCardData pack in packs)
{
if (pack.tid == pack_tid && pack.quantity >= quantity)
return true;
}
return false;
}
public bool HasAvatar(string avatar_tid)
{
return avatars.Contains(avatar_tid);
}
public bool HasCardback(string cardback_tid)
{
return cardbacks.Contains(cardback_tid);
}
public bool HasReward(string reward_id)
{
foreach (string reward in rewards)
{
if (reward == reward_id)
return true;
}
return false;
}
public string GetCoinsString()
{
return coins.ToString();
}
public bool HasFriend(string username)
{
List<string> flist = new List<string>(friends);
return flist.Contains(username);
}
public void AddFriend(string username)
{
List<string> flist = new List<string>(friends);
if (!flist.Contains(username))
flist.Add(username);
friends = flist.ToArray();
}
public void RemoveFriend(string username)
{
List<string> flist = new List<string>(friends);
if (flist.Contains(username))
flist.Remove(username);
friends = flist.ToArray();
}
}
[System.Serializable]
public class UserDeckData : INetworkSerializable
{
public string tid;
public string title;
public UserCardData hero;
public UserCardData[] cards;
public UserDeckData() {}
public UserDeckData(string tid, string title)
{
this.tid = tid;
this.title = title;
hero = new UserCardData();
cards = new UserCardData[0];
}
public UserDeckData(DeckData deck)
{
tid = deck.id;
title = deck.title;
hero = new UserCardData(deck.hero, VariantData.GetDefault());
cards = new UserCardData[deck.cards.Length];
for (int i = 0; i < deck.cards.Length; i++)
{
cards[i] = new UserCardData(deck.cards[i], VariantData.GetDefault());
}
}
public int GetQuantity()
{
int count = 0;
foreach (UserCardData card in cards)
count += card.quantity;
return count;
}
public bool IsValid()
{
return !string.IsNullOrEmpty(tid) && !string.IsNullOrWhiteSpace(title) && GetQuantity() >= GameplayData.Get().deck_size;
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref tid);
serializer.SerializeValue(ref title);
serializer.SerializeValue(ref hero);
NetworkTool.NetSerializeArray(serializer, ref cards);
}
public static UserDeckData Default
{
get
{
UserDeckData deck = new UserDeckData();
deck.tid = "";
deck.title = "";
deck.hero = new UserCardData();
deck.cards = new UserCardData[0];
return deck;
}
}
}
[System.Serializable]
public class UserCardData : INetworkSerializable
{
public string tid;
public string variant;
public int quantity;
public UserCardData() { tid = ""; variant = ""; quantity = 1; }
public UserCardData(string id, string v) { tid = id; variant = v; quantity = 1; }
public UserCardData(CardData card, VariantData variant)
{
this.tid = card != null ? card.id : "";
this.variant = variant != null ? variant.id : "";
this.quantity = 1;
}
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref tid);
serializer.SerializeValue(ref variant);
serializer.SerializeValue(ref quantity);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d3b3bfd879cfe2d4a920e9db8c539b62
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 77c3e5da08ce919409a521ceae835a56
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,34 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Condition that checks the card data matches
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/CardData", order = 10)]
public class ConditionCardData : ConditionData
{
[Header("Card is")]
public CardData card_type;
public ConditionOperatorBool oper;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
return CompareBool(target.card_id == card_type.id, oper);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
return false; //Not a card
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
return false; //Not a card
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dafbad6af09c6ed49bfbe03e2d2bf8f1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,71 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Condition that checks in which pile a card is (deck/discard/hand/board/secrets)
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/CardPile", order = 10)]
public class ConditionCardPile : ConditionData
{
[Header("Card is in pile")]
public PileType type;
public ConditionOperatorBool oper;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
if (target == null)
return false;
if (type == PileType.Hand)
{
return CompareBool(data.IsInHand(target), oper);
}
if (type == PileType.Board)
{
return CompareBool(data.IsOnBoard(target), oper);
}
if (type == PileType.Equipped)
{
return CompareBool(data.IsEquipped(target), oper);
}
if (type == PileType.Deck)
{
return CompareBool(data.IsInDeck(target), oper);
}
if (type == PileType.Discard)
{
return CompareBool(data.IsInDiscard(target), oper);
}
if (type == PileType.Secret)
{
return CompareBool(data.IsInSecret(target), oper);
}
if (type == PileType.Temp)
{
return CompareBool(data.IsInTemp(target), oper);
}
return false;
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
return false; //Player cannot be in a pile
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
return type == PileType.Board && target != Slot.None; //Slot is always on board
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c274f103d1df9814494533f6e0f3aedc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,52 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Condition that checks the type, team and traits of a card
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/CardType", order = 10)]
public class ConditionCardType : ConditionData
{
[Header("Card is of type")]
public CardType has_type;
public TeamData has_team;
public TraitData has_trait;
public ConditionOperatorBool oper;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
return CompareBool(IsTrait(target), oper);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
return false; //Not a card
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
return false; //Not a card
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, CardData target)
{
bool is_type = target.type == has_type || has_type == CardType.None;
bool is_team = target.team == has_team || has_team == null;
bool is_trait = target.HasTrait(has_trait) || has_trait == null;
return (is_type && is_team && is_trait);
}
private bool IsTrait(Card card)
{
bool is_type = card.CardData.type == has_type || has_type == CardType.None;
bool is_team = card.CardData.team == has_team || has_team == null;
bool is_trait = card.HasTrait(has_trait) || has_trait == null;
return (is_type && is_team && is_trait);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d028860b966ab8f44908a62e25280a39
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,95 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
public enum ConditionPlayerType
{
Self = 0,
Opponent = 1,
Both = 2,
}
/// <summary>
/// Trigger condition that count the amount of cards in pile of your choise (deck/discard/hand/board...)
/// Can also only count cards of a specific type/team/trait
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/Count", order = 10)]
public class ConditionCount : ConditionData
{
[Header("Count cards of type")]
public ConditionPlayerType target;
public PileType pile;
public ConditionOperatorInt oper;
public int value;
[Header("Traits")]
public CardType has_type;
public TeamData has_team;
public TraitData has_trait;
public override bool IsTriggerConditionMet(Game data, AbilityData ability, Card caster)
{
int count = 0;
if (target == ConditionPlayerType.Self || target == ConditionPlayerType.Both)
{
Player player = data.GetPlayer(caster.player_id);
count += CountPile(player, pile);
}
if (target == ConditionPlayerType.Opponent || target == ConditionPlayerType.Both)
{
Player player = data.GetOpponentPlayer(caster.player_id);
count += CountPile(player, pile);
}
return CompareInt(count, oper, value);
}
private int CountPile(Player player, PileType pile)
{
List<Card> card_pile = null;
if (pile == PileType.Hand)
card_pile = player.cards_hand;
if (pile == PileType.Board)
card_pile = player.cards_board;
if (pile == PileType.Equipped)
card_pile = player.cards_equip;
if (pile == PileType.Deck)
card_pile = player.cards_deck;
if (pile == PileType.Discard)
card_pile = player.cards_discard;
if (pile == PileType.Secret)
card_pile = player.cards_secret;
if (pile == PileType.Temp)
card_pile = player.cards_temp;
if (card_pile != null)
{
int count = 0;
foreach (Card card in card_pile)
{
if (IsTrait(card))
count++;
}
return count;
}
return 0;
}
private bool IsTrait(Card card)
{
bool is_type = card.CardData.type == has_type || has_type == CardType.None;
bool is_team = card.CardData.team == has_team || has_team == null;
bool is_trait = card.HasTrait(has_trait) || has_trait == null;
return (is_type && is_team && is_trait);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9642af46d64291e468622adccd555b22
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,32 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Check if a card/player is damaged
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/Damaged", order = 10)]
public class ConditionDamaged : ConditionData
{
[Header("Card is damaged")]
public ConditionOperatorBool oper;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
return CompareBool(target.damage > 0, oper);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
return CompareBool(target.hp < target.hp_max, oper);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 39b7e85a87131bc4494769ae3ed91e20
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,27 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Condition that check if the CardData is a valid deckbuilding card (not a summon token)
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/CardDeckbuilding", order = 10)]
public class ConditionDeckbuilding : ConditionData
{
[Header("Card is Deckbuilding")]
public ConditionOperatorBool oper;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
return CompareBool(target.CardData.deckbuilding, oper);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, CardData target)
{
return CompareBool(target.deckbuilding, oper);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 47697886145249247bdbaa7a3452ef34
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,32 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Condition that check if the card is equipped with an equipment card
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/CardEquipped", order = 10)]
public class ConditionEquipped : ConditionData
{
[Header("Target is equipped")]
public ConditionOperatorBool oper;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
return CompareBool(target.equipped_uid != null, oper);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
return false;
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 303a0dbac5969f440a9cf0d8e8c7a577
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,32 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Condition that check if the card is exhausted or not
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/CardExhausted", order = 10)]
public class ConditionExhaust : ConditionData
{
[Header("Target is exhausted")]
public ConditionOperatorBool oper;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
return CompareBool(target.exhausted, oper);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
return CompareBool(false, oper);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
return CompareBool(false, oper);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d05746ad826de244783b23ff99b0cbe5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Add this to an ability to prevent it from being cast more than once per turn
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/OncePerTurn", order = 10)]
public class ConditionOnce : ConditionData
{
public override bool IsTriggerConditionMet(Game data, AbilityData ability, Card caster)
{
return !data.ability_played.Contains(ability.id);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f3b28cf451548d840bae6f9839af5596
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,35 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Condition that check the owner of the target match the owner of the caster
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/CardOwner", order = 10)]
public class ConditionOwner : ConditionData
{
[Header("Target owner is caster owner")]
public ConditionOperatorBool oper;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
bool same_owner = caster.player_id == target.player_id;
return CompareBool(same_owner, oper);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
bool same_owner = caster.player_id == target.player_id;
return CompareBool(same_owner, oper);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
bool same_owner = Slot.GetP(caster.player_id) == target.p;
return CompareBool(same_owner, oper);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 004c422eee4e9ee41a7603d981a2fff7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,51 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Condition that is checked only by the AI.
/// Prevents the AI from targeting itself with bad spells even though you want to give real players the flexibility to do it
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/CardOwnerAI", order = 10)]
public class ConditionOwnerAI : ConditionData
{
[Header("AI Only: Target owner is caster owner")]
public ConditionOperatorBool oper;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
if (!IsAIPlayer(data, caster))
return true; //Condition always true for human players
bool same_owner = caster.player_id == target.player_id;
return CompareBool(same_owner, oper);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
if (!IsAIPlayer(data, caster))
return true; //Condition always true for human players
bool same_owner = caster.player_id == target.player_id;
return CompareBool(same_owner, oper);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
if (!IsAIPlayer(data, caster))
return true; //Condition always true for human players
bool same_owner = Slot.GetP(caster.player_id) == target.p;
return CompareBool(same_owner, oper);
}
private bool IsAIPlayer(Game data, Card caster)
{
Player player = data.GetPlayer(caster.player_id);
return player.is_ai;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b5f2a0ee74f5ccd428221969a56ee854
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,40 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Compares basic player stats such as attack/hp/mana
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/PlayerStat", order = 10)]
public class ConditionPlayerStat : ConditionData
{
[Header("Card stat is")]
public ConditionStatType type;
public ConditionOperatorInt oper;
public int value;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
Player ptarget = data.GetPlayer(target.player_id);
return IsTargetConditionMet(data, ability, caster, ptarget);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
if (type == ConditionStatType.HP)
{
return CompareInt(target.hp, oper, value);
}
if (type == ConditionStatType.Mana)
{
return CompareInt(target.mana, oper, value);
}
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2829dec3f20bf1749be842a462dc7209
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,38 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Checks if its your turn
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/RolledValue", order = 10)]
public class ConditionRolled : ConditionData
{
[Header("Value Rolled is")]
public ConditionOperatorInt oper;
public int value;
public override bool IsTriggerConditionMet(Game data, AbilityData ability, Card caster)
{
return CompareInt(data.rolled_value, oper, value);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
return CompareInt(data.rolled_value, oper, value);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
return CompareInt(data.rolled_value, oper, value);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
return CompareInt(data.rolled_value, oper, value);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d038beb97da312344a27296b92629b7e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,23 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Check selected value for card cost
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/SelectedValue", order = 10)]
public class ConditionSelectedValue : ConditionData
{
[Header("Selected Value is")]
public ConditionOperatorInt oper;
public int value;
public override bool IsTriggerConditionMet(Game data, AbilityData ability, Card caster)
{
return CompareInt(data.selected_value, oper, value);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b36c860069675084cbf42329be6ed7bf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Condition that check if the target is the same as the caster
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/CardSelf", order = 10)]
public class ConditionSelf : ConditionData
{
[Header("Target is caster")]
public ConditionOperatorBool oper;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
return CompareBool(caster == target, oper);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
bool same_owner = caster.player_id == target.player_id;
return CompareBool(same_owner, oper);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
return CompareBool(caster.slot == target, oper);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0b48d9d9834b3f54689381231f73f1d2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,32 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// SlotDist is the travel distance from the caster to the target
/// Unlike SlotRange which is just checking each X,Y,P separately
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/SlotDist", order = 11)]
public class ConditionSlotDist : ConditionData
{
[Header("Slot Distance")]
public int distance = 1;
public bool diagonals;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
return IsTargetConditionMet(data, ability, caster, target.slot);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
Slot cslot = caster.slot;
if (diagonals)
return cslot.IsInDistance(target, distance);
return cslot.IsInDistanceStraight(target, distance);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 23876ed3b55361b4e8abbc86b94a342d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Checks if a slot contains a card or not
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/SlotEmpty", order = 11)]
public class ConditionSlotEmpty : ConditionData
{
[Header("Slot Is Empty")]
public ConditionOperatorBool oper;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
return CompareBool(false, oper); //Target is not empty slot
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
return CompareBool(false, oper); //Target is not empty slot
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
Card slot_card = data.GetSlotCard(target);
return CompareBool(slot_card == null, oper);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d3dd4e502c8dfa34fbb025ff1a8aaeb0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,34 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// SlotRange check each axis variable individualy for range between the caster and target
/// If you want to check the travel distance instead (all at once) use SlotDist
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/SlotRange", order = 11)]
public class ConditionSlotRange : ConditionData
{
[Header("Slot Range")]
public int range_x = 1;
public int range_y = 1;
public int range_p = 0;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
return IsTargetConditionMet(data, ability, caster, target.slot);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
Slot cslot = caster.slot;
int dist_x = Mathf.Abs(cslot.x - target.x);
int dist_y = Mathf.Abs(cslot.y - target.y);
int dist_p = Mathf.Abs(cslot.p - target.p);
return dist_x <= range_x && dist_y <= range_y && dist_p <= range_p;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8debe5efbe667de4fbf47dc9212bd159
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,33 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// SlotValue compare each slot x and y to a specific value, like slot.x >=3 and slot.y < 5
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/SlotValue", order = 11)]
public class ConditionSlotValue : ConditionData
{
[Header("Slot Value")]
public ConditionOperatorInt oper_x;
public int value_x = 0;
public ConditionOperatorInt oper_y;
public int value_y = 0;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
return IsTargetConditionMet(data, ability, caster, target.slot);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
bool valid_x = CompareInt(target.x, oper_x, value_x);
bool valid_y = CompareInt(target.y, oper_y, value_y);
return valid_x && valid_y;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 274374999bbffee429c9dfa75d2592db
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,62 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
public enum ConditionStatType
{
None = 0,
Attack = 10,
HP = 20,
Mana = 30,
}
/// <summary>
/// Compares basic card or player stats such as attack/hp/mana
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/Stat", order = 10)]
public class ConditionStat : ConditionData
{
[Header("Card stat is")]
public ConditionStatType type;
public ConditionOperatorInt oper;
public int value;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
if (type == ConditionStatType.Attack)
{
return CompareInt(target.GetAttack(), oper, value);
}
if (type == ConditionStatType.HP)
{
return CompareInt(target.GetHP(), oper, value);
}
if (type == ConditionStatType.Mana)
{
return CompareInt(target.GetMana(), oper, value);
}
return false;
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
if (type == ConditionStatType.HP)
{
return CompareInt(target.hp, oper, value);
}
if (type == ConditionStatType.Mana)
{
return CompareInt(target.mana, oper, value);
}
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 56b5ff935f8b7194ea353bb38598d96e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,37 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
//Checks if a player or card has a status effect
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/CardStatus", order = 10)]
public class ConditionStatus : ConditionData
{
[Header("Card has status")]
public StatusType has_status;
public int value = 0;
public ConditionOperatorBool oper;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
bool hstatus = target.HasStatus(has_status) && target.GetStatusValue(has_status) >= value;
return CompareBool(hstatus, oper);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
bool hstatus = target.HasStatus(has_status) && target.GetStatusValue(has_status) >= value;
return CompareBool(hstatus, oper);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
Card card = data.GetSlotCard(target);
if (card != null)
return IsTargetConditionMet(data, ability, caster, card);
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7a3c78f482b96ed47896e9c6ad583d1e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,42 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TcgEngine.AI;
namespace TcgEngine
{
/// <summary>
/// Condition that compares the target category of an ability to the actual target (card, player or slot)
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/Player", order = 10)]
public class ConditionTarget : ConditionData
{
[Header("Target is of type")]
public ConditionTargetType type;
public ConditionOperatorBool oper;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
return CompareBool(type == ConditionTargetType.Card, oper); //Is Card
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
return CompareBool(type == ConditionTargetType.Player, oper); //Is Player
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
return CompareBool(type == ConditionTargetType.Slot, oper); //Is Player
}
}
public enum ConditionTargetType
{
None = 0,
Card = 10,
Player = 20,
Slot = 30,
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 06df69f70efef364b8eab89605ed0d70
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,29 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Compares cards or players custom stats
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/StatCustom", order = 10)]
public class ConditionTrait : ConditionData
{
[Header("Card stat is")]
public TraitData trait;
public ConditionOperatorInt oper;
public int value;
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
return CompareInt(target.GetTraitValue(trait.id), oper, value);
}
public override bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
return CompareInt(target.GetTraitValue(trait.id), oper, value);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bc5ee312d5d0efd448bf6e1a851af5ce
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,22 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Checks if its your turn
/// </summary>
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/Turn", order = 10)]
public class ConditionTurn : ConditionData
{
public ConditionOperatorBool oper;
public override bool IsTriggerConditionMet(Game data, AbilityData ability, Card caster)
{
bool yourturn = caster.player_id == data.current_player;
return CompareBool(yourturn, oper);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dbbc55df9ef91354d88c2afe3b5105b4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,38 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
//Pick X first cards from the source array
[CreateAssetMenu(fileName = "filter", menuName = "TcgEngine/Filter/First", order = 10)]
public class FilterFirst : FilterData
{
public int amount = 1; //Number of first targets selected
public override List<Card> FilterTargets(Game data, AbilityData ability, Card caster, List<Card> source, List<Card> dest)
{
int max = Mathf.Min(source.Count, amount);
for (int i = 0; i < max; i++)
dest.Add(source[i]);
return dest;
}
public override List<Player> FilterTargets(Game data, AbilityData ability, Card caster, List<Player> source, List<Player> dest)
{
int max = Mathf.Min(source.Count, amount);
for (int i = 0; i < max; i++)
dest.Add(source[i]);
return dest;
}
public override List<Slot> FilterTargets(Game data, AbilityData ability, Card caster, List<Slot> source, List<Slot> dest)
{
int max = Mathf.Min(source.Count, amount);
for (int i = 0; i < max; i++)
dest.Add(source[i]);
return dest;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 344a45cba28a5db439b6bf1f57127475
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,53 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
//Pick all targets with the highest stat
[CreateAssetMenu(fileName = "filter", menuName = "TcgEngine/Filter/HighestStat", order = 10)]
public class FilterHighestStat : FilterData
{
public ConditionStatType stat;
public override List<Card> FilterTargets(Game data, AbilityData ability, Card caster, List<Card> source, List<Card> dest)
{
//Find highest
int highest = -999;
foreach (Card card in source)
{
int stat = GetStat(card);
if (stat > highest)
highest = stat;
}
//Add all highest
foreach (Card card in source)
{
int stat = GetStat(card);
if (stat == highest)
dest.Add(card);
}
return dest;
}
private int GetStat(Card card)
{
if (stat == ConditionStatType.Attack)
{
return card.GetAttack();
}
if (stat == ConditionStatType.HP)
{
return card.GetHP();
}
if (stat == ConditionStatType.Mana)
{
return card.GetMana();
}
return 0;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 26d6c56df3cf5e64d857568f0f8cc444
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,41 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
//Pick X first cards from the source array
[CreateAssetMenu(fileName = "filter", menuName = "TcgEngine/Filter/Last", order = 10)]
public class FilterLast : FilterData
{
public int amount = 1; //Number of first targets selected
public override List<Card> FilterTargets(Game data, AbilityData ability, Card caster, List<Card> source, List<Card> dest)
{
int max = Mathf.Min(source.Count, amount);
int min = source.Count - max;
for (int i = source.Count-1; i >= min; i--)
dest.Add(source[i]);
return dest;
}
public override List<Player> FilterTargets(Game data, AbilityData ability, Card caster, List<Player> source, List<Player> dest)
{
int max = Mathf.Min(source.Count, amount);
int min = source.Count - max;
for (int i = source.Count - 1; i >= min; i--)
dest.Add(source[i]);
return dest;
}
public override List<Slot> FilterTargets(Game data, AbilityData ability, Card caster, List<Slot> source, List<Slot> dest)
{
int max = Mathf.Min(source.Count, amount);
int min = source.Count - max;
for (int i = source.Count - 1; i >= min; i--)
dest.Add(source[i]);
return dest;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 08d4e8ec101e29f47a83f7531bc32e74
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,53 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
//Pick all targets with the lowest stat
[CreateAssetMenu(fileName = "filter", menuName = "TcgEngine/Filter/LowestStat", order = 10)]
public class FilterLowestStat : FilterData
{
public ConditionStatType stat;
public override List<Card> FilterTargets(Game data, AbilityData ability, Card caster, List<Card> source, List<Card> dest)
{
//Find lowest
int lowest = 99999;
foreach (Card card in source)
{
int stat = GetStat(card);
if (stat < lowest)
lowest = stat;
}
//Add all lowest
foreach (Card card in source)
{
int stat = GetStat(card);
if (stat == lowest)
dest.Add(card);
}
return dest;
}
private int GetStat(Card card)
{
if (stat == ConditionStatType.Attack)
{
return card.GetAttack();
}
if (stat == ConditionStatType.HP)
{
return card.GetHP();
}
if (stat == ConditionStatType.Mana)
{
return card.GetMana();
}
return 0;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8bda5e3e8df054240a5ff29e526a22a4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,34 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
//Pick X number of targets at random from the source array
[CreateAssetMenu(fileName = "filter", menuName = "TcgEngine/Filter/Random", order = 10)]
public class FilterRandom : FilterData
{
public int amount = 1; //Number of random targets selected
public override List<Card> FilterTargets(Game data, AbilityData ability, Card caster, List<Card> source, List<Card> dest)
{
return GameTool.PickXRandom(source, dest, amount);
}
public override List<Player> FilterTargets(Game data, AbilityData ability, Card caster, List<Player> source, List<Player> dest)
{
return GameTool.PickXRandom(source, dest, amount);
}
public override List<Slot> FilterTargets(Game data, AbilityData ability, Card caster, List<Slot> source, List<Slot> dest)
{
return GameTool.PickXRandom(source, dest, amount);
}
public override List<CardData> FilterTargets(Game data, AbilityData ability, Card caster, List<CardData> source, List<CardData> dest)
{
return GameTool.PickXRandom(source, dest, amount);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e5f59c8a06280cb40b425b437dec3c57
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 69515c7f3e81be04c9a5916ddd6a91ab
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,692 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TcgEngine.Gameplay;
namespace TcgEngine
{
/// <summary>
/// Defines all ability data
/// </summary>
[CreateAssetMenu(fileName = "ability", menuName = "TcgEngine/AbilityData", order = 5)]
public class AbilityData : ScriptableObject
{
public string id;
[Header("Trigger")]
public AbilityTrigger trigger; //WHEN does the ability trigger?
public ConditionData[] conditions_trigger; //Condition checked on the card triggering the ability (usually the caster)
[Header("Target")]
public AbilityTarget target; //WHO is targeted?
public ConditionData[] conditions_target; //Condition checked on the target to know if its a valid taget
public FilterData[] filters_target; //Condition checked on the target to know if its a valid taget
[Header("Effect")]
public EffectData[] effects; //WHAT this does?
public StatusData[] status; //Status added by this ability
public int value; //Value passed to the effect (deal X damage)
public int duration; //Duration passed to the effect (usually for status, 0=permanent)
[Header("Chain/Choices")]
public AbilityData[] chain_abilities; //Abilities that will be triggered after this one
[Header("Activated Ability")]
public int mana_cost; //Mana cost for activated abilities
public bool exhaust; //Action cost for activated abilities
[Header("FX")]
public GameObject board_fx;
public GameObject caster_fx;
public GameObject target_fx;
public GameObject projectile_fx;
public AudioClip cast_audio;
public AudioClip target_audio;
public bool charge_target;
[Header("Text")]
public string title;
[TextArea(5, 7)]
public string desc;
public static List<AbilityData> ability_list = new List<AbilityData>(); //Faster access in loops
public static Dictionary<string, AbilityData> ability_dict = new Dictionary<string, AbilityData>(); //Faster access in Get(id)
public static void Load(string folder = "")
{
if (ability_list.Count == 0)
{
ability_list.AddRange(Resources.LoadAll<AbilityData>(folder));
foreach (AbilityData ability in ability_list)
ability_dict.Add(ability.id, ability);
}
}
public string GetTitle()
{
return title;
}
public string GetDesc()
{
return desc;
}
public string GetDesc(CardData card)
{
string dsc = desc;
dsc = dsc.Replace("<name>", card.title);
dsc = dsc.Replace("<value>", value.ToString());
dsc = dsc.Replace("<duration>", duration.ToString());
return dsc;
}
//Generic condition for the ability to trigger
public bool AreTriggerConditionsMet(Game data, Card caster)
{
return AreTriggerConditionsMet(data, caster, caster); //Triggerer is the caster
}
//Some abilities are caused by another card (PlayOther), otherwise most of the time the triggerer is the caster, check condition on triggerer
public bool AreTriggerConditionsMet(Game data, Card caster, Card trigger_card)
{
foreach (ConditionData cond in conditions_trigger)
{
if (cond != null)
{
if (!cond.IsTriggerConditionMet(data, this, caster))
return false;
if (!cond.IsTargetConditionMet(data, this, caster, trigger_card))
return false;
}
}
return true;
}
//Some abilities are caused by an action on a player (OnFight when attacking the player), check condition on that player
public bool AreTriggerConditionsMet(Game data, Card caster, Player trigger_player)
{
foreach (ConditionData cond in conditions_trigger)
{
if (cond != null)
{
if (!cond.IsTriggerConditionMet(data, this, caster))
return false;
if (!cond.IsTargetConditionMet(data, this, caster, trigger_player))
return false;
}
}
return true;
}
//Check if the card target is valid
public bool AreTargetConditionsMet(Game data, Card caster, Card target_card)
{
foreach (ConditionData cond in conditions_target)
{
if (cond != null && !cond.IsTargetConditionMet(data, this, caster, target_card))
return false;
}
return true;
}
//Check if the player target is valid
public bool AreTargetConditionsMet(Game data, Card caster, Player target_player)
{
foreach (ConditionData cond in conditions_target)
{
if (cond != null && !cond.IsTargetConditionMet(data, this, caster, target_player))
return false;
}
return true;
}
//Check if the slot target is valid
public bool AreTargetConditionsMet(Game data, Card caster, Slot target_slot)
{
foreach (ConditionData cond in conditions_target)
{
if (cond != null && !cond.IsTargetConditionMet(data, this, caster, target_slot))
return false;
}
return true;
}
//Check if the card data target is valid
public bool AreTargetConditionsMet(Game data, Card caster, CardData target_card)
{
foreach (ConditionData cond in conditions_target)
{
if (cond != null && !cond.IsTargetConditionMet(data, this, caster, target_card))
return false;
}
return true;
}
//CanTarget is similar to AreTargetConditionsMet but only applies to targets on the board, with extra board-only conditions
public bool CanTarget(Game data, Card caster, Card target)
{
if (target.HasStatus(StatusType.Stealth))
return false; //Hidden
if (target.HasStatus(StatusType.SpellImmunity))
return false; //Spell immunity
bool condition_match = AreTargetConditionsMet(data, caster, target);
return condition_match;
}
//Can target check additional restrictions and is usually for SelectTarget or PlayTarget abilities
public bool CanTarget(Game data, Card caster, Player target)
{
bool condition_match = AreTargetConditionsMet(data, caster, target);
return condition_match;
}
public bool CanTarget(Game data, Card caster, Slot target)
{
return AreTargetConditionsMet(data, caster, target); //No additional conditions for slots
}
//Check if destination array has the target after being filtered, used to support filters in CardSelector
public bool IsCardSelectionValid(Game data, Card caster, Card target, ListSwap<Card> card_array = null)
{
List<Card> targets = GetCardTargets(data, caster, card_array);
return targets.Contains(target); //Card is still in array after filtering
}
public void DoEffects(GameLogic logic, Card caster)
{
foreach(EffectData effect in effects)
effect?.DoEffect(logic, this, caster);
}
public void DoEffects(GameLogic logic, Card caster, Card target)
{
foreach (EffectData effect in effects)
effect?.DoEffect(logic, this, caster, target);
foreach(StatusData stat in status)
target.AddStatus(stat, value, duration);
}
public void DoEffects(GameLogic logic, Card caster, Player target)
{
foreach (EffectData effect in effects)
effect?.DoEffect(logic, this, caster, target);
foreach (StatusData stat in status)
target.AddStatus(stat, value, duration);
}
public void DoEffects(GameLogic logic, Card caster, Slot target)
{
foreach (EffectData effect in effects)
effect?.DoEffect(logic, this, caster, target);
}
public void DoEffects(GameLogic logic, Card caster, CardData target)
{
foreach (EffectData effect in effects)
effect?.DoEffect(logic, this, caster, target);
}
public void DoOngoingEffects(GameLogic logic, Card caster, Card target)
{
foreach (EffectData effect in effects)
effect?.DoOngoingEffect(logic, this, caster, target);
foreach (StatusData stat in status)
target.AddOngoingStatus(stat, value);
}
public void DoOngoingEffects(GameLogic logic, Card caster, Player target)
{
foreach (EffectData effect in effects)
effect?.DoOngoingEffect(logic, this, caster, target);
foreach (StatusData stat in status)
target.AddOngoingStatus(stat, value);
}
public bool HasEffect<T>() where T : EffectData
{
foreach (EffectData eff in effects)
{
if (eff != null && eff is T)
return true;
}
return false;
}
public bool HasStatus(StatusType type)
{
foreach (StatusData sta in status)
{
if (sta != null && sta.effect == type)
return true;
}
return false;
}
public int GetDamage()
{
int damage = 0;
foreach (EffectData eff in effects)
{
if (eff != null && eff is EffectDamage)
{
damage += this.value;
}
}
return damage;
}
private void AddValidCards(Game data, Card caster, List<Card> source, List<Card> targets)
{
foreach (Card card in source)
{
if (AreTargetConditionsMet(data, caster, card))
targets.Add(card);
}
}
//Return cards targets, memory_array is used for optimization and avoid allocating new memory
public List<Card> GetCardTargets(Game data, Card caster, ListSwap<Card> memory_array = null)
{
if (memory_array == null)
memory_array = new ListSwap<Card>(); //Slow operation
List<Card> targets = memory_array.Get();
if (target == AbilityTarget.Self)
{
if (AreTargetConditionsMet(data, caster, caster))
targets.Add(caster);
}
if (target == AbilityTarget.AllCardsBoard || target == AbilityTarget.SelectTarget)
{
foreach (Player player in data.players)
{
foreach (Card card in player.cards_board)
{
if (AreTargetConditionsMet(data, caster, card))
targets.Add(card);
}
}
}
if (target == AbilityTarget.AllCardsHand)
{
foreach (Player player in data.players)
{
foreach (Card card in player.cards_hand)
{
if (AreTargetConditionsMet(data, caster, card))
targets.Add(card);
}
}
}
if (target == AbilityTarget.AllCardsAllPiles || target == AbilityTarget.CardSelector)
{
foreach (Player player in data.players)
{
AddValidCards(data, caster, player.cards_deck, targets);
AddValidCards(data, caster, player.cards_discard, targets);
AddValidCards(data, caster, player.cards_hand, targets);
AddValidCards(data, caster, player.cards_secret, targets);
AddValidCards(data, caster, player.cards_board, targets);
AddValidCards(data, caster, player.cards_equip, targets);
AddValidCards(data, caster, player.cards_temp, targets);
}
}
if (target == AbilityTarget.LastPlayed)
{
Card target = data.GetCard(data.last_played);
if (target != null && AreTargetConditionsMet(data, caster, target))
targets.Add(target);
}
if (target == AbilityTarget.LastDestroyed)
{
Card target = data.GetCard(data.last_destroyed);
if (target != null && AreTargetConditionsMet(data, caster, target))
targets.Add(target);
}
if (target == AbilityTarget.LastTargeted)
{
Card target = data.GetCard(data.last_target);
if (target != null && AreTargetConditionsMet(data, caster, target))
targets.Add(target);
}
if (target == AbilityTarget.LastSummoned)
{
Card target = data.GetCard(data.last_summoned);
if (target != null && AreTargetConditionsMet(data, caster, target))
targets.Add(target);
}
if (target == AbilityTarget.AbilityTriggerer)
{
Card target = data.GetCard(data.ability_triggerer);
if (target != null && AreTargetConditionsMet(data, caster, target))
targets.Add(target);
}
if (target == AbilityTarget.EquippedCard)
{
if (caster.CardData.IsEquipment())
{
//Get bearer of the equipment
Player player = data.GetPlayer(caster.player_id);
Card target = player.GetBearerCard(caster);
if (target != null && AreTargetConditionsMet(data, caster, target))
targets.Add(target);
}
else if(caster.equipped_uid != null)
{
//Get equipped card
Card target = data.GetCard(caster.equipped_uid);
if (target != null && AreTargetConditionsMet(data, caster, target))
targets.Add(target);
}
}
//Filter targets
if (filters_target != null && targets.Count > 0)
{
foreach (FilterData filter in filters_target)
{
if (filter != null)
targets = filter.FilterTargets(data, this, caster, targets, memory_array.GetOther(targets));
}
}
return targets;
}
//Return player targets, memory_array is used for optimization and avoid allocating new memory
public List<Player> GetPlayerTargets(Game data, Card caster, ListSwap<Player> memory_array = null)
{
if (memory_array == null)
memory_array = new ListSwap<Player>(); //Slow operation
List<Player> targets = memory_array.Get();
if (target == AbilityTarget.PlayerSelf)
{
Player player = data.GetPlayer(caster.player_id);
targets.Add(player);
}
else if (target == AbilityTarget.PlayerOpponent)
{
for (int tp = 0; tp < data.players.Length; tp++)
{
if (tp != caster.player_id)
{
Player oplayer = data.players[tp];
targets.Add(oplayer);
}
}
}
else if (target == AbilityTarget.AllPlayers)
{
foreach (Player player in data.players)
{
if (AreTargetConditionsMet(data, caster, player))
targets.Add(player);
}
}
//Filter targets
if (filters_target != null && targets.Count > 0)
{
foreach (FilterData filter in filters_target)
{
if (filter != null)
targets = filter.FilterTargets(data, this, caster, targets, memory_array.GetOther(targets));
}
}
return targets;
}
//Return slot targets, memory_array is used for optimization and avoid allocating new memory
public List<Slot> GetSlotTargets(Game data, Card caster, ListSwap<Slot> memory_array = null)
{
if (memory_array == null)
memory_array = new ListSwap<Slot>(); //Slow operation
List<Slot> targets = memory_array.Get();
if (target == AbilityTarget.AllSlots)
{
List<Slot> slots = Slot.GetAll();
foreach (Slot slot in slots)
{
if (AreTargetConditionsMet(data, caster, slot))
targets.Add(slot);
}
}
//Filter targets
if (filters_target != null && targets.Count > 0)
{
foreach (FilterData filter in filters_target)
{
if (filter != null)
targets = filter.FilterTargets(data, this, caster, targets, memory_array.GetOther(targets));
}
}
return targets;
}
public List<CardData> GetCardDataTargets(Game data, Card caster, ListSwap<CardData> memory_array = null)
{
if (memory_array == null)
memory_array = new ListSwap<CardData>(); //Slow operation
List<CardData> targets = memory_array.Get();
if (target == AbilityTarget.AllCardData)
{
foreach (CardData card in CardData.GetAll())
{
if (AreTargetConditionsMet(data, caster, card))
targets.Add(card);
}
}
//Filter targets
if (filters_target != null && targets.Count > 0)
{
foreach (FilterData filter in filters_target)
{
if (filter != null)
targets = filter.FilterTargets(data, this, caster, targets, memory_array.GetOther(targets));
}
}
return targets;
}
// Check if there is any valid target, if not, AI wont try to cast activated ability
public bool HasValidSelectTarget(Game game_data, Card caster)
{
if (target == AbilityTarget.SelectTarget)
{
if (HasValidBoardCardTarget(game_data, caster))
return true;
if (HasValidPlayerTarget(game_data, caster))
return true;
if (HasValidSlotTarget(game_data, caster))
return true;
return false;
}
if (target == AbilityTarget.CardSelector)
{
if (HasValidCardTarget(game_data, caster))
return true;
return false;
}
if (target == AbilityTarget.ChoiceSelector)
{
foreach (AbilityData choice in chain_abilities)
{
if(choice.AreTriggerConditionsMet(game_data, caster))
return true;
}
return false;
}
return true; //Not selecting, valid
}
public bool HasValidBoardCardTarget(Game game_data, Card caster)
{
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];
if (CanTarget(game_data, caster, card))
return true;
}
}
return false;
}
public bool HasValidCardTarget(Game game_data, Card caster)
{
for (int p = 0; p < game_data.players.Length; p++)
{
Player player = game_data.players[p];
bool v1 = HasValidCardTarget(game_data, caster, player.cards_deck);
bool v2 = HasValidCardTarget(game_data, caster, player.cards_discard);
bool v3 = HasValidCardTarget(game_data, caster, player.cards_hand);
bool v4 = HasValidCardTarget(game_data, caster, player.cards_board);
bool v5 = HasValidCardTarget(game_data, caster, player.cards_equip);
bool v6 = HasValidCardTarget(game_data, caster, player.cards_secret);
bool v7 = HasValidCardTarget(game_data, caster, player.cards_temp);
if (v1 || v2 || v3 || v4 || v5 || v6 || v7)
return true;
}
return false;
}
public bool HasValidCardTarget(Game game_data, Card caster, List<Card> list)
{
for (int c = 0; c < list.Count; c++)
{
Card card = list[c];
if (AreTargetConditionsMet(game_data, caster, card))
return true;
}
return false;
}
public bool HasValidPlayerTarget(Game game_data, Card caster)
{
for (int p = 0; p < game_data.players.Length; p++)
{
Player player = game_data.players[p];
if (CanTarget(game_data, caster, player))
return true;
}
return false;
}
public bool HasValidSlotTarget(Game game_data, Card caster)
{
foreach (Slot slot in Slot.GetAll())
{
if (CanTarget(game_data, caster, slot))
return true;
}
return false;
}
public bool IsSelector()
{
return target == AbilityTarget.SelectTarget || target == AbilityTarget.CardSelector || target == AbilityTarget.ChoiceSelector;
}
public static AbilityData Get(string id)
{
if (id == null)
return null;
bool success = ability_dict.TryGetValue(id, out AbilityData ability);
if (success)
return ability;
return null;
}
public static List<AbilityData> GetAll()
{
return ability_list;
}
}
public enum AbilityTrigger
{
None = 0,
Ongoing = 2, //Always active (does not work with all effects)
Activate = 5, //Action
OnPlay = 10, //When playeds
OnPlayOther = 12, //When another card played
StartOfTurn = 20, //Every turn
EndOfTurn = 22, //Every turn
OnBeforeAttack = 30, //When attacking, before damage
OnAfterAttack = 31, //When attacking, after damage if still alive
OnBeforeDefend = 32, //When being attacked, before damage
OnAfterDefend = 33, //When being attacked, after damage if still alive
OnKill = 35, //When killing another card during an attack
OnDeath = 40, //When dying
OnDeathOther = 42, //When another dying
}
public enum AbilityTarget
{
None = 0,
Self = 1,
PlayerSelf = 4,
PlayerOpponent = 5,
AllPlayers = 7,
AllCardsBoard = 10,
AllCardsHand = 11,
AllCardsAllPiles = 12,
AllSlots = 15,
AllCardData = 17, //For card Create effects only
PlayTarget = 20, //The target selected at the same time the spell was played (spell only)
AbilityTriggerer = 25, //The card that triggered the trap
EquippedCard = 27, //If equipment, the bearer, if character, the item equipped
SelectTarget = 30, //Select a card, player or slot on board
CardSelector = 40, //Card selector menu
ChoiceSelector = 50, //Choice selector menu
LastPlayed = 70, //Last card that was played
LastTargeted = 72, //Last card that was targeted with an ability
LastDestroyed = 74, //Last card that was killed
LastSummoned = 77, //Last card that was summoned or created
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8ce5f81e5cc37f547af6923758602c8c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,50 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
//Default fx and audio, some can be overrided on each individual card
[CreateAssetMenu(fileName = "AssetData", menuName = "TcgEngine/AssetData", order = 0)]
public class AssetData : ScriptableObject
{
[Header("FX")]
public GameObject card_spawn_fx;
public GameObject card_destroy_fx;
public GameObject card_attack_fx;
public GameObject card_damage_fx;
public GameObject card_exhausted_fx;
public GameObject player_damage_fx;
public GameObject damage_fx;
public GameObject play_card_fx;
public GameObject play_card_other_fx;
public GameObject play_secret_fx;
public GameObject play_secret_other_fx;
public GameObject dice_roll_fx;
public GameObject hover_text_box;
public GameObject new_turn_fx;
public GameObject win_fx;
public GameObject lose_fx;
public GameObject tied_fx;
[Header("Audio")]
public AudioClip card_spawn_audio;
public AudioClip card_destroy_audio;
public AudioClip card_attack_audio;
public AudioClip card_move_audio;
public AudioClip card_damage_audio;
public AudioClip player_damage_audio;
public AudioClip hand_card_click_audio;
public AudioClip new_turn_audio;
public AudioClip win_audio;
public AudioClip defeat_audio;
public AudioClip win_music;
public AudioClip defeat_music;
public static AssetData Get()
{
return DataLoader.Get().assets;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cca20da9cbfd28a449b51ac452845bc8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,48 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Defines all avatar data
/// </summary>
[CreateAssetMenu(fileName = "Avatar", menuName = "TcgEngine/Avatar", order = 10)]
public class AvatarData : ScriptableObject
{
public string id;
public Sprite avatar;
public int sort_order;
public static List<AvatarData> avatar_list = new List<AvatarData>();
public static void Load(string folder = "")
{
if (avatar_list.Count == 0)
avatar_list.AddRange(Resources.LoadAll<AvatarData>(folder));
avatar_list.Sort((AvatarData a, AvatarData b) => {
if (a.sort_order == b.sort_order)
return a.id.CompareTo(b.id);
else
return a.sort_order.CompareTo(b.sort_order);
});
}
public static AvatarData Get(string id)
{
foreach (AvatarData avatar in GetAll())
{
if (avatar.id == id)
return avatar;
}
return null;
}
public static List<AvatarData> GetAll()
{
return avatar_list;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c92c9d3c273cc8940be4b9e940d8933e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,316 @@
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
public enum CardType
{
None = 0,
Hero = 5,
Character = 10,
Spell = 20,
Artifact = 30,
Secret = 40,
Equipment = 50,
}
/// <summary>
/// Defines all card data
/// </summary>
[CreateAssetMenu(fileName = "card", menuName = "TcgEngine/CardData", order = 5)]
public class CardData : ScriptableObject
{
public string id;
[Header("Display")]
public string title;
public Sprite art_full;
public Sprite art_board;
[Header("Stats")]
public CardType type;
public TeamData team;
public RarityData rarity;
public int mana;
public int attack;
public int hp;
[Header("Traits")]
public TraitData[] traits;
public TraitStat[] stats;
[Header("Abilities")]
public AbilityData[] abilities;
[Header("Card Text")]
[TextArea(3, 5)]
public string text;
[Header("Description")]
[TextArea(5, 10)]
public string desc;
[Header("FX")]
public GameObject spawn_fx;
public GameObject death_fx;
public GameObject attack_fx;
public GameObject damage_fx;
public GameObject idle_fx;
public AudioClip spawn_audio;
public AudioClip death_audio;
public AudioClip attack_audio;
public AudioClip damage_audio;
[Header("Availability")]
public bool deckbuilding = false;
public int cost = 100;
public PackData[] packs;
public static List<CardData> card_list = new List<CardData>(); //Faster access in loops
public static Dictionary<string, CardData> card_dict = new Dictionary<string, CardData>(); //Faster access in Get(id)
public static void Load(string folder = "")
{
if (card_list.Count == 0)
{
card_list.AddRange(Resources.LoadAll<CardData>(folder));
foreach (CardData card in card_list)
card_dict.Add(card.id, card);
}
}
public Sprite GetBoardArt(VariantData variant)
{
return art_board;
}
public Sprite GetFullArt(VariantData variant)
{
return art_full;
}
public string GetTitle()
{
return title;
}
public string GetText()
{
return text;
}
public string GetDesc()
{
return desc;
}
public string GetTypeId()
{
if (type == CardType.Hero)
return "hero";
if (type == CardType.Character)
return "character";
if (type == CardType.Artifact)
return "artifact";
if (type == CardType.Spell)
return "spell";
if (type == CardType.Secret)
return "secret";
if (type == CardType.Equipment)
return "equipment";
return "";
}
public string GetAbilitiesDesc()
{
string txt = "";
foreach (AbilityData ability in abilities)
{
if (!string.IsNullOrWhiteSpace(ability.desc))
txt += "<b>" + ability.GetTitle() + ":</b> " + ability.GetDesc(this) + "\n";
}
return txt;
}
public bool IsCharacter()
{
return type == CardType.Character;
}
public bool IsSecret()
{
return type == CardType.Secret;
}
public bool IsBoardCard()
{
return type == CardType.Character || type == CardType.Artifact;
}
public bool IsRequireTarget()
{
return type == CardType.Equipment || IsRequireTargetSpell();
}
public bool IsRequireTargetSpell()
{
return type == CardType.Spell && HasAbility(AbilityTrigger.OnPlay, AbilityTarget.PlayTarget);
}
public bool IsEquipment()
{
return type == CardType.Equipment;
}
public bool IsDynamicManaCost()
{
return mana > 99;
}
public bool HasTrait(string trait)
{
foreach (TraitData t in traits)
{
if (t.id == trait)
return true;
}
return false;
}
public bool HasTrait(TraitData trait)
{
if(trait != null)
return HasTrait(trait.id);
return false;
}
public bool HasStat(string trait)
{
if (stats == null)
return false;
foreach (TraitStat stat in stats)
{
if (stat.trait.id == trait)
return true;
}
return false;
}
public bool HasStat(TraitData trait)
{
if(trait != null)
return HasStat(trait.id);
return false;
}
public int GetStat(string trait_id)
{
if (stats == null)
return 0;
foreach (TraitStat stat in stats)
{
if (stat.trait.id == trait_id)
return stat.value;
}
return 0;
}
public int GetStat(TraitData trait)
{
if(trait != null)
return GetStat(trait.id);
return 0;
}
public bool HasAbility(AbilityData tability)
{
foreach (AbilityData ability in abilities)
{
if (ability && ability.id == tability.id)
return true;
}
return false;
}
public bool HasAbility(AbilityTrigger trigger)
{
foreach (AbilityData ability in abilities)
{
if (ability && ability.trigger == trigger)
return true;
}
return false;
}
public bool HasAbility(AbilityTrigger trigger, AbilityTarget target)
{
foreach (AbilityData ability in abilities)
{
if (ability && ability.trigger == trigger && ability.target == target)
return true;
}
return false;
}
public AbilityData GetAbility(AbilityTrigger trigger)
{
foreach (AbilityData ability in abilities)
{
if (ability && ability.trigger == trigger)
return ability;
}
return null;
}
public bool HasPack(PackData pack)
{
foreach (PackData apack in packs)
{
if (apack == pack)
return true;
}
return false;
}
public static CardData Get(string id)
{
if (id == null)
return null;
bool success = card_dict.TryGetValue(id, out CardData card);
if (success)
return card;
return null;
}
public static List<CardData> GetAllDeckbuilding()
{
List<CardData> multi_list = new List<CardData>();
foreach (CardData acard in GetAll())
{
if (acard.deckbuilding)
multi_list.Add(acard);
}
return multi_list;
}
public static List<CardData> GetAll(PackData pack)
{
List<CardData> multi_list = new List<CardData>();
foreach (CardData acard in GetAll())
{
if (acard.HasPack(pack))
multi_list.Add(acard);
}
return multi_list;
}
public static List<CardData> GetAll()
{
return card_list;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 843a21f7f6f205741a0a7341aec8e84d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,49 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Defines all cardback data
/// </summary>
[CreateAssetMenu(fileName = "Cardback", menuName = "TcgEngine/Cardback", order = 10)]
public class CardbackData : ScriptableObject
{
public string id;
public Sprite cardback;
public Sprite deck;
public int sort_order;
public static List<CardbackData> cardback_list = new List<CardbackData>();
public static void Load(string folder = "")
{
if (cardback_list.Count == 0)
cardback_list.AddRange(Resources.LoadAll<CardbackData>(folder));
cardback_list.Sort((CardbackData a, CardbackData b) => {
if (a.sort_order == b.sort_order)
return a.id.CompareTo(b.id);
else
return a.sort_order.CompareTo(b.sort_order);
});
}
public static CardbackData Get(string id)
{
foreach (CardbackData cardback in GetAll())
{
if (cardback.id == id)
return cardback;
}
return null;
}
public static List<CardbackData> GetAll()
{
return cardback_list;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b848b003e799d264cba32a383c514e74
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,90 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Base class for all ability conditions, override the IsConditionMet function
/// </summary>
public class ConditionData : ScriptableObject
{
public virtual bool IsTriggerConditionMet(Game data, AbilityData ability, Card caster)
{
return true; //Override this, applies to any target, always checked
}
public virtual bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Card target)
{
return true; //Override this, condition targeting card
}
public virtual bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Player target)
{
return true; //Override this, condition targeting player
}
public virtual bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, Slot target)
{
return true; //Override this, condition targeting slot
}
public virtual bool IsTargetConditionMet(Game data, AbilityData ability, Card caster, CardData target)
{
return true; //Override this, for effects that create new cards
}
public bool CompareBool(bool condition, ConditionOperatorBool oper)
{
if (oper == ConditionOperatorBool.IsFalse)
return !condition;
return condition;
}
public bool CompareInt(int ival1, ConditionOperatorInt oper, int ival2)
{
if (oper == ConditionOperatorInt.Equal)
{
return ival1 == ival2;
}
if (oper == ConditionOperatorInt.NotEqual)
{
return ival1 != ival2;
}
if (oper == ConditionOperatorInt.GreaterEqual)
{
return ival1 >= ival2;
}
if (oper == ConditionOperatorInt.LessEqual)
{
return ival1 <= ival2;
}
if (oper == ConditionOperatorInt.Greater)
{
return ival1 > ival2;
}
if (oper == ConditionOperatorInt.Less)
{
return ival1 < ival2; ;
}
return false;
}
}
public enum ConditionOperatorInt
{
Equal,
NotEqual,
GreaterEqual,
LessEqual,
Greater,
Less,
}
public enum ConditionOperatorBool
{
IsTrue,
IsFalse,
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 110953428c5a7d942bb601a0ac3acf65
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,166 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TcgEngine.Client;
namespace TcgEngine
{
/// <summary>
/// This script initiates loading all the game data
/// </summary>
public class DataLoader : MonoBehaviour
{
public GameplayData data;
public AssetData assets;
private HashSet<string> card_ids = new HashSet<string>();
private HashSet<string> ability_ids = new HashSet<string>();
private HashSet<string> deck_ids = new HashSet<string>();
private static DataLoader instance;
void Awake()
{
instance = this;
LoadData();
}
public void LoadData()
{
//To make loading faster, add a path inside each Load() function, relative to Resources folder
//For example CardData.Load("Cards"); to only load data inside the Resources/Cards folder
CardData.Load();
TeamData.Load();
RarityData.Load();
TraitData.Load();
VariantData.Load();
PackData.Load();
LevelData.Load();
DeckData.Load();
AbilityData.Load();
StatusData.Load();
AvatarData.Load();
CardbackData.Load();
RewardData.Load();
CheckCardData();
CheckAbilityData();
CheckDeckData();
CheckVariantData();
}
//Make sure the data is valid
private void CheckCardData()
{
card_ids.Clear();
foreach (CardData card in CardData.GetAll())
{
if (string.IsNullOrEmpty(card.id))
Debug.LogError(card.name + " id is empty");
if (card_ids.Contains(card.id))
Debug.LogError("Dupplicate Card ID: " + card.id);
if (card.team == null)
Debug.LogError(card.id + " team is null");
if (card.rarity == null)
Debug.LogError(card.id + " rarity is null");
foreach (TraitData trait in card.traits)
{
if (trait == null)
Debug.LogError(card.id + " has null trait");
}
if (card.stats != null)
{
foreach (TraitStat stat in card.stats)
{
if (stat.trait == null)
Debug.LogError(card.id + " has null stat trait");
}
}
foreach (AbilityData ability in card.abilities)
{
if(ability == null)
Debug.LogError(card.id + " has null ability");
}
card_ids.Add(card.id);
}
}
//Make sure the data is valid
private void CheckAbilityData()
{
ability_ids.Clear();
foreach (AbilityData ability in AbilityData.GetAll())
{
if (string.IsNullOrEmpty(ability.id))
Debug.LogError(ability.name + " id is empty");
if (ability_ids.Contains(ability.id))
Debug.LogError("Dupplicate Ability ID: " + ability.id);
foreach (AbilityData chain in ability.chain_abilities)
{
if (chain == null)
Debug.LogError(ability.id + " has null chain ability");
}
ability_ids.Add(ability.id);
}
}
//Make sure the data is valid
private void CheckDeckData()
{
GameplayData gdata = GameplayData.Get();
CheckDeckArray(gdata.ai_decks);
CheckDeckArray(gdata.free_decks);
CheckDeckArray(gdata.starter_decks);
if(gdata.test_deck == null || gdata.test_deck_ai == null)
Debug.Log("Deck is null in Resources/GameplayData");
deck_ids.Clear();
foreach (DeckData deck in DeckData.GetAll())
{
if (string.IsNullOrEmpty(deck.id))
Debug.LogError(deck.name + " id is empty");
if (deck_ids.Contains(deck.id))
Debug.LogError("Dupplicate Deck ID: " + deck.id);
foreach (CardData card in deck.cards)
{
if (card == null)
Debug.LogError(deck.id + " has null card");
}
deck_ids.Add(deck.id);
}
}
private void CheckDeckArray(DeckData[] decks)
{
foreach (DeckData deck in decks)
{
if (deck == null)
Debug.Log("Deck is null in Resources/GameplayData");
}
}
private void CheckVariantData()
{
VariantData dvariant = VariantData.GetDefault();
if(dvariant == null)
Debug.LogError("No default variant data found, make sure you have a default VariantData");
}
public static DataLoader Get()
{
return instance;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6b8b650ea59aaba4a81d2630d662eabc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,56 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Defines all fixed deck data (for user custom decks, check UserData.cs)
/// </summary>
[CreateAssetMenu(fileName = "DeckData", menuName = "TcgEngine/DeckData", order = 7)]
public class DeckData : ScriptableObject
{
public string id;
[Header("Display")]
public string title;
[Header("Cards")]
public CardData hero;
public CardData[] cards;
public static List<DeckData> deck_list = new List<DeckData>();
public static void Load(string folder = "")
{
if(deck_list.Count == 0)
deck_list.AddRange(Resources.LoadAll<DeckData>(folder));
}
public int GetQuantity()
{
return cards.Length;
}
public bool IsValid()
{
return cards.Length >= GameplayData.Get().deck_size;
}
public static DeckData Get(string id)
{
foreach (DeckData deck in GetAll())
{
if (deck.id == id)
return deck;
}
return null;
}
public static List<DeckData> GetAll()
{
return deck_list;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 87ecef176d27eb040a9770e0eefea2ec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,37 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
/// <summary>
/// Deck with more fields for having specific cards or starting board cards
/// </summary>
[System.Serializable]
public class DeckCardSlot
{
public CardData card;
public SlotXY slot;
}
[CreateAssetMenu(fileName = "DeckPuzzleData", menuName = "TcgEngine/DeckPuzzleData", order = 7)]
public class DeckPuzzleData : DeckData
{
public DeckCardSlot[] board_cards;
public int start_cards = 5;
public int start_mana = 2;
public int start_hp = 20;
public bool dont_shuffle_deck;
public static new DeckPuzzleData Get(string id)
{
foreach (DeckData deck in GetAll())
{
if (deck.id == id && deck is DeckPuzzleData)
return (DeckPuzzleData) deck;
}
return null;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 069f5be85a1dcec46b083fc9c26e6870
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,65 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TcgEngine.Gameplay;
namespace TcgEngine
{
/// <summary>
/// Base class for all ability effects, override the IsConditionMet function
/// </summary>
public class EffectData : ScriptableObject
{
public virtual void DoEffect(GameLogic logic, AbilityData ability, Card caster)
{
//Server side gameplay logic
}
public virtual void DoEffect(GameLogic logic, AbilityData ability, Card caster, Card target)
{
//Server side gameplay logic
}
public virtual void DoEffect(GameLogic logic, AbilityData ability, Card caster, Player target)
{
//Server side gameplay logic
}
public virtual void DoEffect(GameLogic logic, AbilityData ability, Card caster, Slot target)
{
//Server side gameplay logic
}
public virtual void DoEffect(GameLogic logic, AbilityData ability, Card caster, CardData target)
{
//Server side gameplay logic
}
public virtual void DoOngoingEffect(GameLogic logic, AbilityData ability, Card caster, Card target)
{
//Ongoing effect only
}
public virtual void DoOngoingEffect(GameLogic logic, AbilityData ability, Card caster, Player target)
{
//Ongoing effect only
}
public int AddOrSet(int original_val, EffectOperatorInt oper, int add_value)
{
if (oper == EffectOperatorInt.Add)
return original_val + add_value;
if (oper == EffectOperatorInt.Set)
return add_value;
return original_val;
}
}
public enum EffectOperatorInt
{
Add,
Set,
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d9b30fc8e458fec41a859f97cd4db714
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

Some files were not shown because too many files have changed in this diff Show More