
Godot Genre Card Game
- 196 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-genre-card-game for development tasks
About
godot-genre-card-game: A skill for development. This provides functionality for development workflows.
- godot-genre-card-game
Godot Genre Card Game by the numbers
- 196 all-time installs (skills.sh)
- +11 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,033 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-genre-card-gameAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 196 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-genre-card-game for development tasks
Files
Genre: Card Game
Expert blueprint for digital card games with data-driven design and juicy UI.
NEVER Do (Expert Anti-Patterns)
Logic & Architecture
- NEVER hardcode card logic inside UI scripts; strictly encapsulate gameplay effects in `Callable` objects or Command resources pushed to a LIFO stack.
- NEVER perform board-state calculations (Power/Toughness) in
_process(); strictly use Signal-driven triggers or a centralizedEffectStackresolver. - NEVER forget LIFO Stack Resolution; strictly use `Array.push_back()` and `Array.pop_back()` to resolve reactions from top-to-bottom.
UX & Animation
- NEVER skip Z-Index management during drag-and-drop; strictly raise the card to the front on click to prevent it sliding under other cards.
- NEVER allow instant card "teleportation" between piles; strictly use Tween animations (0.2s+) to give cards a tactile, physical feel.
- NEVER use
global_positionfor cards in hand; strictly position them using a `Curve2D` (Bezier) layout with `sample_baked()` for smooth, non-circular arcs. - NEVER allow instant card "teleportation" between piles; strictly use `create_tween()` and `tween_property` chainings (0.2s+) for juicy card-feel.
Deck & State Management
- NEVER forget to handle Empty Deck scenarios; strictly implement auto-reshuffle of the discard pile to prevent soft-locks.
- NEVER use floating point numbers for discrete card stats; strictly use
intfor Costs, Attack, and Health to avoid precision drift. - NEVER use standard Control nodes for mass tokens/battlefields; strictly use `_draw()` custom drawing to bypass SceneTree overhead when rendering 100+ cards or map icons.
- NEVER rely on SceneTree order for hand logic; strictly manage logical order in an Array and update visuals via `queue_redraw()`.
- NEVER erase array elements during a standard
forloop; strictly iterate in reverse or usefilter()to avoid indexing errors. - NEVER forget to provide parameterless constructors in
_init(); otherwise, Resources will fail to load in the Inspector.
---
🛠 Expert Components (scripts/)
Original Expert Patterns
- card_effect_resolution.gd - Stack-based effect resolver (LIFO/FIFO) handling nested triggers and counter-play.
Modular Components
- card_data_resource.gd - Data-driven card definitions allowing Inspector-based design.
- deck_shuffle_bag.gd - Secure randomization patterns for uniform card distribution.
- turn_state_machine.gd - Managing rigid phases (Draw, Play, Combat) via state matching.
- card_drag_drop.gd - Implementation of native
_get_drag_data()for Control nodes. - board_query_filter.gd - Functional
filter()patterns for querying board metadata. - card_tween_manager.gd - Managing interruptible card juice and board transitions.
- reactive_card_ui.gd - Resource-signal driven UI for automatic visual state updates.
- board_state_dictionary.gd - Grid-based tracking (Vector2i) decoupled from Node order.
- match_state_resetter.gd - Clean-up pattern for in-match temporary Resource modifications.
- deck_builder_validator.gd - Backend logic for deck-building constraints and mana curves.
---
Core Loop
1. Draw: Player draws cards from a deck into their hand. 2. Evaluate: Player assesses board state, mana/energy, and card options. 3. Play: Player plays cards to trigger effects (damage, buff, summon). 4. Resolve: Effects occur immediately or go onto a stack. 5. Discard/End: Unused cards are discarded (roguelike) or kept (TCG), turn ends.
Skill Chain
| Phase | Skills | Purpose |
|---|---|---|
| 1. Data | resources, custom-resources | Defining Card properties (Cost, Type, Effect) |
| 2. UI | control-nodes, layout-containers | Hand layout, card positioning, tooltips |
| 3. Input | drag-and-drop, state-machines | Dragging cards to targets, hovering |
| 4. Logic | command-pattern, signals | Executing card effects, turn phases |
| 5. Polish | godot-tweening, shaders | Draw animations, holographic foils |
Architecture Overview
1. Card Data (Resource-based)
Godot Resources are perfect for card data.
# card_data.gd
extends Resource
class_name CardData
enum Type { ATTACK, SKILL, POWER }
enum Target { ENEMY, SELF, ALL_ENEMIES }
@export var id: String
@export var name: String
@export_multiline var description: String
@export var cost: int
@export var type: Type
@export var target_type: Target
@export var icon: Texture2D
@export var effect_script: Script # Custom logic per card2. Deck Manager
Handles the piles: Draw Pile, Hand, Discard Pile, Exhaust Pile.
# deck_manager.gd
var draw_pile: Array[CardData] = []
var hand: Array[CardData] = []
var discard_pile: Array[CardData] = []
func draw_cards(amount: int) -> void:
for i in amount:
if draw_pile.is_empty():
reshuffle_discard()
if draw_pile.is_empty():
break # No cards left
var card = draw_pile.pop_back()
hand.append(card)
card_drawn.emit(card)
func reshuffle_discard() -> void:
draw_pile.append_array(discard_pile)
discard_pile.clear()
draw_pile.shuffle()3. Card Visual (UI)
The interactive node representing a card in hand.
# card_ui.gd
extends Control
var card_data: CardData
var start_pos: Vector2
var is_dragging: bool = false
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
start_drag()
else:
end_drag()
func _process(delta: float) -> void:
if is_dragging:
global_position = get_global_mouse_position() - size / 2
else:
# Hover effect or return to hand position
passKey Mechanics Implementation
Effect Resolution (Command Pattern)
Decouple the "playing" of a card from its "effect".
func play_card(card: CardData, target: Node) -> void:
if current_energy < card.cost:
show_error("Not enough energy")
return
current_energy -= card.cost
# Execute effect
var effect = card.effect_script.new()
effect.execute(target)
move_to_discard(card)Hand Layout (Arching)
Cards in hand usually form an arc. Use a math formula (Bezier or Circle) to position them based on index and total_cards.
func update_hand_visuals() -> void:
var center_x = screen_width / 2
var radius = 1000.0
var angle_step = 5.0
for i in hand_visuals.size():
var card = hand_visuals[i]
var angle = deg_to_rad((i - hand_visuals.size() / 2.0) * angle_step)
var target_pos = Vector2(
center_x + sin(angle) * radius,
screen_height + cos(angle) * radius
)
card.target_rotation = angle
card.target_position = target_posCommon Pitfalls
1. Complexity Overload: Too many keywords. Fix: Stick to 3-5 core keywords (e.g., Taunt, Poison, Shield) and expand slowly. 2. Unreadable Text: Tiny fonts on cards. Fix: Use icons for common stats (Damage, Block) and keep text short. 3. Animation Lock: Waiting for slow animations to finish before playing the next card. Fix: Allow queueing actions or keep animations snappy (< 0.3s).
Godot-Specific Tips
- MouseFilter: Getting drag/drop to work with overlapping UI requires careful setup of
mouse_filter(Pass vs Stop). - Z-Index: Use
z_indexorCanvasLayerto ensure the dragged card is always on top of everything else. - Tweens: Essential! Tween position, rotation, and scale for that "juicy" Hearthstone/Slay the Spire feel.
---
🚀 Elite Technical Implementations (Batch 09)
1. Holographic Foil (Shader Script)
Add visual rarity and "juice" to cards using a holographic shader. This script uses iridescence based on TIME and UV coordinates to create a shifting rainbow effect.
shader_type canvas_item;
uniform float foil_speed : hint_range(0.1, 5.0) = 1.0;
uniform float foil_intensity : hint_range(0.0, 1.0) = 0.5;
void fragment() {
vec4 base_color = texture(TEXTURE, UV);
// Create shifting rainbow iridescence
vec3 holo_color = vec3(
0.5 + 0.5 * sin(TIME * foil_speed + UV.x * 10.0),
0.5 + 0.5 * sin(TIME * foil_speed + UV.y * 10.0 + 2.0),
0.5 + 0.5 * sin(TIME * foil_speed + (UV.x + UV.y) * 10.0 + 4.0)
);
// Blend with base texture alpha
COLOR = vec4(mix(base_color.rgb, holo_color, foil_intensity * base_color.a), base_color.a);
}2. Card-History Logging (Action Tracking)
Track card actions (Played, Drawn, Discarded) for a history panel using a custom Logger. This intercepts messages tagged with [CARD] and routes them to a turn history buffer.
class_name CardHistoryLogger extends Logger
signal history_updated(entry: String)
var turn_history: Array[String] = []
func _log_message(message: String, error: bool) -> void:
if not error and message.begins_with("[CARD]"):
turn_history.append(message)
history_updated.emit(message)
# To register (in an Autoload):
# func _init(): OS.add_logger(CardHistoryLogger.new())3. Hand-Limit Logic (Over-Draw Protection)
Encapsulate hand data and enforce a maximum size. Use signals to notify the UI when a card is successfully drawn or discarded due to being overdrawn.
class_name HandManager extends Node
signal card_drawn(card: Resource)
signal card_overdrawn(card: Resource)
@export var max_hand_size: int = 10
var _current_hand: Array[Resource] = []
func draw_card(new_card: Resource) -> void:
if _current_hand.size() >= max_hand_size:
# Hand is full; trigger overdraw
card_overdrawn.emit(new_card)
else:
_current_hand.append(new_card)
card_drawn.emit(new_card)- Master Skill: godot-master
# board_query_filter.gd
# Using functional filtering to query card states
extends Node
# EXPERT NOTE: Array.filter() is highly efficient for
# logic like "Find all Taunt cards with health > 2".
func find_taunters(board_cards: Array[Node]) -> Array[Node]:
return board_cards.filter(func(card):
return card.get("is_taunt") == true and card.health > 2
)
# board_state_dictionary.gd
# Tracking card positions via typed dictionaries
extends Node
# EXPERT NOTE: Dictionaries mapping Vector2i to CardData
# are better for board logic than 2D Godot node arrays.
var board: Dictionary = {} # Vector2i -> CardData
func place_card(coord: Vector2i, card: CardData):
if !board.has(coord):
board[coord] = card
print("Card placed at ", coord)
func get_card_at(coord: Vector2i) -> CardData:
return board.get(coord)
# card_data_resource.gd
# Defining cards as lightweight data-driven Resources
class_name CardData extends Resource
# EXPERT NOTE: Resources allow designers to edit card stats
# in the Godot Inspector, saving them as .tres files.
@export var card_name: String = "Blank"
@export var mana_cost: int = 1
@export var attack: int = 0
@export var health: int = 1
# Setter with changed emission for reactive UI
func update_stats(new_atk, new_hp):
attack = new_atk
health = new_hp
emit_changed()
# skills/genre-card-game/scripts/card_data.gd
extends Resource
## Card Data Resource (Expert Pattern)
## Data definition for cards. Can be created in Inspector.
class_name CardData
enum CardType { ATTACK, SKILL, POWER, CURSE }
enum TargetType { ENEMY, SELF, ALL_ENEMIES, NONE }
@export_group("Visuals")
@export var id: String
@export var name: String
@export_multiline var description: String
@export var icon: Texture2D
@export_group("Stats")
@export var cost: int = 1
@export var type: CardType = CardType.ATTACK
@export var target: TargetType = TargetType.ENEMY
@export var value: int = 0 # Generic value (Damage, Block amount)
@export_group("Behavior")
@export var script_logic: Script # Optional: Attach custom script for unique effects
func get_modified_cost(player_stats: Dictionary) -> int:
# Hook for cost reduction logic
return cost
## EXPERT USAGE:
## Right-click FileSystem -> Create New -> Resource -> CardData.
## Fill in fields. Load these Resources into DeckManager.
# card_drag_drop.gd
# Native Control node drag-and-drop implementation
extends Control
# EXPERT NOTE: Using Godot's built-in drag API ensures
# consistency and handles OS-level cursor and window events.
func _get_drag_data(_at_position: Vector2):
var preview = Label.new()
preview.text = name
set_drag_preview(preview)
return self # Pass card data or node to the drop target
func _can_drop_data(_pos: Vector2, _data):
return _data is Control # Basic validation
# skills/genre-card-game/scripts/card_effect_resolution.gd
extends Node
## Card Effect Resolution (Expert Pattern)
## Implements a Command Pattern / Stack for resolving card effects.
## Allows for complex chains, counter-play, and sequential animations.
class_name CardEffectResolution
signal effect_started(effect: CardEffect)
signal effect_finished(effect: CardEffect)
signal queue_empty
var effect_queue: Array[CardEffect] = []
var is_resolving: bool = false
# Inner class or external resource for Effect
class CardEffect:
var source_card: Resource
var target: Node
var type: String # DAMAGE, HEAL, DRAW
var value: int
func execute() -> void:
# Override this in subclasses
pass
func add_effect(effect: CardEffect) -> void:
effect_queue.append(effect)
if not is_resolving:
_resolve_next()
func _resolve_next() -> void:
if effect_queue.is_empty():
is_resolving = false
queue_empty.emit()
return
is_resolving = true
var effect = effect_queue.pop_front() # BFS or FIFO. Use pop_back for LIFO (Stack)
effect_started.emit(effect)
# Execute logic
await _execute_effect_logic(effect)
effect_finished.emit(effect)
# Recursive next
_resolve_next()
func _execute_effect_logic(effect: CardEffect) -> void:
# In a full system, this would call effect.execute()
# Here we simulate with a match or generic handler
print("Resolving Effect: %s on %s" % [effect.type, effect.target])
match effect.type:
"DAMAGE":
if effect.target.has_method("take_damage"):
effect.target.take_damage(effect.value)
"HEAL":
if effect.target.has_method("heal"):
effect.target.heal(effect.value)
# Fake animation delay
await get_tree().create_timer(0.5).timeout
## EXPERT USAGE:
## When playing a card, instantiate CardEffect and pass to add_effect().
## Listen to signals to block UI during resolution.
# card_tween_manager.gd
# Managing fluent and interruptible card animations
extends Node
# EXPERT NOTE: Always assign Tweens to variables to allow
# kill() or parallel() management if board state changes fast.
func play_to_board(card: Control, target_pos: Vector2):
var tween = create_tween().set_trans(Tween.TRANS_QUART).set_ease(Tween.EASE_OUT)
tween.tween_property(card, "global_position", target_pos, 0.4)
# Interruptible: if card is destroyed, this tween stays clean.
# deck_builder_validator.gd
# Enforcing rules during card collection management
extends Node
# EXPERT NOTE: Use for validating "Max 3 copies of a card"
# or "Total mana curve" constraints.
func is_deck_valid(deck: Array[CardData]) -> bool:
if deck.size() != 30: return false
var counts = {}
for card in deck:
counts[card.card_name] = counts.get(card.card_name, 0) + 1
if counts[card.card_name] > 2: return false # Duplicate limit
return true
# deck_shuffle_bag.gd
# Secure deck randomization using the "Shuffle Bag" pattern
extends Node
# EXPERT NOTE: Shuffle-bag logic prevents streaks of bad luck
# by ensuring a uniform distribution over the deck lifetime.
var deck: Array[CardData] = []
var rng := RandomNumberGenerator.new()
func _ready():
rng.randomize()
func shuffle_deck():
deck.shuffle() # Engine-level randomized shuffle
func draw_card() -> CardData:
return deck.pop_back() if !deck.is_empty() else null
# match_state_resetter.gd
# Cleaning up temporary match buffs on resources
extends Node
# EXPERT NOTE: Implement a reset on resources to ensure
# match-only buffs don't persist in the .tres files.
func reset_card_collection(collection: Array[CardData]):
for card in collection:
# Custom logic to restore base values
card.update_stats(card.get("base_atk"), card.get("base_hp"))
# reactive_card_ui.gd
# Automatically updating UI nodes via Resource listeners
extends Control
@export var data: CardData
@onready var label = $NameLabel
func _ready():
# EXPERT: React to data changes from ANY system
data.changed.connect(_update_ui)
_update_ui()
func _update_ui():
label.text = data.card_name
print("UI Refreshed for ", data.card_name)
# turn_state_machine.gd
# Handling rigid turn phases via match patterns
extends Node
# EXPERT NOTE: match statements are the first-class way
# to handle discrete turn-based game states.
enum Phase { DRAW, MAIN, COMBAT, END }
var current_phase: Phase = Phase.DRAW
func advance_phase():
match current_phase:
Phase.DRAW: current_phase = Phase.MAIN
Phase.MAIN: current_phase = Phase.COMBAT
Phase.COMBAT: current_phase = Phase.END
Phase.END: current_phase = Phase.DRAW
print("New Phase: ", Phase.keys()[current_phase])