Skip to content

Instantly share code, notes, and snippets.

@LuizMoratelli
Last active October 15, 2024 10:47
Show Gist options
  • Save LuizMoratelli/b4f1cf30f5bd9a6479c7cb51834bbb08 to your computer and use it in GitHub Desktop.
Save LuizMoratelli/b4f1cf30f5bd9a6479c7cb51834bbb08 to your computer and use it in GitHub Desktop.
[TCGEngine] Stack Response

This is build on top of the Response. Check if first to get the basics.

So, to start of, I DONT recommend you to just add this because you like other games with stack, yeah, is cool to counter some cards but it slow a lot the game flow and if you don't know much about how the engine works, it will become a lot harder to debug behaviors.

Tips:

  • //replace(xNumber) means you must replace X times that line appears on that file (generally are all of them);
  • //Method ... //End means you should put it inside a specific method on that file, if not specified, don't matter too much where;
  • //... Means there is more code between the before and after part that you SHOULDNT remove;
  • //remove Means you SHOULD remove that code.

Response Phase

Going Forward

You could also keep it as a list and just add methods to pop elements from it, what could be nice if you want to be able to counter

Lands skip the stack

image

Going Forward

You could make it so after each card resolve you could play another and another...

Creating Stack UI

Scene

I duplicated the HistoryBar and changed a bit, removing the main script and adding our StackUI, and adjusting the visuals, feel free to do whatever you prefer as UI. The "Base" is just a History Line resized and renamed. image

Creating Counter Effect

Creating Counter Card Spell

Effect SO

image

Condition SO

image

Ability SO

image

Card SO

image

Result (Counter happening Thrice)

