Need help with waiting for input

Godot Version

4.3

Question

I’m currently developing a card game in Godot where each card has unique rules for how it can be played. Specifically, some cards can only be placed on empty tiles, some must be placed on empty spaces adjacent to existing tiles, and others require the player to select multiple targets or positions sequentially before the card effect is resolved. For example, one card may require selecting an entity and then selecting an adjacent space to push that entity.

Current Implementation

I’ve created a CardConstraintResource class that extends Resource to define how each card can be played:

class_name CardConstraintResource extends Resource

enum constraint_type {ENTITY, EMPTY_TILE, SPACE_ADJACENT_TO_TILE}
@export var constraint_sequence: Array[constraint_type]

Each card references a CardConstraintResource that dictates the valid play sequence. For example, a card that plants something can only be played on EMPTY_TILE, so its constraint_sequence would reflect this.

I’ve also started implementing a CardPlayer script to manage the process of playing a card, but I’m facing difficulties in completing the logic where the game should wait for player input and proceed to the next step in the constraint_sequence after each selection.

CardPlayer.gd

extends Node2D

func collect_play_data(card: Card):
    var card_resource: CardResource = card.card_resource
    var constraint_sequence = card.card_resource.constraint_resource.constraint_sequence
    if constraint_sequence == null:
        return
    
    var valid_moves = get_valid_positions(constraint_sequence[0])
    highlight_available_moves(card, 0)
    var selected_move = await wait_for_move_selection(valid_moves)

func play(card: Card):
    pass

func highlight_available_moves(card: Card, index: int):
    var positions = get_valid_positions(card.card_resource.constraint_resource.constraint_sequence[index])
    for position in positions:
        var valid_move_square = ColorRect.new()
        valid_move_square.color = Color.GREEN
        valid_move_square.color.a = 0.5
        valid_move_square.position = TileManager.tilemap.map_to_local(position)
        valid_move_square.size = Vector2(16, 16)
        valid_move_square.z_index = 100
        valid_move_square.position += Vector2(-8, -8)
        add_child(valid_move_square)

func get_valid_positions(constraint_type: CardConstraintResource.constraint_type):
    var valid_positions: Array[Vector2i] = []
    match constraint_type:
        CardConstraintResource.constraint_type.ENTITY:
            for entity in get_tree().get_nodes_in_group("Entity"):
                if entity is Entity:
                    valid_positions.append(TileManager.tilemap.local_to_map(entity.position))
                else:
                    print_debug("Trying to get entity position on the node that is not of type Entity!")
        
        CardConstraintResource.constraint_type.EMPTY_TILE:
            var tile_positions = TileManager.tilemap.get_used_cells()
            for tile_position in tile_positions:
                var tile: Tile = TileManager.get_tile_at_position(tile_position)
                if tile.entity_on_top == null:
                    valid_positions.append(tile_position)
        
        CardConstraintResource.constraint_type.SPACE_ADJACENT_TO_TILE:
            var tile_positions = TileManager.tilemap.get_used_cells()
            var offset_vectors = [Vector2i.UP, Vector2i.DOWN, Vector2i.LEFT, Vector2i.RIGHT]
            for tile_position in tile_positions:
                for offset_vector in offset_vectors:
                    if TileManager.get_tile_at_position(tile_position + offset_vector) == null:
                        valid_positions.append(tile_position + offset_vector)
    return valid_positions

func wait_for_move_selection(valid_moves: Array[Vector2i]):
    pass

The Problem

The part I’m struggling with is how to handle waiting for player input to select a valid move and then continue to the next step in the constraint_sequence. For example, if a card requires the player to:

  1. Select an entity (constraint ENTITY)
  2. Choose a direction adjacent to that entity (constraint DIRECTION)

(I didn’t implement DIRECTION constraint yet, since this constraint is dependent on what the player selected previously, and I can’t get any ideas on how to implement this)

Then the flow should be:

  1. Highlight available moves based on the first constraint.
  2. Wait for the player to select one.
  3. Highlight the moves for the next constraint.
  4. Wait for the player to select again.
  5. Resolve the card’s action.

What I Need Help With

I’m not sure how to structure the logic to handle this sequence of player selections effectively. Should I be looking at implementing a state machine to manage these interactions? Or is there a different pattern or method in Godot that would make managing this sequential player input easier?

Any advice, examples, or resources would be greatly appreciated!

State machine is not necessary, this is pretty simple,

Is card selected; if is card plant, and is selected space empty?

If yes, yes, and yes move.

but if you need to dehighlight once a card is selected you can have a very simple hand state machine. Starts as: player turn, card in hand, play card. repeat

Previously, when my card system was simpler and each card type corresponded to a single, straightforward placement strategy, implementation was manageable, I made it work.

However, I have now expanded the functionality of my cards, introducing more complex mechanics such as:

  1. Cards that allow entities to swap positions.
  2. Cards that can apply buffs to multiple targets simultaneously.
  3. Cards that require the player to select an entity first and then choose a tile.

This is where I’ve encountered challenges. I am struggling to design the logic that prompts the game to guide the player through sequential steps: selecting the first target, choosing the next target, and only then executing the card’s effect, for example.

The main question I face is how to effectively structure this process. Should the game enter a while loop that waits for the player to complete all necessary selections before playing the card’s effect? I am hesitant to use a while loop to pause the game state while awaiting input, as it feels problematic.

I mean by definition this is a state machine process. And no you don’t want to wait for input.

Will say as a design process you want to boil down the requirements to be able to divide responsibilities and encapsulate concerns.

To me I envision an invisible hand that is controlled by the player. It has two states, card or no card selected.

The card will have a classification that dictates legal moves. And this will highlight on the table what can be done.

So you now have a highlight system that will only do that and you want, by passing a list of cards/spaces to highlight.

So basically you need another system that maintains the positions and if those positions have cards, I’ll just call this “table system”. So if I have a specific card in hand I need to call this system to get all valid positions then pass those positions to the highlight system.

Then when the player tries to place the card you should check that specific position via the table system to validate the move again before updating the table, as the player can ignore highlight and try to place on an invalid spot, nothing should happen, or the hand should reset. (design decisions.)

If there is a enhancement based on a card effect and placement that can be triggered by hoovering input while card is in hand, I would extend the table to perform a special template search on the table and extend highlight system to perform a secondary highlight.

The card type should detail a lot of specifics and specific enhancements but contain no behavior. The table should act on some these specifics to find valid spots and trigger highlights. the table should not really care to much about buffs and debuffs. That can be handled by some game manager system that can read the table state at appropriate times.

So I see the card as the central behavior less data points that will need to be written in an extendable way and systems that can behave on the card stats. And this data, like multi-card enhancements, needs to be defined before you start implementing the systems that will use it.

1 Like

@pennyloafers answer was excellent BTW. But the answer to the question above you asked is a resounding no, absolutely not, this is a terrible, terrible idea.

Instead think more about activating events. This is really no different than receiving a button input. It activates events you act on. So player clicks on card, event fired, card has been clicked. Just like a button press, are they allowed to do that at this moment in time? Is it their turn? Is that appropriate etc etc. And that is where all your separate systems come into play like @pennyloafers described.

The card should keep only card stats.
The table should keep only table states and table rules
The highlight system should only keep highlight states and highlight rules
The options system should only keep possible options and option rules
The gameplay system should only keep overall gameplay stats and rules
The turnsystem should only keep turn stats and turn rules

Your game manager will talk to all these systems and they might ask the GameManager questions back. Your CardPlayer.gd is already trying to cram tons of these bits into one script. It will turn into a nightmare.

  • Separate your concerns.
  • Be event driven.
  • Systems should have a single responsibility
  • Encapsulate your systems and expose only necessary interfaces

Sounds like an interesting coding project. Good luck with it.

PS Don’t overthink it either, it sounds like you might be prototyping still.

1 Like

@pauldrewett, event driven! Yes! thanks, that’s what I was feeling on the inside and failing to recognize it.:sweat_smile:

1 Like