445 lines
13 KiB
GDScript3
445 lines
13 KiB
GDScript3
extends Node
|
|
|
|
## GameManager - Main game coordinator singleton
|
|
## Bridges game state, visuals, and UI
|
|
|
|
signal game_ready
|
|
signal game_started
|
|
signal game_ended(winner_name: String)
|
|
signal turn_changed(player_name: String, turn_number: int)
|
|
signal phase_changed(phase_name: String)
|
|
signal card_played(card_data: Dictionary)
|
|
signal damage_dealt(player_name: String, amount: int)
|
|
signal message(text: String)
|
|
signal action_undone(action_name: String)
|
|
signal undo_available_changed(available: bool)
|
|
|
|
# Game state
|
|
var game_state: GameState = null
|
|
|
|
# Undo system
|
|
var undo_system: UndoSystem = null
|
|
|
|
# State flags
|
|
var is_initialized: bool = false
|
|
var is_game_active: bool = false
|
|
|
|
# Current input mode
|
|
enum InputMode {
|
|
NONE,
|
|
SELECT_CARD_TO_PLAY,
|
|
SELECT_CP_SOURCE,
|
|
SELECT_ATTACKER,
|
|
SELECT_BLOCKER,
|
|
SELECT_TARGET
|
|
}
|
|
var input_mode: InputMode = InputMode.NONE
|
|
|
|
# Selection tracking
|
|
var selected_card: CardInstance = null
|
|
var pending_action: Callable = func(): pass
|
|
|
|
func _ready() -> void:
|
|
# Wait for CardDatabase to load
|
|
if CardDatabase.get_all_cards().size() == 0:
|
|
CardDatabase.database_loaded.connect(_on_database_loaded)
|
|
else:
|
|
_on_database_loaded()
|
|
|
|
func _on_database_loaded() -> void:
|
|
is_initialized = true
|
|
game_ready.emit()
|
|
print("GameManager: Ready")
|
|
|
|
## Start a new game
|
|
## deck1 and deck2 are optional arrays of card IDs
|
|
## If empty, test decks will be created
|
|
func start_new_game(deck1: Array = [], deck2: Array = []) -> void:
|
|
if not is_initialized:
|
|
push_error("GameManager not initialized")
|
|
return
|
|
|
|
# Create new game state
|
|
game_state = GameState.new()
|
|
|
|
# Create undo system
|
|
undo_system = UndoSystem.new(game_state)
|
|
undo_system.undo_available_changed.connect(_on_undo_available_changed)
|
|
undo_system.action_undone.connect(_on_action_undone)
|
|
|
|
# Connect signals
|
|
_connect_game_signals()
|
|
|
|
# Use provided decks or create test decks
|
|
var player1_deck: Array[String] = []
|
|
var player2_deck: Array[String] = []
|
|
|
|
if deck1.is_empty():
|
|
player1_deck = CardDatabase.create_test_deck(0)
|
|
else:
|
|
for card_id in deck1:
|
|
player1_deck.append(card_id)
|
|
|
|
if deck2.is_empty():
|
|
player2_deck = CardDatabase.create_test_deck(1)
|
|
else:
|
|
for card_id in deck2:
|
|
player2_deck.append(card_id)
|
|
|
|
# Setup and start
|
|
game_state.setup_game(player1_deck, player2_deck)
|
|
game_state.start_game()
|
|
|
|
is_game_active = true
|
|
game_started.emit()
|
|
message.emit("Game started!")
|
|
|
|
## Connect to game state signals
|
|
func _connect_game_signals() -> void:
|
|
game_state.game_ended.connect(_on_game_ended)
|
|
game_state.card_played.connect(_on_card_played)
|
|
game_state.summon_cast.connect(_on_summon_cast)
|
|
game_state.damage_dealt.connect(_on_damage_dealt)
|
|
game_state.forward_broken.connect(_on_forward_broken)
|
|
game_state.attack_declared.connect(_on_attack_declared)
|
|
game_state.block_declared.connect(_on_block_declared)
|
|
game_state.combat_resolved.connect(_on_combat_resolved)
|
|
|
|
game_state.turn_manager.turn_changed.connect(_on_turn_changed)
|
|
game_state.turn_manager.phase_changed.connect(_on_phase_changed)
|
|
|
|
## Get current player
|
|
func get_current_player() -> Player:
|
|
if game_state:
|
|
return game_state.get_current_player()
|
|
return null
|
|
|
|
## Get opponent
|
|
func get_opponent() -> Player:
|
|
if game_state:
|
|
return game_state.get_opponent()
|
|
return null
|
|
|
|
## Check if it's a specific player's turn
|
|
func is_player_turn(player_index: int) -> bool:
|
|
if game_state:
|
|
return game_state.turn_manager.current_player_index == player_index
|
|
return false
|
|
|
|
## Get current phase
|
|
func get_current_phase() -> Enums.TurnPhase:
|
|
if game_state:
|
|
return game_state.turn_manager.current_phase
|
|
return Enums.TurnPhase.ACTIVE
|
|
|
|
## Try to play a card
|
|
func try_play_card(card: CardInstance) -> bool:
|
|
if not game_state or not is_game_active:
|
|
return false
|
|
|
|
var player_index = card.controller_index
|
|
if not is_player_turn(player_index):
|
|
message.emit("Not your turn!")
|
|
return false
|
|
|
|
if not game_state.turn_manager.is_main_phase():
|
|
message.emit("Can only play cards during Main Phase!")
|
|
return false
|
|
|
|
var player = game_state.get_player(player_index)
|
|
|
|
# Check if we can afford the card
|
|
if not player.cp_pool.can_afford_card(card.card_data):
|
|
message.emit("Not enough CP!")
|
|
# Enter CP generation mode
|
|
input_mode = InputMode.SELECT_CP_SOURCE
|
|
selected_card = card
|
|
return false
|
|
|
|
# Check play restrictions before attempting to play
|
|
var play_error = _check_play_restrictions(player, card)
|
|
if play_error != "":
|
|
message.emit(play_error)
|
|
return false
|
|
|
|
# Handle summons differently
|
|
if card.is_summon():
|
|
return _try_cast_summon(player_index, card)
|
|
|
|
# Determine target zone for Forwards/Backups
|
|
var to_zone = Enums.ZoneType.FIELD_FORWARDS if card.is_forward() else Enums.ZoneType.FIELD_BACKUPS
|
|
|
|
# Try to play (returns CP spent dict)
|
|
var cp_spent = game_state.play_card(player_index, card)
|
|
if not cp_spent.is_empty():
|
|
# Record for undo with actual CP spent
|
|
if undo_system:
|
|
undo_system.record_play_card(player_index, card, to_zone, cp_spent)
|
|
|
|
message.emit("Played " + card.get_display_name())
|
|
return true
|
|
else:
|
|
# This shouldn't happen if _check_play_restrictions works correctly
|
|
# but provide context if it does
|
|
message.emit("Failed to play " + card.get_display_name() + " (internal error)")
|
|
return false
|
|
|
|
## Try to cast a summon
|
|
func _try_cast_summon(player_index: int, card: CardInstance) -> bool:
|
|
if game_state.cast_summon(player_index, card):
|
|
# Note: Summons cannot be undone as they have immediate effects
|
|
message.emit("Cast " + card.get_display_name() + "!")
|
|
# TODO: Implement effect resolution system
|
|
# For now, summons just go to break zone with no effect
|
|
return true
|
|
else:
|
|
message.emit("Failed to cast " + card.get_display_name() + " (internal error)")
|
|
return false
|
|
|
|
## Check play restrictions and return error message, or empty string if playable
|
|
func _check_play_restrictions(player: Player, card: CardInstance) -> String:
|
|
# Check if card is in hand
|
|
if not player.hand.has_card(card):
|
|
return "Card is no longer in your hand!"
|
|
|
|
# Check backup limit
|
|
if card.is_backup():
|
|
if player.field_backups.get_count() >= Player.MAX_BACKUPS:
|
|
return "Cannot play: Maximum 5 Backups allowed!"
|
|
|
|
# Check unique name restriction (non-generic cards can't share names across entire field)
|
|
if not card.card_data.is_generic:
|
|
if player.field_forwards.has_card_with_name(card.card_data.name):
|
|
return "Cannot play: You already have " + card.card_data.name + " on the field!"
|
|
if player.field_backups.has_card_with_name(card.card_data.name):
|
|
return "Cannot play: You already have " + card.card_data.name + " on the field!"
|
|
|
|
# Check Light/Dark restriction
|
|
if card.is_light_or_dark():
|
|
if player.field_forwards.has_light_or_dark() or player.field_backups.has_light_or_dark():
|
|
return "Cannot play: You can only have one Light/Dark card on the field!"
|
|
|
|
return ""
|
|
|
|
## Discard a card to generate CP
|
|
func discard_card_for_cp(card: CardInstance) -> bool:
|
|
if not game_state or not is_game_active:
|
|
return false
|
|
|
|
# Check Light/Dark restriction before attempting
|
|
if card.is_light_or_dark():
|
|
message.emit("Cannot discard Light/Dark cards for CP!")
|
|
return false
|
|
|
|
var player_index = card.owner_index
|
|
var element = card.get_element()
|
|
|
|
if game_state.discard_for_cp(player_index, card):
|
|
# Record for undo
|
|
if undo_system:
|
|
undo_system.record_discard_for_cp(player_index, card, element)
|
|
|
|
message.emit("Discarded " + card.get_display_name() + " for 2 CP")
|
|
_check_pending_action()
|
|
return true
|
|
|
|
return false
|
|
|
|
## Dull a backup to generate CP
|
|
func dull_backup_for_cp(card: CardInstance) -> bool:
|
|
if not game_state or not is_game_active:
|
|
return false
|
|
|
|
var player_index = card.controller_index
|
|
var element = card.get_element()
|
|
|
|
if game_state.dull_backup_for_cp(player_index, card):
|
|
# Record for undo
|
|
if undo_system:
|
|
undo_system.record_dull_backup_for_cp(player_index, card, element)
|
|
|
|
message.emit("Dulled " + card.get_display_name() + " for 1 CP")
|
|
_check_pending_action()
|
|
return true
|
|
|
|
return false
|
|
|
|
## Check if we can now afford the pending card
|
|
func _check_pending_action() -> void:
|
|
if input_mode == InputMode.SELECT_CP_SOURCE and selected_card:
|
|
var player = game_state.get_player(selected_card.controller_index)
|
|
|
|
# Check if the card is still in hand (it may have been discarded for CP)
|
|
if not player.hand.has_card(selected_card):
|
|
message.emit("The card you wanted to play was discarded!")
|
|
clear_selection()
|
|
return
|
|
|
|
if player.cp_pool.can_afford_card(selected_card.card_data):
|
|
# Per FF-TCG rules: CP is generated to pay for a specific card
|
|
# Auto-play when we have enough CP
|
|
var card_to_play = selected_card
|
|
clear_selection() # Clear first to avoid re-entry
|
|
try_play_card(card_to_play)
|
|
|
|
## Declare an attack
|
|
func declare_attack(card: CardInstance) -> bool:
|
|
if not game_state or not is_game_active:
|
|
return false
|
|
|
|
if game_state.declare_attack(card):
|
|
input_mode = InputMode.SELECT_BLOCKER
|
|
return true
|
|
|
|
return false
|
|
|
|
## Declare a block
|
|
func declare_block(card: CardInstance) -> bool:
|
|
if not game_state or not is_game_active:
|
|
return false
|
|
|
|
if game_state.declare_block(card):
|
|
game_state.resolve_combat()
|
|
input_mode = InputMode.SELECT_ATTACKER
|
|
return true
|
|
|
|
return false
|
|
|
|
## Skip blocking
|
|
func skip_block() -> bool:
|
|
if not game_state or not is_game_active:
|
|
return false
|
|
|
|
if game_state.skip_block():
|
|
game_state.resolve_combat()
|
|
input_mode = InputMode.SELECT_ATTACKER
|
|
return true
|
|
|
|
return false
|
|
|
|
## Pass priority / end current phase
|
|
func pass_priority() -> void:
|
|
if not game_state or not is_game_active:
|
|
return
|
|
|
|
var phase = game_state.turn_manager.current_phase
|
|
|
|
match phase:
|
|
Enums.TurnPhase.MAIN_1, Enums.TurnPhase.MAIN_2:
|
|
game_state.end_main_phase()
|
|
Enums.TurnPhase.ATTACK:
|
|
if game_state.turn_manager.attack_step == Enums.AttackStep.DECLARATION:
|
|
game_state.end_attack_phase()
|
|
elif game_state.turn_manager.attack_step == Enums.AttackStep.BLOCK_DECLARATION:
|
|
skip_block()
|
|
|
|
clear_selection()
|
|
|
|
## Clear current selection
|
|
func clear_selection() -> void:
|
|
input_mode = InputMode.NONE
|
|
selected_card = null
|
|
pending_action = func(): pass
|
|
|
|
## Undo the last action
|
|
func undo_last_action() -> bool:
|
|
if not undo_system:
|
|
return false
|
|
|
|
return undo_system.undo()
|
|
|
|
## Check if undo is available
|
|
func can_undo() -> bool:
|
|
if not undo_system:
|
|
return false
|
|
return undo_system.can_undo()
|
|
|
|
## Get description of last undoable action
|
|
func get_undo_description() -> String:
|
|
if not undo_system:
|
|
return ""
|
|
return undo_system.get_last_action_description()
|
|
|
|
## Restore the input mode based on the current phase
|
|
func restore_input_mode_for_phase() -> void:
|
|
if not game_state:
|
|
return
|
|
|
|
var phase = game_state.turn_manager.current_phase
|
|
match phase:
|
|
Enums.TurnPhase.MAIN_1, Enums.TurnPhase.MAIN_2:
|
|
input_mode = InputMode.SELECT_CARD_TO_PLAY
|
|
Enums.TurnPhase.ATTACK:
|
|
input_mode = InputMode.SELECT_ATTACKER
|
|
_:
|
|
input_mode = InputMode.NONE
|
|
|
|
## Signal handlers
|
|
|
|
func _on_undo_available_changed(available: bool) -> void:
|
|
undo_available_changed.emit(available)
|
|
|
|
func _on_action_undone(action_name: String) -> void:
|
|
message.emit("Undid: " + action_name)
|
|
action_undone.emit(action_name)
|
|
|
|
func _on_game_ended(winner_index: int) -> void:
|
|
is_game_active = false
|
|
var winner_name = game_state.get_player(winner_index).player_name
|
|
game_ended.emit(winner_name)
|
|
message.emit(winner_name + " wins!")
|
|
|
|
func _on_card_played(card: CardInstance, _player_index: int) -> void:
|
|
card_played.emit({
|
|
"name": card.get_display_name(),
|
|
"type": Enums.card_type_to_string(card.card_data.type)
|
|
})
|
|
|
|
func _on_summon_cast(card: CardInstance, _player_index: int) -> void:
|
|
card_played.emit({
|
|
"name": card.get_display_name(),
|
|
"type": "Summon"
|
|
})
|
|
|
|
func _on_damage_dealt(player_index: int, amount: int, _cards: Array) -> void:
|
|
var player_name = game_state.get_player(player_index).player_name
|
|
damage_dealt.emit(player_name, amount)
|
|
message.emit(player_name + " takes " + str(amount) + " damage!")
|
|
|
|
func _on_forward_broken(card: CardInstance) -> void:
|
|
message.emit(card.get_display_name() + " was broken!")
|
|
|
|
func _on_attack_declared(attacker: CardInstance) -> void:
|
|
message.emit(attacker.get_display_name() + " attacks!")
|
|
|
|
func _on_block_declared(blocker: CardInstance) -> void:
|
|
message.emit(blocker.get_display_name() + " blocks!")
|
|
|
|
func _on_combat_resolved(_attacker: CardInstance, blocker: CardInstance) -> void:
|
|
if blocker == null:
|
|
message.emit("Attack hits!")
|
|
else:
|
|
message.emit("Combat resolved!")
|
|
|
|
func _on_turn_changed(player_index: int) -> void:
|
|
var player = game_state.get_player(player_index)
|
|
turn_changed.emit(player.player_name, game_state.turn_manager.turn_number)
|
|
message.emit(player.player_name + "'s turn")
|
|
|
|
func _on_phase_changed(phase: Enums.TurnPhase) -> void:
|
|
var phase_name = Enums.phase_to_string(phase)
|
|
phase_changed.emit(phase_name)
|
|
|
|
# Clear undo history on phase change
|
|
if undo_system:
|
|
undo_system.clear_history()
|
|
|
|
# Set appropriate input mode
|
|
match phase:
|
|
Enums.TurnPhase.MAIN_1, Enums.TurnPhase.MAIN_2:
|
|
input_mode = InputMode.SELECT_CARD_TO_PLAY
|
|
Enums.TurnPhase.ATTACK:
|
|
input_mode = InputMode.SELECT_ATTACKER
|
|
_:
|
|
input_mode = InputMode.NONE
|