Skip to content

Instantly share code, notes, and snippets.

@LuizMoratelli
Last active October 11, 2024 13:43
Show Gist options
  • Save LuizMoratelli/3a442d74d6e764f7fa6c4e705bc12595 to your computer and use it in GitHub Desktop.
Save LuizMoratelli/3a442d74d6e764f7fa6c4e705bc12595 to your computer and use it in GitHub Desktop.
[TCG Engine] Response Turn, interaction in opponent's turns.

First of all, this is a poc (proof of concept) be prepared to adapt, fix or encounter bugs for your specific game, I would be happy trying to help you.

Response Idea

The idea here is to allow some interaction between players in same turn. You can expand, adapt and change to fit your game, adding response on stack, or attack as response, is up to you.

What could be interacted following the steps below:

  • Selectors (Card, Target and Choice);
  • Play Cards with specific trait (similar to flash);
  • Activate Abilities;
  • Required (not cancellable) selectors;

What could NOT be interacted following the steps below:

  • Attack;
  • Block;
  • Move;
  • Response on Stack;
  • ...

Use Scenarios

  1. You want your opponent to choose a card to discard instead of random or active player choosing
  2. Enable opponent to spend mana and play some type of cards in a specific moment, like right before pass the turn.

Code Changes

Game.cs

public ResponsePhase response_phase = ResponsePhase.None;
public float response_timer = 0f;
public bool selector_cancelable;

public virtual bool IsPlayerActionTurn(Player player)
{
    return player != null
        && state == GameState.Play && selector == SelectorType.None
        && (response_phase == ResponsePhase.None) == (current_player == player.player_id); // Last line ensure that active players don't play on Response, but not-active can.
}

public static void Clone(Game source, Game dest)
{
    //...
    dest.response_phase = source.response_phase;
    //...
}

// Arguments are hidden just make it simple, don't remove yours
public virtual bool CanPlayCard() {
    // Add after the CanPayMana check
    if (!skip_cost && response_phase == ResponsePhase.Response && !card.HasTrait("response"))
    return false; //Cant response with it
    //...
}

// Arguments are hidden just make it simple, don't remove yours
public virtual bool CanMoveCard()
{
    // Add after the IsValid check
    if (response_phase == ResponsePhase.Response)
        return false;
    //...
}

// Arguments are hidden just make it simple, don't remove yours
// Remember that there are 1 method for attacking creatures and another for attacking players, you probably want to update both
public virtual bool CanAttackTarget()
{
    // After CanAttack check
    if (response_phase == ResponsePhase.Response)
        return false;
    //...
}

[System.Serializable]
public enum ResponsePhase
{
    None = 0,
    Response = 10,
    ResponseSelector = 20,
}

GameLogic.cs

public virtual void EndTurn() 
{
    if (game_data.state == GameState.GameEnded)
        return;
        
    // This will always move to Response after active player End a Turn. You can do something similar to other moments if you want, like after playing a card, before start Main phase, etc
    if (game_data.response_phase == ResponsePhase.None)
    {
        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
        RefreshData();
        return;
    }
    
    game_data.response_phase = ResponsePhase.None;
    //...
}

// Repeat for other selectors, like: SelectPlayer, SelectChoice, 
public virtual void SelectCard(Card target)
{
    //...
    //after game_data.selector = SelectorType.None;
    if (game_data.response_phase == ResponsePhase.ResponseSelector)
    {
        game_data.response_phase = ResponsePhase.None;
    }
    
    //...
}

// 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;
    }
}

// Add this to all GoToSelect..., like
protected virtual void GoToSelectTarget(AbilityData iability, Card caster)
{
    // Before RefreshData();
    game_data.selector_cancelable = iability.selector_cancelable;
    CheckResponseSelector(iability, caster);
}

GameServer.cs

public virtual void Update()
{
    if (game_data.phase == GamePhase.Main && game.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();
    }
}

AbilityData.cs

We'll add 2 new variables, 1 to set who choose from selector, and another to say if Ability could be canceld (X button)

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?

AILogic.cs

Replace the following line with the new one (you should find ~3 occurences)

//old Player player = data.GetPlayer(data.current_player);
//new
data.response_phase == ResponsePhase.Response ? data.GetOpponentPlayer(data.current_player) : data.GetPlayer(data.current_player);

HandCard.cs

Disable cards from being played on response, except the ones with a specific trait response as example.

//On TryPlayCard, after the CanPayMana check
if (gdata.response_phase == ResponsePhase.Response && !card.HasTrait("response"))
{
    WarningText.ShowNoResponse();
    return;
}

GameUI.cs

public Text end_turn_text;


void Update()
{
    //...
    //Timer
    var timer_val = data.response_phase != ResponsePhase.None ? Mathf.RoundToInt(data.response_timer) : Mathf.RoundToInt(data.turn_timer);
    if (yourturn)
    {
        end_turn_text.text = data.response_phase == ResponsePhase.Response ? "END\nRESPONSE" : "END\nTURN";
    }
    else
    {
        end_turn_text.text = data.response_phase == ResponsePhase.Response ? "ENEMY\nRESPONSE" : "ENEMY\nTURN";
    }
    //...
    //old turn_timer.text = Mathf.RoundToInt(data.turn_timer).ToString();
    //new
    turn_timer.text = timer_val.ToString();
    
    //Simulate timer
    if (data.state == GameState.Play && data.phase == GamePhase.Main && data.response_phase == ResponsePhase.None && data.turn_timer > 0f)
        data.turn_timer -= Time.deltaTime;
    if (data.state == GameState.Play && data.response_phase != ResponsePhase.None && data.response_timer > 0f)
        data.response_timer -= Time.deltaTime;
        
     //Timer warning
        if (data.state == GameState.Play)
        {
            int tick_val = 10;
            if (timer_val < prev_time_val && timer_val <= tick_val)
                PulseFX();
            prev_time_val = timer_val;
        }
}

HeroUI.cs

//After CanPayAbility check
if (gdata.response_phase == ResponsePhase.Response && !ability.use_as_response)
{
    WarningText.ShowNoResponse();
    return;
}

WarningText.cs

public static void ShowNoResponse()
{
    ShowText("This cannot be used as response");
}

CardSelector.cs, ChoiceSelector.cs and SelectTargetUI.cs

public GameObject cancel_button; // Remember to attach it via inspector, is the X GameObject

// On Show/ShowMsg methods, add this to hide the X button when is not cancelable
cancel_button.SetActive(GameClient.Get().GetGameData().selector_cancelable);

Going Forward

You can keep expanding the idea, I thought about 2 main ways to do that

  1. Calling response in more Phases on Game (Like Before change to Main, adding a combat phase, etc), you could also move to Response after EACH action to enable responses, but that could be hard to handle and boring to play, but is also a design problem to solve in your game.

  2. As an example of Response Selector I used the card selector but you would need to adapt for each selector you plan to use, specially because of, when times runs out, some abilities should resolve with a random/specific value auto selected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment