You're going to put the pieces together: ActionQueue drives the turn flow, an EnemyStrategy picks moves, the intent appears on screen before the enemy acts, and Bleed/Strength status effects resolve correctly. When you're done, you have a game that looks and feels like a turn-based card game even if the art is a gray rectangle.
1Combatant base class. Build the Combatant from Lesson 3. Make Player and Enemy nodes extend it (they're both combatants — DRY this up).
2ActionQueue in DuelController. Add var queue := ActionQueue.new() to DuelController. Replace inline state logic with enqueues:
func _on_state_entered(s: State) -> void:
match s:
State.PLAYER_DRAW:
queue.enqueue(DrawAction.new(player, 7 - player.hand.card_count()))
await queue.idle
transition_to(State.PLAYER_PLAY)
State.RESOLVING:
# player's word already queued its damage via _on_word_submitted
await queue.idle
transition_to(State.ENEMY_TURN)
State.ENEMY_TURN:
enemy.start_turn() # ticks statuses
var actions := enemy.strategy.act({ "player": player, "rng": Global.rng, "self": enemy })
for a in actions:
queue.enqueue(a)
await queue.idle
# Decide next intent
enemy.plan_next()
if enemy.hp <= 0: transition_to(State.VICTORY)
elif player.hp <= 0: transition_to(State.DEFEAT)
else: transition_to(State.PLAYER_DRAW)
3Intent UI. Above the enemy sprite, add a TextureRect + Label. On each enemy.plan_next(), update them based on enemy.strategy.intent(context). At duel start, call plan_next once to seed the first intent.
4Damage popups. When a Combatant takes damage, spawn a floating Label over it that animates upward and fades out. Minimal polish, huge game-feel impact:
# scripts/damage_popup.gd (attached to a Label)
extends Label
func show_damage(amount: int, color: Color = Color.RED) -> void:
text = str(amount)
modulate = color
var t := create_tween()
t.set_parallel()
t.tween_property(self, "position:y", position.y - 60, 0.6)
t.tween_property(self, "modulate:a", 0.0, 0.6).set_delay(0.2)
t.chain().tween_callback(queue_free)
Connect Combatant.damaged → spawn a popup at the Combatant's position.
5Goblin enemy. Create enemies/goblin.tres:
Attack 4, Block 3, Attack 6.Paste into your Main scene's enemy slot.
6Bleed card. Add a CardData with a BleedApplyEffect in its effects. When played, the word deals normal damage AND applies 2 stacks of Bleed to the enemy.
class_name BleedApplyEffect
extends CardEffect
@export var amount: int = 2
func apply(context: Dictionary) -> void:
var bleed := BleedEffect.new()
bleed.stacks = amount
context.target.status_effects.append(bleed)
Craft a special CardData venom_tile.tres that's a "V" tile with this effect. Add one to the starter deck.
7Ship it, playtest. Play five full duels. Lose one intentionally to test DEFEAT state. Win one to test VICTORY. Note what feels bad — intent unclear? Damage numbers too small? Enemy pattern too predictable? Write the notes in your progress journal. You'll revisit in Module 6.
enemies/grammar_lich.tres: 40 HP, cycles attack/block/big attack/heal, with longer words in its pool.