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.
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;
- ...
- You want your opponent to choose a card to discard instead of random or active player choosing
- Enable opponent to spend mana and play some type of cards in a specific moment, like right before pass the turn.
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,
}
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);
}
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();
}
}
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?
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);
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;
}
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;
}
}
//After CanPayAbility check
if (gdata.response_phase == ResponsePhase.Response && !ability.use_as_response)
{
WarningText.ShowNoResponse();
return;
}
public static void ShowNoResponse()
{
ShowText("This cannot be used as response");
}
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);
You can keep expanding the idea, I thought about 2 main ways to do that
-
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.
-
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.