init game files

This commit is contained in:
2026-01-24 16:29:11 -05:00
commit ea2028cf13
171 changed files with 191733 additions and 0 deletions

178
scripts/game/CPPool.gd Normal file
View File

@@ -0,0 +1,178 @@
class_name CPPool
extends RefCounted
## CPPool - Tracks Crystal Points generated during a turn
# CP stored by element
var _cp: Dictionary = {}
func _init() -> void:
clear()
## Clear all CP (called at end of each action)
func clear() -> void:
for element in Enums.Element.values():
_cp[element] = 0
## Add CP of a specific element
func add_cp(element: Enums.Element, amount: int) -> void:
_cp[element] = _cp.get(element, 0) + amount
## Get CP of a specific element
func get_cp(element: Enums.Element) -> int:
return _cp.get(element, 0)
## Get total CP available
func get_total_cp() -> int:
var total = 0
for element in _cp:
total += _cp[element]
return total
## Check if we can afford a card cost
func can_afford_card(card_data: CardDatabase.CardData) -> bool:
if not card_data:
return false
var cost = card_data.cost
var elements = card_data.elements
# For Light/Dark cards, just need total CP
var is_light_dark = false
for element in elements:
if Enums.is_light_or_dark(element):
is_light_dark = true
break
if is_light_dark:
return get_total_cp() >= cost
# For multi-element cards, need at least 1 CP of each element
if elements.size() > 1:
for element in elements:
if get_cp(element) < 1:
return false
# Total must be at least the cost
return get_total_cp() >= cost
# For single element cards, need at least 1 CP of that element
var primary_element = elements[0] if elements.size() > 0 else Enums.Element.FIRE
if get_cp(primary_element) < 1:
return false
return get_total_cp() >= cost
## Spend CP to pay a card cost
## Returns true if successful, false if cannot afford
func spend_for_card(card_data: CardDatabase.CardData) -> bool:
if not can_afford_card(card_data):
return false
var cost = card_data.cost
var elements = card_data.elements
var remaining = cost
# For multi-element, spend 1 of each required element first
if elements.size() > 1:
for element in elements:
_cp[element] -= 1
remaining -= 1
# For single element (non-Light/Dark), spend at least 1 of that element
elif elements.size() == 1:
var element = elements[0]
if not Enums.is_light_or_dark(element):
_cp[element] -= 1
remaining -= 1
# Spend remaining from any element
while remaining > 0:
var spent = false
for element in _cp:
if _cp[element] > 0:
_cp[element] -= 1
remaining -= 1
spent = true
break
if not spent:
push_error("Failed to spend remaining CP")
return false
return true
## Check if we can afford an ability cost
func can_afford_ability(cost: CardDatabase.CostData) -> bool:
if not cost:
return true # No cost means free
# Check element-specific requirements
if cost.fire > 0 and get_cp(Enums.Element.FIRE) < cost.fire:
return false
if cost.ice > 0 and get_cp(Enums.Element.ICE) < cost.ice:
return false
if cost.wind > 0 and get_cp(Enums.Element.WIND) < cost.wind:
return false
if cost.lightning > 0 and get_cp(Enums.Element.LIGHTNING) < cost.lightning:
return false
if cost.water > 0 and get_cp(Enums.Element.WATER) < cost.water:
return false
if cost.earth > 0 and get_cp(Enums.Element.EARTH) < cost.earth:
return false
if cost.light > 0 and get_cp(Enums.Element.LIGHT) < cost.light:
return false
if cost.dark > 0 and get_cp(Enums.Element.DARK) < cost.dark:
return false
# Check total
return get_total_cp() >= cost.get_total_cp()
## Spend CP to pay an ability cost
func spend_for_ability(cost: CardDatabase.CostData) -> bool:
if not can_afford_ability(cost):
return false
# Spend element-specific CP
if cost.fire > 0:
_cp[Enums.Element.FIRE] -= cost.fire
if cost.ice > 0:
_cp[Enums.Element.ICE] -= cost.ice
if cost.wind > 0:
_cp[Enums.Element.WIND] -= cost.wind
if cost.lightning > 0:
_cp[Enums.Element.LIGHTNING] -= cost.lightning
if cost.water > 0:
_cp[Enums.Element.WATER] -= cost.water
if cost.earth > 0:
_cp[Enums.Element.EARTH] -= cost.earth
if cost.light > 0:
_cp[Enums.Element.LIGHT] -= cost.light
if cost.dark > 0:
_cp[Enums.Element.DARK] -= cost.dark
# Spend generic from any element
var generic_remaining = cost.generic
while generic_remaining > 0:
for element in _cp:
if _cp[element] > 0:
_cp[element] -= 1
generic_remaining -= 1
break
return true
## Get a display-friendly dictionary of current CP
func get_display_data() -> Dictionary:
var data = {}
for element in _cp:
if _cp[element] > 0:
data[Enums.element_to_string(element)] = _cp[element]
return data
func _to_string() -> String:
var parts = []
for element in _cp:
if _cp[element] > 0:
parts.append("%s: %d" % [Enums.element_to_string(element), _cp[element]])
if parts.size() == 0:
return "[CPPool: empty]"
return "[CPPool: " + ", ".join(parts) + "]"

View File

@@ -0,0 +1,193 @@
class_name CardInstance
extends RefCounted
## CardInstance - Runtime instance of a card in the game
## Represents a specific card in play with its current state
# Reference to card definition
var card_data: CardDatabase.CardData
# Unique instance ID
var instance_id: int = 0
# Current state
var state: Enums.CardState = Enums.CardState.ACTIVE
var current_power: int = 0
var damage_received: int = 0
# Owner and controller
var owner_index: int = 0 # Player who owns this card (0 or 1)
var controller_index: int = 0 # Player who currently controls this card
# Current zone
var zone_type: Enums.ZoneType = Enums.ZoneType.DECK
# Temporary effects (cleared at end of turn)
var power_modifiers: Array[int] = []
var temporary_abilities: Array = []
# Turn tracking
var turns_on_field: int = 0
var attacked_this_turn: bool = false
# Static counter for unique IDs
static var _next_id: int = 1
func _init(data: CardDatabase.CardData = null, owner: int = 0) -> void:
card_data = data
owner_index = owner
controller_index = owner
instance_id = _next_id
_next_id += 1
if data:
current_power = data.power
## Get the card's current power (base + modifiers)
func get_power() -> int:
var total = current_power
for mod in power_modifiers:
total += mod
return max(0, total)
## Check if this is a Forward
func is_forward() -> bool:
return card_data and card_data.type == Enums.CardType.FORWARD
## Check if this is a Backup
func is_backup() -> bool:
return card_data and card_data.type == Enums.CardType.BACKUP
## Check if this is a Summon
func is_summon() -> bool:
return card_data and card_data.type == Enums.CardType.SUMMON
## Check if the card is active (not dull)
func is_active() -> bool:
return state == Enums.CardState.ACTIVE
## Check if the card is dull
func is_dull() -> bool:
return state == Enums.CardState.DULL
## Dull this card
func dull() -> void:
state = Enums.CardState.DULL
## Activate this card
func activate() -> void:
state = Enums.CardState.ACTIVE
## Check if this card can attack
func can_attack() -> bool:
if not is_forward():
return false
if is_dull():
return false
if attacked_this_turn:
return false
# Must have been on field since start of turn (or have Haste)
if turns_on_field < 1 and not has_haste():
return false
return true
## Check if this card can block
func can_block() -> bool:
if not is_forward():
return false
if is_dull():
return false
return true
## Check if this card can use dull abilities
func can_use_dull_ability() -> bool:
# Must have been on field since start of turn (or have Haste)
# Monsters are exception
if card_data.type == Enums.CardType.MONSTER:
return true
if turns_on_field < 1 and not has_haste():
return false
return true
## Check if card has Haste (from abilities)
func has_haste() -> bool:
if not card_data:
return false
for ability in card_data.abilities:
if ability.type == Enums.AbilityType.FIELD:
if "haste" in ability.effect.to_lower():
return true
return false
## Check if card has Brave (from abilities)
func has_brave() -> bool:
if not card_data:
return false
for ability in card_data.abilities:
if ability.type == Enums.AbilityType.FIELD:
if "brave" in ability.effect.to_lower():
return true
return false
## Check if card has First Strike
func has_first_strike() -> bool:
if not card_data:
return false
for ability in card_data.abilities:
if ability.type == Enums.AbilityType.FIELD:
if "first strike" in ability.effect.to_lower():
return true
return false
## Get primary element
func get_element() -> Enums.Element:
if card_data:
return card_data.get_primary_element()
return Enums.Element.FIRE
## Get all elements
func get_elements() -> Array[Enums.Element]:
if card_data:
return card_data.elements
return []
## Check if card is Light or Dark element
func is_light_or_dark() -> bool:
for element in get_elements():
if Enums.is_light_or_dark(element):
return true
return false
## Apply damage to this Forward
func apply_damage(amount: int) -> bool:
if not is_forward():
return false
damage_received += amount
# Check if broken (damage >= power)
return damage_received >= get_power()
## Reset temporary effects at end of turn
func end_turn_cleanup() -> void:
power_modifiers.clear()
temporary_abilities.clear()
damage_received = 0
attacked_this_turn = false
## Called when card enters field
func entered_field() -> void:
turns_on_field = 0
attacked_this_turn = false
damage_received = 0
## Called at start of each turn
func start_turn() -> void:
turns_on_field += 1
## Get a display string for this card
func get_display_name() -> String:
if card_data:
return card_data.name
return "Unknown Card"
func _to_string() -> String:
return "[CardInstance: %s (%s)]" % [get_display_name(), instance_id]

141
scripts/game/Enums.gd Normal file
View File

@@ -0,0 +1,141 @@
class_name Enums
extends RefCounted
## Card Elements
enum Element {
FIRE,
ICE,
WIND,
LIGHTNING,
WATER,
EARTH,
LIGHT,
DARK
}
## Card Types
enum CardType {
FORWARD,
BACKUP,
SUMMON,
MONSTER
}
## Ability Types
enum AbilityType {
FIELD,
AUTO,
ACTION,
SPECIAL
}
## Game Phases
enum TurnPhase {
ACTIVE,
DRAW,
MAIN_1,
ATTACK,
MAIN_2,
END
}
## Attack Phase Steps
enum AttackStep {
PREPARATION,
DECLARATION,
BLOCK_DECLARATION,
DAMAGE_RESOLUTION
}
## Zone Types
enum ZoneType {
DECK,
HAND,
FIELD_FORWARDS,
FIELD_BACKUPS,
DAMAGE,
BREAK,
STACK,
REMOVED
}
## Card States
enum CardState {
ACTIVE,
DULL
}
## Helper functions for Element
static func element_from_string(s: String) -> Element:
match s.to_lower():
"fire": return Element.FIRE
"ice": return Element.ICE
"wind": return Element.WIND
"lightning": return Element.LIGHTNING
"water": return Element.WATER
"earth": return Element.EARTH
"light": return Element.LIGHT
"dark": return Element.DARK
push_error("Unknown element: " + s)
return Element.FIRE
static func element_to_string(e: Element) -> String:
match e:
Element.FIRE: return "Fire"
Element.ICE: return "Ice"
Element.WIND: return "Wind"
Element.LIGHTNING: return "Lightning"
Element.WATER: return "Water"
Element.EARTH: return "Earth"
Element.LIGHT: return "Light"
Element.DARK: return "Dark"
return "Unknown"
static func element_to_color(e: Element) -> Color:
match e:
Element.FIRE: return Color(0.9, 0.2, 0.2) # Red
Element.ICE: return Color(0.4, 0.8, 0.9) # Cyan
Element.WIND: return Color(0.3, 0.8, 0.3) # Green
Element.LIGHTNING: return Color(0.6, 0.3, 0.8) # Purple
Element.WATER: return Color(0.2, 0.4, 0.9) # Blue
Element.EARTH: return Color(0.8, 0.7, 0.3) # Yellow
Element.LIGHT: return Color(1.0, 1.0, 0.9) # White
Element.DARK: return Color(0.2, 0.1, 0.3) # Dark Purple
return Color.WHITE
## Helper functions for CardType
static func card_type_from_string(s: String) -> CardType:
match s.to_lower():
"forward": return CardType.FORWARD
"backup": return CardType.BACKUP
"summon": return CardType.SUMMON
"monster": return CardType.MONSTER
push_error("Unknown card type: " + s)
return CardType.FORWARD
static func card_type_to_string(t: CardType) -> String:
match t:
CardType.FORWARD: return "Forward"
CardType.BACKUP: return "Backup"
CardType.SUMMON: return "Summon"
CardType.MONSTER: return "Monster"
return "Unknown"
## Helper functions for TurnPhase
static func phase_to_string(p: TurnPhase) -> String:
match p:
TurnPhase.ACTIVE: return "Active Phase"
TurnPhase.DRAW: return "Draw Phase"
TurnPhase.MAIN_1: return "Main Phase 1"
TurnPhase.ATTACK: return "Attack Phase"
TurnPhase.MAIN_2: return "Main Phase 2"
TurnPhase.END: return "End Phase"
return "Unknown Phase"
## Check if element is Light or Dark (special rules apply)
static func is_light_or_dark(e: Element) -> bool:
return e == Element.LIGHT or e == Element.DARK
## Alias for element_to_color
static func get_element_color(e: Element) -> Color:
return element_to_color(e)

332
scripts/game/GameState.gd Normal file
View File

@@ -0,0 +1,332 @@
class_name GameState
extends RefCounted
## GameState - Central game state container and rules engine
signal game_started
signal game_ended(winner_index: int)
signal card_played(card: CardInstance, player_index: int)
signal card_moved(card: CardInstance, from_zone: Enums.ZoneType, to_zone: Enums.ZoneType)
signal damage_dealt(player_index: int, amount: int, cards: Array[CardInstance])
signal forward_broken(card: CardInstance)
signal attack_declared(attacker: CardInstance)
signal block_declared(blocker: CardInstance)
signal combat_resolved(attacker: CardInstance, blocker: CardInstance)
signal cp_generated(player_index: int, element: Enums.Element, amount: int)
# Players
var players: Array[Player] = []
# Turn management
var turn_manager: TurnManager
# Shared stack for abilities/summons
var stack: Zone
# Game state flags
var game_active: bool = false
var winner_index: int = -1
func _init() -> void:
turn_manager = TurnManager.new()
stack = Zone.new(Enums.ZoneType.STACK, -1)
# Connect turn manager signals
turn_manager.phase_changed.connect(_on_phase_changed)
turn_manager.turn_started.connect(_on_turn_started)
turn_manager.turn_ended.connect(_on_turn_ended)
## Initialize a new game with two players
func setup_game(deck1: Array[String], deck2: Array[String]) -> void:
# Create players
players.clear()
players.append(Player.new(0, "Player 1"))
players.append(Player.new(1, "Player 2"))
# Setup decks
players[0].setup_deck(deck1)
players[1].setup_deck(deck2)
game_active = false
winner_index = -1
## Start the game
func start_game(first_player: int = -1) -> void:
if first_player < 0:
first_player = randi() % 2
players[first_player].is_first_player = true
# Draw initial hands (5 cards each)
players[0].draw_cards(5)
players[1].draw_cards(5)
game_active = true
turn_manager.start_game(first_player)
game_started.emit()
## Get current player
func get_current_player() -> Player:
return players[turn_manager.current_player_index]
## Get opponent of current player
func get_opponent() -> Player:
return players[1 - turn_manager.current_player_index]
## Get player by index
func get_player(index: int) -> Player:
if index >= 0 and index < players.size():
return players[index]
return null
## Execute Active Phase
func execute_active_phase() -> void:
var player = get_current_player()
player.activate_all()
turn_manager.advance_phase()
## Execute Draw Phase
func execute_draw_phase() -> void:
var player = get_current_player()
var draw_count = turn_manager.get_draw_count()
var drawn = player.draw_cards(draw_count)
# Check for loss condition (can't draw)
if drawn.size() < draw_count and player.deck.is_empty():
_check_loss_conditions()
turn_manager.advance_phase()
## End Main Phase
func end_main_phase() -> void:
# Clear any unused CP
get_current_player().cp_pool.clear()
turn_manager.advance_phase()
## Play a card from hand
func play_card(player_index: int, card: CardInstance) -> bool:
if not game_active:
return false
if player_index != turn_manager.current_player_index:
return false
if not turn_manager.is_main_phase():
return false
var player = players[player_index]
# Validate and play
if player.play_card(card):
card_played.emit(card, player_index)
card_moved.emit(card, Enums.ZoneType.HAND,
Enums.ZoneType.FIELD_FORWARDS if card.is_forward() else Enums.ZoneType.FIELD_BACKUPS)
return true
return false
## Discard a card for CP
func discard_for_cp(player_index: int, card: CardInstance) -> bool:
if not game_active:
return false
var player = players[player_index]
var element = card.get_element()
if player.discard_for_cp(card):
cp_generated.emit(player_index, element, 2)
card_moved.emit(card, Enums.ZoneType.HAND, Enums.ZoneType.BREAK)
return true
return false
## Dull a backup for CP
func dull_backup_for_cp(player_index: int, card: CardInstance) -> bool:
if not game_active:
return false
var player = players[player_index]
var element = card.get_element()
if player.dull_backup_for_cp(card):
cp_generated.emit(player_index, element, 1)
return true
return false
## Start Attack Phase
func start_attack_phase() -> void:
turn_manager.start_attack_declaration()
## Declare an attack
func declare_attack(attacker: CardInstance) -> bool:
if not game_active:
return false
if not turn_manager.is_attack_phase():
return false
var player = get_current_player()
if not player.field_forwards.has_card(attacker):
return false
if not attacker.can_attack():
return false
# Dull the attacker (unless Brave)
if not attacker.has_brave():
attacker.dull()
attacker.attacked_this_turn = true
turn_manager.set_attacker(attacker)
attack_declared.emit(attacker)
return true
## Declare a block
func declare_block(blocker: CardInstance) -> bool:
if not game_active:
return false
if turn_manager.attack_step != Enums.AttackStep.BLOCK_DECLARATION:
return false
var opponent = get_opponent()
if blocker != null:
if not opponent.field_forwards.has_card(blocker):
return false
if not blocker.can_block():
return false
turn_manager.set_blocker(blocker)
if blocker:
block_declared.emit(blocker)
return true
## Skip blocking
func skip_block() -> bool:
return declare_block(null)
## Resolve combat damage
func resolve_combat() -> void:
if turn_manager.attack_step != Enums.AttackStep.DAMAGE_RESOLUTION:
return
var attacker = turn_manager.current_attacker
var blocker = turn_manager.current_blocker
if blocker == null:
# Unblocked - deal 1 damage to opponent
_deal_damage_to_player(1 - turn_manager.current_player_index, 1)
else:
# Blocked - exchange damage
var attacker_power = attacker.get_power()
var blocker_power = blocker.get_power()
# Apply damage
var attacker_broken = attacker.apply_damage(blocker_power)
var blocker_broken = blocker.apply_damage(attacker_power)
# Break destroyed forwards
if attacker_broken:
_break_forward(attacker, turn_manager.current_player_index)
if blocker_broken:
_break_forward(blocker, 1 - turn_manager.current_player_index)
combat_resolved.emit(attacker, blocker)
turn_manager.complete_attack()
## End Attack Phase (no more attacks)
func end_attack_phase() -> void:
turn_manager.end_attack_phase()
## Execute End Phase
func execute_end_phase() -> void:
var player = get_current_player()
# Discard to hand limit
var discarded = player.discard_to_hand_limit()
for card in discarded:
card_moved.emit(card, Enums.ZoneType.HAND, Enums.ZoneType.BREAK)
# Cleanup
player.end_turn_cleanup()
turn_manager.advance_phase()
## Deal damage to a player
func _deal_damage_to_player(player_index: int, amount: int) -> void:
var player = players[player_index]
var damage_cards = player.take_damage(amount)
damage_dealt.emit(player_index, amount, damage_cards)
# Check for EX Bursts (simplified - would need full implementation)
for card in damage_cards:
if card.card_data and card.card_data.has_ex_burst:
# TODO: Offer EX Burst choice to player
pass
_check_loss_conditions()
## Break a forward
func _break_forward(card: CardInstance, player_index: int) -> void:
var player = players[player_index]
if player.break_card(card):
forward_broken.emit(card)
card_moved.emit(card, Enums.ZoneType.FIELD_FORWARDS, Enums.ZoneType.BREAK)
## Check for game-ending conditions
func _check_loss_conditions() -> void:
for i in range(players.size()):
var player = players[i]
# Check damage
if player.has_lost():
_end_game(1 - i) # Other player wins
return
# Check deck out (can't draw when needed)
# This is checked during draw phase
## End the game
func _end_game(winner: int) -> void:
game_active = false
winner_index = winner
game_ended.emit(winner)
## Phase change handler
func _on_phase_changed(phase: Enums.TurnPhase) -> void:
match phase:
Enums.TurnPhase.ACTIVE:
execute_active_phase()
Enums.TurnPhase.DRAW:
execute_draw_phase()
Enums.TurnPhase.ATTACK:
start_attack_phase()
Enums.TurnPhase.END:
execute_end_phase()
## Turn started handler
func _on_turn_started(player_index: int, _turn_number: int) -> void:
players[player_index].start_turn()
## Turn ended handler
func _on_turn_ended(_player_index: int) -> void:
pass
func _to_string() -> String:
if not game_active:
return "[GameState: Inactive]"
return "[GameState: Turn %d, Phase: %s, Player %d]" % [
turn_manager.turn_number,
turn_manager.get_phase_string(),
turn_manager.current_player_index + 1
]

263
scripts/game/Player.gd Normal file
View File

@@ -0,0 +1,263 @@
class_name Player
extends RefCounted
## Player - Represents a player's game state
var player_index: int = 0
var player_name: String = "Player"
# Zones
var deck: Zone
var hand: Zone
var field_forwards: Zone
var field_backups: Zone
var damage_zone: Zone
var break_zone: Zone
# Resources
var cp_pool: CPPool
# Game state
var is_first_player: bool = false
var has_mulliganed: bool = false
# Constants
const MAX_HAND_SIZE: int = 5
const MAX_BACKUPS: int = 5
const DAMAGE_TO_LOSE: int = 7
func _init(index: int, name: String = "") -> void:
player_index = index
player_name = name if name != "" else "Player %d" % (index + 1)
# Initialize zones
deck = Zone.new(Enums.ZoneType.DECK, index)
hand = Zone.new(Enums.ZoneType.HAND, index)
field_forwards = Zone.new(Enums.ZoneType.FIELD_FORWARDS, index)
field_backups = Zone.new(Enums.ZoneType.FIELD_BACKUPS, index)
damage_zone = Zone.new(Enums.ZoneType.DAMAGE, index)
break_zone = Zone.new(Enums.ZoneType.BREAK, index)
# Initialize CP pool
cp_pool = CPPool.new()
## Setup the player's deck from a list of card IDs
func setup_deck(card_ids: Array[String]) -> void:
for card_id in card_ids:
var card_data = CardDatabase.get_card(card_id)
if card_data:
var card = CardInstance.new(card_data, player_index)
deck.add_card(card)
# Shuffle the deck
deck.shuffle()
## Draw cards from deck to hand
func draw_cards(count: int) -> Array[CardInstance]:
var drawn: Array[CardInstance] = []
for i in range(count):
if deck.is_empty():
break # Can't draw from empty deck
var card = deck.pop_top_card()
if card:
hand.add_card(card)
drawn.append(card)
return drawn
## Check if player can draw (deck not empty)
func can_draw() -> bool:
return not deck.is_empty()
## Get current damage count
func get_damage_count() -> int:
return damage_zone.get_count()
## Check if player has lost (7+ damage)
func has_lost() -> bool:
return get_damage_count() >= DAMAGE_TO_LOSE
## Take damage (move cards from deck to damage zone)
func take_damage(amount: int) -> Array[CardInstance]:
var damage_cards: Array[CardInstance] = []
for i in range(amount):
if deck.is_empty():
break
var card = deck.pop_top_card()
if card:
damage_zone.add_card(card)
damage_cards.append(card)
return damage_cards
## Discard a card from hand to generate CP
func discard_for_cp(card: CardInstance) -> bool:
if not hand.has_card(card):
return false
# Light/Dark cards cannot be discarded for CP
if card.is_light_or_dark():
return false
hand.remove_card(card)
break_zone.add_card(card)
# Generate 2 CP of the card's element
var element = card.get_element()
cp_pool.add_cp(element, 2)
return true
## Dull a backup to generate CP
func dull_backup_for_cp(card: CardInstance) -> bool:
if not field_backups.has_card(card):
return false
if not card.is_backup():
return false
if card.is_dull():
return false
card.dull()
# Generate 1 CP of the backup's element
var element = card.get_element()
cp_pool.add_cp(element, 1)
return true
## Play a card from hand to field
func play_card(card: CardInstance) -> bool:
if not hand.has_card(card):
return false
# Check if we can afford it
if not cp_pool.can_afford_card(card.card_data):
return false
# Check field restrictions
if card.is_backup():
if field_backups.get_count() >= MAX_BACKUPS:
return false
# Check unique name restriction
if not card.card_data.is_generic:
if card.is_forward() and field_forwards.has_card_with_name(card.card_data.name):
return false
if card.is_backup() and field_backups.has_card_with_name(card.card_data.name):
return false
# Check Light/Dark restriction
if card.is_light_or_dark():
if field_forwards.has_light_or_dark() or field_backups.has_light_or_dark():
return false
# Pay the cost
if not cp_pool.spend_for_card(card.card_data):
return false
# Move to field
hand.remove_card(card)
if card.is_forward():
field_forwards.add_card(card)
card.state = Enums.CardState.ACTIVE
elif card.is_backup():
field_backups.add_card(card)
card.state = Enums.CardState.DULL # Backups enter dull
card.entered_field()
return true
## Break a card (move from field to break zone)
func break_card(card: CardInstance) -> bool:
var removed = false
if field_forwards.has_card(card):
field_forwards.remove_card(card)
removed = true
elif field_backups.has_card(card):
field_backups.remove_card(card)
removed = true
if removed:
break_zone.add_card(card)
return true
return false
## Activate all dull cards (Active Phase)
func activate_all() -> void:
for card in field_forwards.get_dull_cards():
card.activate()
for card in field_backups.get_dull_cards():
card.activate()
## Discard down to hand size (End Phase)
func discard_to_hand_limit() -> Array[CardInstance]:
var discarded: Array[CardInstance] = []
while hand.get_count() > MAX_HAND_SIZE:
# For now, just discard the last card
# In actual game, player would choose
var card = hand.pop_top_card()
if card:
break_zone.add_card(card)
discarded.append(card)
return discarded
## End of turn cleanup
func end_turn_cleanup() -> void:
# Clear CP pool
cp_pool.clear()
# Reset temporary effects on field cards
for card in field_forwards.get_cards():
card.end_turn_cleanup()
for card in field_backups.get_cards():
card.end_turn_cleanup()
## Start of turn setup
func start_turn() -> void:
# Increment turn counter for field cards
for card in field_forwards.get_cards():
card.start_turn()
for card in field_backups.get_cards():
card.start_turn()
## Get all cards on field
func get_all_field_cards() -> Array[CardInstance]:
var cards: Array[CardInstance] = []
cards.append_array(field_forwards.get_cards())
cards.append_array(field_backups.get_cards())
return cards
## Get forwards that can attack
func get_attackable_forwards() -> Array[CardInstance]:
var attackers: Array[CardInstance] = []
for card in field_forwards.get_cards():
if card.can_attack():
attackers.append(card)
return attackers
## Get forwards that can block
func get_blockable_forwards() -> Array[CardInstance]:
var blockers: Array[CardInstance] = []
for card in field_forwards.get_cards():
if card.can_block():
blockers.append(card)
return blockers
func _to_string() -> String:
return "[Player: %s, Hand: %d, Forwards: %d, Backups: %d, Damage: %d]" % [
player_name,
hand.get_count(),
field_forwards.get_count(),
field_backups.get_count(),
damage_zone.get_count()
]

150
scripts/game/TurnManager.gd Normal file
View File

@@ -0,0 +1,150 @@
class_name TurnManager
extends RefCounted
## TurnManager - Handles turn and phase progression
signal phase_changed(phase: Enums.TurnPhase)
signal turn_changed(player_index: int)
signal turn_started(player_index: int, turn_number: int)
signal turn_ended(player_index: int)
var current_phase: Enums.TurnPhase = Enums.TurnPhase.ACTIVE
var current_player_index: int = 0
var turn_number: int = 0
var is_first_turn: bool = true
# Attack phase tracking
var attack_step: Enums.AttackStep = Enums.AttackStep.PREPARATION
var current_attacker: CardInstance = null
var current_blocker: CardInstance = null
func _init() -> void:
pass
## Start a new game
func start_game(first_player: int) -> void:
current_player_index = first_player
turn_number = 1
is_first_turn = true
current_phase = Enums.TurnPhase.ACTIVE
turn_started.emit(current_player_index, turn_number)
## Advance to the next phase
func advance_phase() -> Enums.TurnPhase:
var next_phase = _get_next_phase(current_phase)
if next_phase == Enums.TurnPhase.ACTIVE:
# Starting a new turn
_end_current_turn()
_start_new_turn()
else:
current_phase = next_phase
phase_changed.emit(current_phase)
return current_phase
## Get the next phase in sequence
func _get_next_phase(phase: Enums.TurnPhase) -> Enums.TurnPhase:
match phase:
Enums.TurnPhase.ACTIVE:
return Enums.TurnPhase.DRAW
Enums.TurnPhase.DRAW:
return Enums.TurnPhase.MAIN_1
Enums.TurnPhase.MAIN_1:
return Enums.TurnPhase.ATTACK
Enums.TurnPhase.ATTACK:
return Enums.TurnPhase.MAIN_2
Enums.TurnPhase.MAIN_2:
return Enums.TurnPhase.END
Enums.TurnPhase.END:
return Enums.TurnPhase.ACTIVE # Loop back to start new turn
return Enums.TurnPhase.ACTIVE
## End the current turn
func _end_current_turn() -> void:
turn_ended.emit(current_player_index)
## Start a new turn
func _start_new_turn() -> void:
# Switch to other player
current_player_index = 1 - current_player_index
turn_number += 1
is_first_turn = false
current_phase = Enums.TurnPhase.ACTIVE
_reset_attack_state()
turn_changed.emit(current_player_index)
turn_started.emit(current_player_index, turn_number)
phase_changed.emit(current_phase)
## Reset attack phase state
func _reset_attack_state() -> void:
attack_step = Enums.AttackStep.PREPARATION
current_attacker = null
current_blocker = null
## Check if we're in a main phase (can play cards)
func is_main_phase() -> bool:
return current_phase == Enums.TurnPhase.MAIN_1 or current_phase == Enums.TurnPhase.MAIN_2
## Check if we're in attack phase
func is_attack_phase() -> bool:
return current_phase == Enums.TurnPhase.ATTACK
## Get cards drawn this turn (2 normally, 1 on first turn for first player)
func get_draw_count() -> int:
if is_first_turn and current_player_index == 0:
return 1
return 2
## Attack phase state management
## Start attack declaration
func start_attack_declaration() -> void:
attack_step = Enums.AttackStep.DECLARATION
## Set the attacking forward
func set_attacker(attacker: CardInstance) -> void:
current_attacker = attacker
attack_step = Enums.AttackStep.BLOCK_DECLARATION
## Set the blocking forward (or null for no block)
func set_blocker(blocker: CardInstance) -> void:
current_blocker = blocker
attack_step = Enums.AttackStep.DAMAGE_RESOLUTION
## Complete the current attack
func complete_attack() -> void:
current_attacker = null
current_blocker = null
attack_step = Enums.AttackStep.DECLARATION
## End the attack phase (no more attacks)
func end_attack_phase() -> void:
_reset_attack_state()
advance_phase()
## Get current phase as string
func get_phase_string() -> String:
return Enums.phase_to_string(current_phase)
## Get current attack step as string
func get_attack_step_string() -> String:
match attack_step:
Enums.AttackStep.PREPARATION:
return "Preparation"
Enums.AttackStep.DECLARATION:
return "Declare Attacker"
Enums.AttackStep.BLOCK_DECLARATION:
return "Declare Blocker"
Enums.AttackStep.DAMAGE_RESOLUTION:
return "Damage Resolution"
return "Unknown"
func _to_string() -> String:
return "[TurnManager: Turn %d, Player %d, Phase: %s]" % [
turn_number,
current_player_index + 1,
get_phase_string()
]

158
scripts/game/Zone.gd Normal file
View File

@@ -0,0 +1,158 @@
class_name Zone
extends RefCounted
## Zone - Container for cards in a specific game area
var zone_type: Enums.ZoneType
var owner_index: int # Player who owns this zone (-1 for shared zones like stack)
var _cards: Array[CardInstance] = []
# Signals (these would need to be connected via the parent)
# In GDScript, RefCounted can't have signals, so we'll use callbacks
var on_card_added: Callable = func(_card): pass
var on_card_removed: Callable = func(_card): pass
func _init(type: Enums.ZoneType, owner: int = -1) -> void:
zone_type = type
owner_index = owner
## Add a card to this zone
func add_card(card: CardInstance, at_top: bool = true) -> void:
if card in _cards:
push_warning("Card already in zone: " + str(card))
return
card.zone_type = zone_type
if at_top:
_cards.append(card)
else:
_cards.insert(0, card)
on_card_added.call(card)
## Remove a card from this zone
func remove_card(card: CardInstance) -> bool:
var index = _cards.find(card)
if index >= 0:
_cards.remove_at(index)
on_card_removed.call(card)
return true
return false
## Get all cards in this zone
func get_cards() -> Array[CardInstance]:
return _cards.duplicate()
## Get card count
func get_count() -> int:
return _cards.size()
## Check if zone contains a card
func has_card(card: CardInstance) -> bool:
return card in _cards
## Get card at index
func get_card_at(index: int) -> CardInstance:
if index >= 0 and index < _cards.size():
return _cards[index]
return null
## Get top card (last added)
func get_top_card() -> CardInstance:
if _cards.size() > 0:
return _cards[_cards.size() - 1]
return null
## Remove and return top card
func pop_top_card() -> CardInstance:
if _cards.size() > 0:
var card = _cards.pop_back()
on_card_removed.call(card)
return card
return null
## Get bottom card (first added)
func get_bottom_card() -> CardInstance:
if _cards.size() > 0:
return _cards[0]
return null
## Check if zone is empty
func is_empty() -> bool:
return _cards.size() == 0
## Shuffle the zone (for decks)
func shuffle() -> void:
_cards.shuffle()
## Clear all cards from zone
func clear() -> Array[CardInstance]:
var removed = _cards.duplicate()
_cards.clear()
for card in removed:
on_card_removed.call(card)
return removed
## Get all Forwards in zone
func get_forwards() -> Array[CardInstance]:
var forwards: Array[CardInstance] = []
for card in _cards:
if card.is_forward():
forwards.append(card)
return forwards
## Get all Backups in zone
func get_backups() -> Array[CardInstance]:
var backups: Array[CardInstance] = []
for card in _cards:
if card.is_backup():
backups.append(card)
return backups
## Get all active cards
func get_active_cards() -> Array[CardInstance]:
var active: Array[CardInstance] = []
for card in _cards:
if card.is_active():
active.append(card)
return active
## Get all dull cards
func get_dull_cards() -> Array[CardInstance]:
var dull: Array[CardInstance] = []
for card in _cards:
if card.is_dull():
dull.append(card)
return dull
## Find cards by name
func find_cards_by_name(card_name: String) -> Array[CardInstance]:
var found: Array[CardInstance] = []
for card in _cards:
if card.card_data and card.card_data.name == card_name:
found.append(card)
return found
## Find cards by element
func find_cards_by_element(element: Enums.Element) -> Array[CardInstance]:
var found: Array[CardInstance] = []
for card in _cards:
if element in card.get_elements():
found.append(card)
return found
## Check if zone has a card with specific name (non-generic check)
func has_card_with_name(card_name: String) -> bool:
for card in _cards:
if card.card_data and card.card_data.name == card_name:
if not card.card_data.is_generic:
return true
return false
## Check if zone has any Light or Dark card
func has_light_or_dark() -> bool:
for card in _cards:
if card.is_light_or_dark():
return true
return false