TCG.Engine.Git.-.Game.-.Windows.Mac.Linux.-.Unity.2022.3.34f1._DX11_.2024-09-12.08-28-28.online-video-cutter.com.mp4
public bool selector_owner = true; //WHO choses? AI or Player?
public bool selector_cancelable = true; //CAN be cancelled? Click on "X" to close window / timeout
public bool use_as_response = false; //CAN be used in response phase?
//replace(2x) Player player = data.GetPlayer(data.current_player); ->
Player player = data.response_phase == ResponsePhase.Response ? data.GetPlayer(data.response_player) : data.GetPlayer(data.current_player);
//replace(1x) Player player = data.GetPlayer(player_id); ->
Player player = data.response_phase == ResponsePhase.Response ? data.GetPlayer(data.response_player) : data.GetPlayer(data.current_player);
public bool skip_stack;
//path Assets/TcgEngine/Scripts/Conditions/ConditionCardStack.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace TcgEngine
{
[CreateAssetMenu(fileName = "condition", menuName = "TcgEngine/Condition/CardStack", order = 10)]
public class ConditionCardStack : ConditionData
{
[Header("Card is in stack")]
public ConditionOperatorBool oper;
public override bool IsTriggerConditionMet(Game data, AbilityData ability, Card caster)
{
return data.history_list.Count > 0;
}
}
}
//path Assets/TcgEngine/Scripts/Effects/EffectCounter.cs
using UnityEngine;
using TcgEngine.Gameplay;
namespace TcgEngine
{
[CreateAssetMenu(fileName = "effect", menuName = "TcgEngine/Effect/Counter", order = 10)]
public class EffectCounter : EffectData
{
public override void DoEffect(GameLogic logic, AbilityData ability, Card caster)
{
if (logic.ResolveQueue.GetCardQueue().Count > 0)
{
var countered = logic.ResolveQueue.GetCardQueue().Pop();
var player = logic.GameData.GetPlayer(countered.caster.player_id);
player.AddCard(player.cards_discard, logic.GameData.GetCard(countered.caster.uid));
logic.GetGameData().history_list.Remove(logic.GetGameData().history_list.Find(item => item.card_uid == countered.caster.uid));
}
}
}
}
public float response_timer = 0f;
public ResponsePhase response_phase = ResponsePhase.None;
public int response_player = 0;
public bool selector_cancelable;
public List<ActionHistory> history_list = new List<ActionHistory>();
public virtual bool IsPlayerTurn(Player player)
{
//replace(1x) return IsPlayerActionTurn(player) || IsPlayerSelectorTurn(player); ->
return IsPlayerActionTurn(player) || IsPlayerSelectorTurn(player) || IsResponsePlayerTurn(player);
}
public virtual bool IsPlayerActionTurn(Player player)
{
return player != null && current_player == player.player_id
&& state == GameState.Play && selector == SelectorType.None
&& response_phase == ResponsePhase.None;
}
public virtual bool IsResponsePlayerTurn(Player player)
{
return player != null && response_phase != ResponsePhase.None && response_player == player.player_id;
}
//CanPlayCard
if (!skip_cost && response_phase == ResponsePhase.Response && !card.HasTrait("response"))
return false; //Cant response with it
//End
//CanMoveCard
if (response_phase == ResponsePhase.Response)
return false;
//End
//CanAttackTarget
if (response_phase == ResponsePhase.Response)
return false;
//End
//Clone
dest.response_phase = source.response_phase;
dest.response_player = source.response_player;
dest.response_timer = source.response_timer;
for (int i = 0; i < source.history_list.Count; i++)
{
if (i < dest.history_list.Count)
ActionHistory.Clone(source.history_list[i], dest.history_list[i]);
else
dest.history_list.Add(ActionHistory.CloneNew(source.history_list[i]));
}
if (dest.history_list.Count > source.history_list.Count)
dest.history_list.RemoveRange(source.history_list.Count, dest.history_list.Count - source.history_list.Count);
//End
[System.Serializable]
public enum ResponsePhase
{
None = 0,
Response = 10,
ResponseSelector = 20,
}
//EndTurn
//remove if (game_data.phase != GamePhase.Main)
//remove return;
if (game_data.response_phase == ResponsePhase.Response)
{
game_data.GetPlayer(game_data.response_player).resolve = true;
if (!game_data.GetOpponentPlayer(game_data.response_player).resolve)
{
game_data.response_player = game_data.GetOpponentPlayer(game_data.response_player).player_id;
RefreshData();
return;
}
else
{
game_data.response_phase = ResponsePhase.None;
game_data.response_player = game_data.GetOpponentPlayer(game_data.response_player).player_id;
resolve_queue.ResolveAll(true);
RefreshData();
return;
}
}
//End
public virtual void PlayCard(Card card, Slot slot, bool skip_cost = false)
{
if (game_data.CanPlayCard(card, slot, skip_cost))
{
Player player = game_data.GetPlayer(card.player_id);
//Cost
if (!skip_cost)
player.PayMana(card);
//Play card
player.RemoveCardFromAllGroups(card);
if (card.CardData.skip_stack || skip_cost)
{
TriggerCard(card, player, slot);
}
else
{
resolve_queue.AddCard(card, player, slot, TriggerCard);
ActionHistory order = new ActionHistory();
order.type = GameAction.PlayCard;
order.card_id = card.card_id;
order.card_uid = card.uid;
game_data.history_list.Add(order);
Player responseOp = game_data.GetOpponentPlayer(game_data.response_phase == ResponsePhase.None ? player.player_id : game_data.response_player);
Player resposePl = game_data.GetOpponentPlayer(responseOp.player_id);
if (game_data.response_phase == ResponsePhase.None || (!resposePl.resolve || !responseOp.resolve))
{
game_data.response_phase = ResponsePhase.Response;
game_data.response_timer = GameplayData.Get().turn_duration; // you can a different amout for response timer, just add on GameplayData and setup as you want
resposePl.resolve = true;
responseOp.resolve = false;
game_data.response_player = responseOp.player_id;
RefreshData();
return;
}
game_data.response_phase = ResponsePhase.None;
}
RefreshData();
onCardPlayed?.Invoke(card, slot);
resolve_queue.ResolveAll(0.3f);
}
}
public virtual void TriggerCard(Card card, Player player, Slot slot)
{
//Add to board
CardData icard = card.CardData;
if (icard.IsBoardCard() || icard.IsLandCard())
{
player.cards_board.Add(card);
card.slot = slot;
card.exhausted = true; //Cant attack first turn
}
else if (icard.IsEquipment())
{
Card bearer = game_data.GetSlotCard(slot);
EquipCard(bearer, card);
card.exhausted = true;
}
else if (icard.IsSecret())
{
player.cards_secret.Add(card);
}
else
{
player.cards_discard.Add(card);
card.slot = slot; //Save slot in case spell has PlayTarget
}
var history_card = game_data.history_list.Find(item => item.card_uid == card.uid);
game_data.history_list.Remove(history_card);
//History
if (!is_ai_predict && !icard.IsSecret())
player.AddHistory(GameAction.PlayCard, card);
//Update ongoing effects
game_data.last_played = card.uid;
UpdateOngoing();
//Trigger abilities
if (card.CardData.IsDynamicManaCost())
{
GoToSelectorCost(card);
}
else
{
TriggerSecrets(AbilityTrigger.OnPlayOther, card); //After playing card
TriggerCardAbilityType(AbilityTrigger.OnPlay, card);
TriggerOtherCardsAbilityType(AbilityTrigger.OnPlayOther, card);
}
}
// This method will solve the selectors that aren't cancelable, choosing the first valid option, you could remove this logic, adapt, pick random, is in your hands to choose.
public virtual void CancelSelection()
{
if (game_data.selector != SelectorType.None)
{
if (!game_data.selector_cancelable)
{
if (game_data.selector == SelectorType.SelectorCard)
{
var iability = AbilityData.Get(game_data.selector_ability_id);
var icard = game_data.GetCard(game_data.selector_caster_uid);
var options = iability.GetCardTargets(game_data, icard);
if (options.Count > 0 && options?.First() != null)
{
Card target = game_data.GetCard(options.First().uid);
SelectCard(target);
}
}
else if (game_data.selector == SelectorType.SelectorChoice)
{
var icard = game_data.GetCard(game_data.selector_caster_uid);
var iability = AbilityData.Get(game_data.selector_ability_id);
if (iability != null && iability.chain_abilities.Length > 0)
{
int i = 0;
foreach (var chain_ability in iability.chain_abilities)
{
if (game_data.CanSelectAbility(icard, chain_ability))
{
SelectChoice(i);
break;
}
i++;
}
}
}
else if (game_data.selector == SelectorType.SelectTarget)
{
var icard = game_data.GetCard(game_data.selector_caster_uid);
AbilityData iability = AbilityData.Get(game_data.selector_ability_id);
var found = false;
foreach (var player in game_data.players)
{
for (var i = 0; i < player.cards_board.Count; i++)
{
if (found) break;
var target = player.cards_board[i];
if (iability.CanTarget(game_data, icard, target))
{
SelectCard(target);
found = true;
}
}
if (found) break;
}
}
}
//End selection
game_data.selector = SelectorType.None;
RefreshData();
}
}
// This method triggers a ResponseSelector, unlike Response, we will finish the response of this kind after an action is made
protected virtual void CheckResponseSelector(AbilityData iability, Card caster)
{
game_data.selector_player_id = iability.selector_owner ? caster.player_id : game_data.GetOpponentPlayer(caster.player_id).player_id;
if (iability.selector_owner != (game_data.GetActivePlayer().player_id == caster.player_id) && game_data.response_phase == ResponsePhase.None)
{
game_data.response_timer = GameplayData.Get().turn_duration;
game_data.response_phase = ResponsePhase.ResponseSelector;
}
}
//Timer during game
if (game_data.state == GameState.Play && !gameplay.IsResolving())
{
game_data.turn_timer -= Time.deltaTime;
if (game_data.phase == GamePhase.Main && game_data.response_phase == ResponsePhase.None)
{
game_data.turn_timer -= Time.deltaTime;
}
else if (game_data.response_phase != ResponsePhase.None)
{
game_data.response_timer -= Time.deltaTime;
}
if (game_data.turn_timer <= 0f)
{
//Time expired during turn
gameplay.NextStep();
}
if (game_data.response_timer <= 0f)
{
// Time expired to response action
gameplay.CancelSelection();
}
}
//replace(2x) if (player != null && msg != null && game_data.IsPlayerActionTurn(player) && !gameplay.IsResolving()) ->
if (player != null && msg != null && (game_data.IsPlayerActionTurn(player) || game_data.IsResponsePlayerTurn(player)) && !gameplay.IsResolving())
public bool resolve = true;
public class ActionHistory
{
//...
public static void Clone(ActionHistory dest, ActionHistory source)
{
source.type = dest.type;
source.card_id = dest.card_id;
source.card_uid = dest.card_uid;
source.target_uid = dest.target_uid;
source.ability_id = dest.ability_id;
source.target_id = dest.target_id;
source.slot = dest.slot;
}
public static ActionHistory CloneNew(ActionHistory dest)
{
var source = new ActionHistory();
source.type = dest.type;
source.card_id = dest.card_id;
source.card_uid = dest.card_uid;
source.target_uid = dest.target_uid;
source.ability_id = dest.ability_id;
source.target_id = dest.target_id;
source.slot = dest.slot;
return source;
}
}
//replace(1x) else if (gdata.IsPlayerActionTurn(player) && card.player_id == player.player_id) ->
else if ((gdata.IsPlayerActionTurn(player) || gdata.IsResponsePlayerTurn(player)) && card.player_id == player.player_id)
private Pool<CardQueueElement> card_elem_pool = new Pool<CardQueueElement>();
/*Removed
private Queue<AbilityQueueElement> ability_queue = new Queue<AbilityQueueElement>();
private Queue<SecretQueueElement> secret_queue = new Queue<SecretQueueElement>();
private Queue<AttackQueueElement> attack_queue = new Queue<AttackQueueElement>();
private Queue<CallbackQueueElement> callback_queue = new Queue<CallbackQueueElement>();
*/End
private Stack<AbilityQueueElement> ability_queue = new Stack<AbilityQueueElement>();
private Stack<SecretQueueElement> secret_queue = new Stack<SecretQueueElement>();
private Stack<AttackQueueElement> attack_queue = new Stack<AttackQueueElement>();
private Stack<CallbackQueueElement> callback_queue = new Stack<CallbackQueueElement>();
private Stack<CardQueueElement> card_elem_queue = new Stack<CardQueueElement>();
private bool stack = false;
//Update
this.stack = game_data.response_phase != ResponsePhase.Response;
//End
public virtual void AddCard(Card caster, Player owner, Slot slot, Action<Card, Player, Slot> callback)
{
if (caster != null && owner != null)
{
CardQueueElement elem = card_elem_pool.Create();
elem.caster = caster;
elem.owner = owner;
elem.slot = slot;
elem.callback = callback;
card_elem_queue.Push(elem);
}
}
//replace(x5) .Enqueue ->
.Push
//replace(x1) public virtual void Resolve() ->
public virtual void Resolve(bool stack = false)
//replace(x4) .Dequeue ->
.Pop
//...
}
else if(stack && card_elem_queue.Count > 0)
{
//Resolve Card
CardQueueElement elem = card_elem_queue.Pop();
card_elem_pool.Dispose(elem);
elem.callback?.Invoke(elem.caster, elem.owner, elem.slot);
}
else if (callback_queue.Count > 0)
{
//...
//CanResolve
// replace(x1) return attack_queue.Count > 0 || ability_queue.Count > 0 || secret_queue.Count > 0 || callback_queue.Count > 0; ->
return (stack && card_elem_queue.Count > 0) || attack_queue.Count > 0 || ability_queue.Count > 0 || secret_queue.Count > 0 || callback_queue.Count > 0;
//End
//Clear()
card_elem_pool.DisposeAll();
card_elem_queue.Clear();
//End
public Stack<CardQueueElement> GetCardQueue()
{
return card_elem_queue;
}
//replace(x4) Queue< ->
Stack<
//replace(x1) public virtual void ResolveAll() ->
public virtual void ResolveAll(bool force_stack = false)
//replace(x1) while (CanResolve(stack)) ->
while (CanResolve(force_stack || stack))
//replace(x1) Resolve(stack); ->
Resolve(force_stack || stack);
public class CardQueueElement
{
public Card caster;
public Slot slot;
public Player owner;
public Action<Card, Player, Slot> callback;
}
//path Assets/TcgEngine/Scripts/UI/StackUI.cs
using System.Collections;
using System.Collections.Generic;
using TcgEngine.Client;
using UnityEngine;
using UnityEngine.UI;
namespace TcgEngine.UI
{
/// <summary>
/// History bar shows all the previous moved perform by a player this turn
/// </summary>
public class StackUI : MonoBehaviour
{
public List<TurnHistoryLine> history_lines;
public GameObject template;
public Text header;
void Start()
{
template.SetActive(false);
header.gameObject.SetActive(false);
}
void Update()
{
if (!GameClient.Get().IsReady())
return;
Game data = GameClient.Get().GetGameData();
if (data.history_list != null)
{
header.gameObject.SetActive(data.history_list.Count > 0);
int index = 0;
foreach (ActionHistory order in data.history_list)
{
if (index < history_lines.Count)
{
history_lines[index].SetLine(order);
history_lines[index].gameObject.SetActive(true);
index++;
}
else
{
var line = Instantiate(template, transform).GetComponent<TurnHistoryLine>();
line.gameObject.SetActive(true);
line.SetLine(order);
history_lines.Add(line);
}
}
while (index < history_lines.Count)
{
history_lines[index].gameObject.SetActive(false);
index++;
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment