init game files
This commit is contained in:
107
scripts/GameController.gd
Normal file
107
scripts/GameController.gd
Normal file
@@ -0,0 +1,107 @@
|
||||
extends Node
|
||||
|
||||
## GameController - Top-level controller managing menu and game states
|
||||
|
||||
enum State {
|
||||
MENU,
|
||||
PLAYING,
|
||||
PAUSED
|
||||
}
|
||||
|
||||
var current_state: State = State.MENU
|
||||
|
||||
# Scene references
|
||||
var main_menu: MainMenu = null
|
||||
var game_scene: Node3D = null
|
||||
var pause_menu: PauseMenu = null
|
||||
|
||||
# Preload the main game scene script
|
||||
const MainScript = preload("res://scripts/Main.gd")
|
||||
|
||||
func _ready() -> void:
|
||||
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
_show_main_menu()
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed:
|
||||
if event.keycode == KEY_ESCAPE:
|
||||
match current_state:
|
||||
State.PLAYING:
|
||||
_show_pause_menu()
|
||||
State.PAUSED:
|
||||
_hide_pause_menu()
|
||||
|
||||
func _show_main_menu() -> void:
|
||||
# Clean up any existing game
|
||||
if game_scene:
|
||||
game_scene.queue_free()
|
||||
game_scene = null
|
||||
|
||||
if pause_menu:
|
||||
pause_menu.queue_free()
|
||||
pause_menu = null
|
||||
|
||||
# Create main menu
|
||||
if not main_menu:
|
||||
main_menu = MainMenu.new()
|
||||
add_child(main_menu)
|
||||
main_menu.start_game.connect(_on_start_game)
|
||||
|
||||
main_menu.visible = true
|
||||
current_state = State.MENU
|
||||
|
||||
func _on_start_game() -> void:
|
||||
# Hide menu
|
||||
if main_menu:
|
||||
main_menu.visible = false
|
||||
|
||||
# Create game scene
|
||||
_start_new_game()
|
||||
|
||||
func _start_new_game() -> void:
|
||||
# Make sure game isn't paused
|
||||
get_tree().paused = false
|
||||
|
||||
# Clean up existing game
|
||||
if game_scene:
|
||||
game_scene.queue_free()
|
||||
game_scene = null
|
||||
|
||||
# Reset GameManager state
|
||||
if GameManager:
|
||||
GameManager.is_game_active = false
|
||||
GameManager.game_state = null
|
||||
|
||||
# Create new game scene
|
||||
game_scene = Node3D.new()
|
||||
game_scene.set_script(MainScript)
|
||||
add_child(game_scene)
|
||||
|
||||
# Create pause menu
|
||||
if not pause_menu:
|
||||
pause_menu = PauseMenu.new()
|
||||
add_child(pause_menu)
|
||||
pause_menu.resume_game.connect(_on_resume_game)
|
||||
pause_menu.restart_game.connect(_on_restart_game)
|
||||
pause_menu.return_to_menu.connect(_on_return_to_menu)
|
||||
|
||||
current_state = State.PLAYING
|
||||
|
||||
func _show_pause_menu() -> void:
|
||||
if pause_menu:
|
||||
pause_menu.show_menu()
|
||||
current_state = State.PAUSED
|
||||
|
||||
func _hide_pause_menu() -> void:
|
||||
if pause_menu:
|
||||
pause_menu.hide_menu()
|
||||
current_state = State.PLAYING
|
||||
|
||||
func _on_resume_game() -> void:
|
||||
current_state = State.PLAYING
|
||||
|
||||
func _on_restart_game() -> void:
|
||||
_start_new_game()
|
||||
|
||||
func _on_return_to_menu() -> void:
|
||||
_show_main_menu()
|
||||
230
scripts/Main.gd
Normal file
230
scripts/Main.gd
Normal file
@@ -0,0 +1,230 @@
|
||||
extends Node3D
|
||||
|
||||
## Main - Root scene that coordinates the game
|
||||
|
||||
# Components
|
||||
var table_setup: TableSetup
|
||||
var game_ui: GameUI
|
||||
var hand_display: HandDisplay
|
||||
var hand_layer: CanvasLayer
|
||||
|
||||
# Player damage displays
|
||||
var damage_displays: Array[DamageDisplay] = []
|
||||
|
||||
func _ready() -> void:
|
||||
_setup_table()
|
||||
_setup_ui()
|
||||
_connect_signals()
|
||||
|
||||
# Start game when ready
|
||||
if GameManager.is_initialized:
|
||||
_start_game()
|
||||
else:
|
||||
GameManager.game_ready.connect(_start_game)
|
||||
|
||||
func _setup_table() -> void:
|
||||
table_setup = TableSetup.new()
|
||||
add_child(table_setup)
|
||||
|
||||
# Connect table signals
|
||||
table_setup.card_clicked.connect(_on_table_card_clicked)
|
||||
|
||||
func _setup_ui() -> void:
|
||||
# Main game UI overlay (has its own CanvasLayer)
|
||||
game_ui = GameUI.new()
|
||||
game_ui.layer = 10 # Base UI layer
|
||||
add_child(game_ui)
|
||||
|
||||
# Hand display needs its own CanvasLayer to render on top of 3D
|
||||
hand_layer = CanvasLayer.new()
|
||||
hand_layer.layer = 11 # Above the game UI
|
||||
add_child(hand_layer)
|
||||
|
||||
# Container for hand at bottom of screen - must fill the viewport
|
||||
var hand_container = Control.new()
|
||||
hand_layer.add_child(hand_container)
|
||||
hand_container.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
hand_container.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
# Hand display positioned at bottom - use explicit positioning
|
||||
hand_display = HandDisplay.new()
|
||||
hand_container.add_child(hand_display)
|
||||
|
||||
# Position at bottom of screen with explicit coordinates
|
||||
# We'll update position in _process or use a deferred call after container is sized
|
||||
call_deferred("_position_hand_display")
|
||||
|
||||
hand_display.card_selected.connect(_on_hand_card_selected)
|
||||
hand_display.card_hovered.connect(_on_hand_card_hovered)
|
||||
hand_display.card_unhovered.connect(_on_hand_card_unhovered)
|
||||
|
||||
# Create damage displays (positioned by 3D camera overlay later)
|
||||
for i in range(2):
|
||||
var damage_display = DamageDisplay.new()
|
||||
damage_displays.append(damage_display)
|
||||
|
||||
func _position_hand_display() -> void:
|
||||
# Get viewport size and position hand at bottom
|
||||
var viewport = get_viewport()
|
||||
if viewport:
|
||||
var vp_size = viewport.get_visible_rect().size
|
||||
hand_display.position = Vector2(50, vp_size.y - 180)
|
||||
hand_display.size = Vector2(vp_size.x - 100, 170)
|
||||
|
||||
func _connect_signals() -> void:
|
||||
# GameManager signals
|
||||
GameManager.game_started.connect(_on_game_started)
|
||||
GameManager.game_ended.connect(_on_game_ended)
|
||||
GameManager.turn_changed.connect(_on_turn_changed)
|
||||
GameManager.phase_changed.connect(_on_phase_changed)
|
||||
GameManager.damage_dealt.connect(_on_damage_dealt)
|
||||
|
||||
func _start_game() -> void:
|
||||
GameManager.start_new_game()
|
||||
# Force an update of visuals after a frame to ensure everything is ready
|
||||
call_deferred("_force_initial_update")
|
||||
|
||||
func _force_initial_update() -> void:
|
||||
_sync_visuals()
|
||||
_update_hand_display()
|
||||
_update_cp_display()
|
||||
|
||||
func _on_game_started() -> void:
|
||||
_sync_visuals()
|
||||
_update_hand_display()
|
||||
|
||||
func _on_game_ended(winner_name: String) -> void:
|
||||
game_ui.show_message(winner_name + " wins the game!")
|
||||
|
||||
func _on_turn_changed(_player_name: String, _turn_number: int) -> void:
|
||||
_sync_visuals()
|
||||
_update_hand_display()
|
||||
_update_cp_display()
|
||||
|
||||
func _on_phase_changed(_phase_name: String) -> void:
|
||||
_update_playable_highlights()
|
||||
_update_cp_display()
|
||||
|
||||
func _on_damage_dealt(player_name: String, _amount: int) -> void:
|
||||
# Find player index
|
||||
if GameManager.game_state:
|
||||
for i in range(2):
|
||||
var player = GameManager.game_state.get_player(i)
|
||||
if player and player.player_name == player_name:
|
||||
if i < damage_displays.size():
|
||||
damage_displays[i].set_damage(player.damage_zone.get_count())
|
||||
|
||||
func _sync_visuals() -> void:
|
||||
if GameManager.game_state and table_setup:
|
||||
table_setup.sync_with_game_state(GameManager.game_state)
|
||||
|
||||
func _update_hand_display() -> void:
|
||||
if not GameManager.game_state:
|
||||
return
|
||||
|
||||
# Show current player's hand
|
||||
var current_player = GameManager.get_current_player()
|
||||
if current_player:
|
||||
var cards = current_player.hand.get_cards()
|
||||
hand_display.update_hand(cards)
|
||||
|
||||
func _update_cp_display() -> void:
|
||||
if not GameManager.game_state:
|
||||
return
|
||||
|
||||
var current_player = GameManager.get_current_player()
|
||||
if current_player:
|
||||
game_ui.update_cp_display(current_player.cp_pool)
|
||||
|
||||
func _update_playable_highlights() -> void:
|
||||
if not GameManager.game_state:
|
||||
return
|
||||
|
||||
var phase = GameManager.get_current_phase()
|
||||
var player = GameManager.get_current_player()
|
||||
|
||||
if not player:
|
||||
hand_display.clear_highlights()
|
||||
return
|
||||
|
||||
match phase:
|
||||
Enums.TurnPhase.MAIN_1, Enums.TurnPhase.MAIN_2:
|
||||
# Highlight cards that can be played
|
||||
hand_display.highlight_playable(func(card: CardInstance) -> bool:
|
||||
return player.cp_pool.can_afford_card(card.card_data)
|
||||
)
|
||||
_:
|
||||
hand_display.clear_highlights()
|
||||
|
||||
func _on_hand_card_selected(card: CardInstance) -> void:
|
||||
print("Main: Card selected: ", card.card_data.name, " input_mode=", GameManager.input_mode)
|
||||
var input_mode = GameManager.input_mode
|
||||
|
||||
match input_mode:
|
||||
GameManager.InputMode.SELECT_CARD_TO_PLAY:
|
||||
print("Main: Trying to play card")
|
||||
GameManager.try_play_card(card)
|
||||
_sync_visuals()
|
||||
_update_hand_display()
|
||||
_update_cp_display()
|
||||
|
||||
GameManager.InputMode.SELECT_CP_SOURCE:
|
||||
# Discard for CP
|
||||
print("Main: Discarding for CP")
|
||||
GameManager.discard_card_for_cp(card)
|
||||
_update_hand_display()
|
||||
_update_cp_display()
|
||||
_:
|
||||
print("Main: Input mode not handled: ", input_mode)
|
||||
|
||||
func _on_hand_card_hovered(card: CardInstance) -> void:
|
||||
game_ui.show_card_detail(card)
|
||||
|
||||
func _on_hand_card_unhovered() -> void:
|
||||
game_ui.hide_card_detail()
|
||||
|
||||
func _on_table_card_clicked(card: CardInstance, zone_type: Enums.ZoneType, player_index: int) -> void:
|
||||
var input_mode = GameManager.input_mode
|
||||
|
||||
match input_mode:
|
||||
GameManager.InputMode.SELECT_CP_SOURCE:
|
||||
# Check if it's a backup we can dull
|
||||
if zone_type == Enums.ZoneType.FIELD_BACKUPS:
|
||||
if player_index == GameManager.game_state.turn_manager.current_player_index:
|
||||
GameManager.dull_backup_for_cp(card)
|
||||
_sync_visuals()
|
||||
_update_cp_display()
|
||||
|
||||
GameManager.InputMode.SELECT_ATTACKER:
|
||||
# Select attacker
|
||||
if zone_type == Enums.ZoneType.FIELD_FORWARDS:
|
||||
if player_index == GameManager.game_state.turn_manager.current_player_index:
|
||||
GameManager.declare_attack(card)
|
||||
_sync_visuals()
|
||||
|
||||
GameManager.InputMode.SELECT_BLOCKER:
|
||||
# Select blocker
|
||||
if zone_type == Enums.ZoneType.FIELD_FORWARDS:
|
||||
var opponent_index = 1 - GameManager.game_state.turn_manager.current_player_index
|
||||
if player_index == opponent_index:
|
||||
GameManager.declare_block(card)
|
||||
_sync_visuals()
|
||||
|
||||
# Show card detail on any click
|
||||
game_ui.show_card_detail(card)
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
# Keyboard shortcuts
|
||||
if event is InputEventKey and event.pressed:
|
||||
match event.keycode:
|
||||
KEY_SPACE:
|
||||
# Pass priority / end phase
|
||||
GameManager.pass_priority()
|
||||
_sync_visuals()
|
||||
_update_hand_display()
|
||||
_update_cp_display()
|
||||
KEY_ESCAPE:
|
||||
# Cancel current selection
|
||||
GameManager.clear_selection()
|
||||
table_setup.clear_all_highlights()
|
||||
hand_display.clear_highlights()
|
||||
260
scripts/autoload/CardDatabase.gd
Normal file
260
scripts/autoload/CardDatabase.gd
Normal file
@@ -0,0 +1,260 @@
|
||||
extends Node
|
||||
|
||||
## CardDatabase - Singleton for managing card data
|
||||
## Loads card definitions from JSON and provides lookup methods
|
||||
|
||||
const CARDS_PATH = "res://data/cards.json"
|
||||
|
||||
# Loaded card data
|
||||
var _cards: Dictionary = {} # id -> CardData
|
||||
var _cards_by_element: Dictionary = {} # Element -> Array[CardData]
|
||||
var _cards_by_type: Dictionary = {} # CardType -> Array[CardData]
|
||||
var _card_textures: Dictionary = {} # id -> Texture2D
|
||||
|
||||
# Signals
|
||||
signal database_loaded
|
||||
signal load_error(message: String)
|
||||
|
||||
func _ready() -> void:
|
||||
_load_database()
|
||||
|
||||
func _load_database() -> void:
|
||||
var file = FileAccess.open(CARDS_PATH, FileAccess.READ)
|
||||
if not file:
|
||||
push_error("Failed to open cards database: " + CARDS_PATH)
|
||||
load_error.emit("Failed to open cards database")
|
||||
return
|
||||
|
||||
var json_text = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json = JSON.new()
|
||||
var error = json.parse(json_text)
|
||||
if error != OK:
|
||||
push_error("Failed to parse cards JSON: " + json.get_error_message())
|
||||
load_error.emit("Failed to parse cards JSON")
|
||||
return
|
||||
|
||||
var data = json.get_data()
|
||||
if not data.has("cards"):
|
||||
push_error("Cards database missing 'cards' array")
|
||||
load_error.emit("Invalid database format")
|
||||
return
|
||||
|
||||
# Initialize element and type dictionaries
|
||||
for element in Enums.Element.values():
|
||||
_cards_by_element[element] = []
|
||||
for card_type in Enums.CardType.values():
|
||||
_cards_by_type[card_type] = []
|
||||
|
||||
# Parse cards
|
||||
for card_data in data["cards"]:
|
||||
var card = _parse_card(card_data)
|
||||
if card:
|
||||
_cards[card.id] = card
|
||||
|
||||
# Index by element(s)
|
||||
for element in card.elements:
|
||||
_cards_by_element[element].append(card)
|
||||
|
||||
# Index by type
|
||||
_cards_by_type[card.type].append(card)
|
||||
|
||||
print("CardDatabase: Loaded ", _cards.size(), " cards")
|
||||
database_loaded.emit()
|
||||
|
||||
func _parse_card(data: Dictionary) -> CardData:
|
||||
var card = CardData.new()
|
||||
|
||||
# Required fields
|
||||
if not data.has("id") or not data.has("name") or not data.has("type") or not data.has("element") or not data.has("cost"):
|
||||
push_error("Card missing required fields: " + str(data))
|
||||
return null
|
||||
|
||||
card.id = data["id"]
|
||||
card.name = data["name"]
|
||||
card.type = Enums.card_type_from_string(data["type"])
|
||||
card.cost = data["cost"]
|
||||
|
||||
# Parse element (can be string or array)
|
||||
if data["element"] is Array:
|
||||
for elem_str in data["element"]:
|
||||
card.elements.append(Enums.element_from_string(elem_str))
|
||||
else:
|
||||
card.elements.append(Enums.element_from_string(data["element"]))
|
||||
|
||||
# Optional fields
|
||||
card.power = data.get("power", 0) if data.get("power") != null else 0
|
||||
card.job = data.get("job", "") if data.get("job") != null else ""
|
||||
card.category = data.get("category", "") if data.get("category") != null else ""
|
||||
card.is_generic = data.get("is_generic", false) if data.get("is_generic") != null else false
|
||||
card.has_ex_burst = data.get("has_ex_burst", false) if data.get("has_ex_burst") != null else false
|
||||
card.image_path = data.get("image", "") if data.get("image") != null else ""
|
||||
|
||||
# Parse abilities
|
||||
if data.has("abilities"):
|
||||
for ability_data in data["abilities"]:
|
||||
var ability = _parse_ability(ability_data)
|
||||
if ability:
|
||||
card.abilities.append(ability)
|
||||
|
||||
return card
|
||||
|
||||
func _parse_ability(data: Dictionary) -> AbilityData:
|
||||
var ability = AbilityData.new()
|
||||
|
||||
if not data.has("type") or not data.has("effect"):
|
||||
push_error("Ability missing required fields")
|
||||
return null
|
||||
|
||||
match data["type"].to_lower():
|
||||
"field": ability.type = Enums.AbilityType.FIELD
|
||||
"auto": ability.type = Enums.AbilityType.AUTO
|
||||
"action": ability.type = Enums.AbilityType.ACTION
|
||||
"special": ability.type = Enums.AbilityType.SPECIAL
|
||||
|
||||
ability.effect = data["effect"]
|
||||
ability.name = data.get("name", "")
|
||||
ability.trigger = data.get("trigger", "")
|
||||
ability.is_ex_burst = data.get("is_ex_burst", false)
|
||||
|
||||
# Parse cost if present
|
||||
if data.has("cost"):
|
||||
ability.cost = _parse_cost(data["cost"])
|
||||
|
||||
return ability
|
||||
|
||||
func _parse_cost(data: Dictionary) -> CostData:
|
||||
var cost = CostData.new()
|
||||
|
||||
cost.generic = data.get("generic", 0)
|
||||
cost.fire = data.get("fire", 0)
|
||||
cost.ice = data.get("ice", 0)
|
||||
cost.wind = data.get("wind", 0)
|
||||
cost.lightning = data.get("lightning", 0)
|
||||
cost.water = data.get("water", 0)
|
||||
cost.earth = data.get("earth", 0)
|
||||
cost.light = data.get("light", 0)
|
||||
cost.dark = data.get("dark", 0)
|
||||
cost.requires_dull = data.get("dull", false)
|
||||
cost.discard_count = data.get("discard", 0)
|
||||
cost.specific_discard = data.get("specific_discard", "")
|
||||
|
||||
return cost
|
||||
|
||||
## Get a card by ID
|
||||
func get_card(id: String) -> CardData:
|
||||
return _cards.get(id)
|
||||
|
||||
## Get all cards
|
||||
func get_all_cards() -> Array:
|
||||
return _cards.values()
|
||||
|
||||
## Get cards by element
|
||||
func get_cards_by_element(element: Enums.Element) -> Array:
|
||||
return _cards_by_element.get(element, [])
|
||||
|
||||
## Get cards by type
|
||||
func get_cards_by_type(card_type: Enums.CardType) -> Array:
|
||||
return _cards_by_type.get(card_type, [])
|
||||
|
||||
## Get or load a card texture
|
||||
func get_card_texture(card: CardData) -> Texture2D:
|
||||
if card.id in _card_textures:
|
||||
return _card_textures[card.id]
|
||||
|
||||
if card.image_path.is_empty():
|
||||
return null
|
||||
|
||||
var texture_path = "res://assets/cards/" + card.image_path
|
||||
if ResourceLoader.exists(texture_path):
|
||||
var texture = load(texture_path)
|
||||
_card_textures[card.id] = texture
|
||||
return texture
|
||||
|
||||
return null
|
||||
|
||||
## Create a list of card IDs for a deck (for testing)
|
||||
func create_test_deck(_player_index: int) -> Array[String]:
|
||||
var deck: Array[String] = []
|
||||
|
||||
# Get all cards and create a 50-card deck
|
||||
var all_cards = get_all_cards()
|
||||
|
||||
# Add 3 copies of each card until we have 50
|
||||
for card in all_cards:
|
||||
if deck.size() >= 50:
|
||||
break
|
||||
# Add up to 3 copies
|
||||
for i in range(3):
|
||||
if deck.size() >= 50:
|
||||
break
|
||||
deck.append(card.id)
|
||||
|
||||
# Fill remaining slots if needed
|
||||
while deck.size() < 50 and all_cards.size() > 0:
|
||||
deck.append(all_cards[0].id)
|
||||
|
||||
return deck
|
||||
|
||||
|
||||
## Data Classes
|
||||
|
||||
class CardData:
|
||||
var id: String = ""
|
||||
var name: String = ""
|
||||
var type: Enums.CardType = Enums.CardType.FORWARD
|
||||
var elements: Array[Enums.Element] = []
|
||||
var cost: int = 0
|
||||
var power: int = 0
|
||||
var job: String = ""
|
||||
var category: String = ""
|
||||
var is_generic: bool = false
|
||||
var has_ex_burst: bool = false
|
||||
var image_path: String = ""
|
||||
var abilities: Array[AbilityData] = []
|
||||
|
||||
func get_primary_element() -> Enums.Element:
|
||||
if elements.size() > 0:
|
||||
return elements[0]
|
||||
return Enums.Element.FIRE
|
||||
|
||||
func is_multi_element() -> bool:
|
||||
return elements.size() > 1
|
||||
|
||||
class AbilityData:
|
||||
var type: Enums.AbilityType = Enums.AbilityType.FIELD
|
||||
var name: String = ""
|
||||
var effect: String = ""
|
||||
var trigger: String = ""
|
||||
var is_ex_burst: bool = false
|
||||
var cost: CostData = null
|
||||
|
||||
class CostData:
|
||||
var generic: int = 0
|
||||
var fire: int = 0
|
||||
var ice: int = 0
|
||||
var wind: int = 0
|
||||
var lightning: int = 0
|
||||
var water: int = 0
|
||||
var earth: int = 0
|
||||
var light: int = 0
|
||||
var dark: int = 0
|
||||
var requires_dull: bool = false
|
||||
var discard_count: int = 0
|
||||
var specific_discard: String = ""
|
||||
|
||||
func get_total_cp() -> int:
|
||||
return generic + fire + ice + wind + lightning + water + earth + light + dark
|
||||
|
||||
func get_element_requirements() -> Dictionary:
|
||||
var reqs = {}
|
||||
if fire > 0: reqs[Enums.Element.FIRE] = fire
|
||||
if ice > 0: reqs[Enums.Element.ICE] = ice
|
||||
if wind > 0: reqs[Enums.Element.WIND] = wind
|
||||
if lightning > 0: reqs[Enums.Element.LIGHTNING] = lightning
|
||||
if water > 0: reqs[Enums.Element.WATER] = water
|
||||
if earth > 0: reqs[Enums.Element.EARTH] = earth
|
||||
if light > 0: reqs[Enums.Element.LIGHT] = light
|
||||
if dark > 0: reqs[Enums.Element.DARK] = dark
|
||||
return reqs
|
||||
286
scripts/autoload/GameManager.gd
Normal file
286
scripts/autoload/GameManager.gd
Normal file
@@ -0,0 +1,286 @@
|
||||
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)
|
||||
|
||||
# Game state
|
||||
var game_state: GameState = 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
|
||||
func start_new_game() -> void:
|
||||
if not is_initialized:
|
||||
push_error("GameManager not initialized")
|
||||
return
|
||||
|
||||
# Create new game state
|
||||
game_state = GameState.new()
|
||||
|
||||
# Connect signals
|
||||
_connect_game_signals()
|
||||
|
||||
# Create test decks
|
||||
var deck1 = CardDatabase.create_test_deck(0)
|
||||
var deck2 = CardDatabase.create_test_deck(1)
|
||||
|
||||
# Setup and start
|
||||
game_state.setup_game(deck1, deck2)
|
||||
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.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
|
||||
|
||||
# Try to play
|
||||
if game_state.play_card(player_index, card):
|
||||
message.emit("Played " + card.get_display_name())
|
||||
return true
|
||||
else:
|
||||
message.emit("Cannot play that card!")
|
||||
return false
|
||||
|
||||
## 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
|
||||
|
||||
var player_index = card.owner_index
|
||||
if game_state.discard_for_cp(player_index, card):
|
||||
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
|
||||
if game_state.dull_backup_for_cp(player_index, card):
|
||||
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)
|
||||
if player.cp_pool.can_afford_card(selected_card.card_data):
|
||||
# Can now afford - try to play
|
||||
try_play_card(selected_card)
|
||||
clear_selection()
|
||||
|
||||
## 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
|
||||
|
||||
## Signal handlers
|
||||
|
||||
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_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)
|
||||
|
||||
# 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
|
||||
178
scripts/game/CPPool.gd
Normal file
178
scripts/game/CPPool.gd
Normal 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) + "]"
|
||||
193
scripts/game/CardInstance.gd
Normal file
193
scripts/game/CardInstance.gd
Normal 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
141
scripts/game/Enums.gd
Normal 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
332
scripts/game/GameState.gd
Normal 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
263
scripts/game/Player.gd
Normal 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
150
scripts/game/TurnManager.gd
Normal 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
158
scripts/game/Zone.gd
Normal 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
|
||||
67
scripts/ui/DamageDisplay.gd
Normal file
67
scripts/ui/DamageDisplay.gd
Normal file
@@ -0,0 +1,67 @@
|
||||
class_name DamageDisplay
|
||||
extends Control
|
||||
|
||||
## DamageDisplay - Shows player damage counters
|
||||
|
||||
# Display settings
|
||||
const MAX_DAMAGE: int = 7
|
||||
const MARKER_SIZE: float = 30.0
|
||||
const MARKER_SPACING: float = 5.0
|
||||
|
||||
# UI elements
|
||||
var markers: Array[ColorRect] = []
|
||||
var damage_label: Label
|
||||
|
||||
# Current damage
|
||||
var current_damage: int = 0
|
||||
|
||||
func _ready() -> void:
|
||||
_create_display()
|
||||
|
||||
func _create_display() -> void:
|
||||
# Container for markers
|
||||
var marker_container = HBoxContainer.new()
|
||||
add_child(marker_container)
|
||||
marker_container.add_theme_constant_override("separation", int(MARKER_SPACING))
|
||||
|
||||
# Create damage markers
|
||||
for i in range(MAX_DAMAGE):
|
||||
var marker = ColorRect.new()
|
||||
marker.custom_minimum_size = Vector2(MARKER_SIZE, MARKER_SIZE)
|
||||
marker.color = Color(0.3, 0.3, 0.3) # Empty state
|
||||
marker_container.add_child(marker)
|
||||
markers.append(marker)
|
||||
|
||||
# Damage count label
|
||||
damage_label = Label.new()
|
||||
damage_label.text = "0 / 7"
|
||||
damage_label.position = Vector2(0, MARKER_SIZE + 5)
|
||||
damage_label.add_theme_font_size_override("font_size", 14)
|
||||
add_child(damage_label)
|
||||
|
||||
## Update damage display
|
||||
func set_damage(amount: int) -> void:
|
||||
current_damage = clampi(amount, 0, MAX_DAMAGE)
|
||||
|
||||
# Update markers
|
||||
for i in range(MAX_DAMAGE):
|
||||
if i < current_damage:
|
||||
markers[i].color = Color(0.8, 0.2, 0.2) # Damage taken
|
||||
else:
|
||||
markers[i].color = Color(0.3, 0.3, 0.3) # Empty
|
||||
|
||||
# Update label
|
||||
damage_label.text = str(current_damage) + " / " + str(MAX_DAMAGE)
|
||||
|
||||
# Flash on damage change
|
||||
if current_damage > 0:
|
||||
_flash_latest()
|
||||
|
||||
func _flash_latest() -> void:
|
||||
if current_damage <= 0 or current_damage > MAX_DAMAGE:
|
||||
return
|
||||
|
||||
var marker = markers[current_damage - 1]
|
||||
var tween = create_tween()
|
||||
tween.tween_property(marker, "color", Color(1.0, 0.5, 0.5), 0.1)
|
||||
tween.tween_property(marker, "color", Color(0.8, 0.2, 0.2), 0.2)
|
||||
336
scripts/ui/GameUI.gd
Normal file
336
scripts/ui/GameUI.gd
Normal file
@@ -0,0 +1,336 @@
|
||||
class_name GameUI
|
||||
extends CanvasLayer
|
||||
|
||||
## GameUI - Main UI overlay for game information and controls
|
||||
|
||||
signal end_phase_pressed
|
||||
signal pass_priority_pressed
|
||||
|
||||
# UI Components
|
||||
var turn_panel: PanelContainer
|
||||
var phase_panel: PanelContainer
|
||||
var cp_panel: PanelContainer
|
||||
var message_panel: PanelContainer
|
||||
var card_detail_panel: PanelContainer
|
||||
var action_buttons: HBoxContainer
|
||||
|
||||
# Labels
|
||||
var turn_label: Label
|
||||
var phase_label: Label
|
||||
var cp_label: Label
|
||||
var message_label: Label
|
||||
|
||||
# Card detail labels
|
||||
var detail_name_label: Label
|
||||
var detail_type_label: Label
|
||||
var detail_cost_label: Label
|
||||
var detail_power_label: Label
|
||||
var detail_element_label: Label
|
||||
var detail_ability_label: Label
|
||||
|
||||
# Buttons
|
||||
var end_phase_button: Button
|
||||
var pass_button: Button
|
||||
|
||||
# Message queue
|
||||
var message_queue: Array[String] = []
|
||||
var message_timer: Timer
|
||||
const MESSAGE_DISPLAY_TIME: float = 3.0
|
||||
|
||||
func _ready() -> void:
|
||||
_create_ui()
|
||||
_connect_signals()
|
||||
|
||||
func _create_ui() -> void:
|
||||
# Root control that fills the screen
|
||||
var root = Control.new()
|
||||
add_child(root)
|
||||
root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
root.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
# === TOP BAR ===
|
||||
var top_bar = HBoxContainer.new()
|
||||
root.add_child(top_bar)
|
||||
top_bar.set_anchors_preset(Control.PRESET_TOP_WIDE)
|
||||
top_bar.offset_left = 10
|
||||
top_bar.offset_right = -10
|
||||
top_bar.offset_top = 10
|
||||
top_bar.offset_bottom = 70
|
||||
top_bar.add_theme_constant_override("separation", 20)
|
||||
|
||||
# Turn panel (top left)
|
||||
turn_panel = _create_panel()
|
||||
top_bar.add_child(turn_panel)
|
||||
var turn_vbox = VBoxContainer.new()
|
||||
turn_panel.add_child(turn_vbox)
|
||||
|
||||
var turn_header = Label.new()
|
||||
turn_header.text = "Turn"
|
||||
turn_header.add_theme_font_size_override("font_size", 12)
|
||||
turn_vbox.add_child(turn_header)
|
||||
|
||||
turn_label = Label.new()
|
||||
turn_label.text = "Player 1 - Turn 1"
|
||||
turn_label.add_theme_font_size_override("font_size", 16)
|
||||
turn_vbox.add_child(turn_label)
|
||||
|
||||
# Phase panel (top center-left)
|
||||
phase_panel = _create_panel()
|
||||
top_bar.add_child(phase_panel)
|
||||
var phase_vbox = VBoxContainer.new()
|
||||
phase_panel.add_child(phase_vbox)
|
||||
|
||||
var phase_header = Label.new()
|
||||
phase_header.text = "Phase"
|
||||
phase_header.add_theme_font_size_override("font_size", 12)
|
||||
phase_vbox.add_child(phase_header)
|
||||
|
||||
phase_label = Label.new()
|
||||
phase_label.text = "Active Phase"
|
||||
phase_label.add_theme_font_size_override("font_size", 16)
|
||||
phase_vbox.add_child(phase_label)
|
||||
|
||||
# CP panel
|
||||
cp_panel = _create_panel()
|
||||
top_bar.add_child(cp_panel)
|
||||
var cp_vbox = VBoxContainer.new()
|
||||
cp_panel.add_child(cp_vbox)
|
||||
|
||||
var cp_header = Label.new()
|
||||
cp_header.text = "CP Pool"
|
||||
cp_header.add_theme_font_size_override("font_size", 12)
|
||||
cp_vbox.add_child(cp_header)
|
||||
|
||||
cp_label = Label.new()
|
||||
cp_label.text = "0 CP"
|
||||
cp_label.add_theme_font_size_override("font_size", 16)
|
||||
cp_vbox.add_child(cp_label)
|
||||
|
||||
# Spacer to push buttons to the right
|
||||
var spacer = Control.new()
|
||||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
top_bar.add_child(spacer)
|
||||
|
||||
# Action buttons in top bar (right side)
|
||||
action_buttons = HBoxContainer.new()
|
||||
top_bar.add_child(action_buttons)
|
||||
action_buttons.add_theme_constant_override("separation", 10)
|
||||
|
||||
pass_button = Button.new()
|
||||
pass_button.text = "Pass"
|
||||
pass_button.custom_minimum_size = Vector2(80, 40)
|
||||
action_buttons.add_child(pass_button)
|
||||
|
||||
end_phase_button = Button.new()
|
||||
end_phase_button.text = "End Phase"
|
||||
end_phase_button.custom_minimum_size = Vector2(100, 40)
|
||||
action_buttons.add_child(end_phase_button)
|
||||
|
||||
# === MESSAGE PANEL (center, above hand) ===
|
||||
message_panel = _create_panel()
|
||||
root.add_child(message_panel)
|
||||
message_panel.set_anchors_preset(Control.PRESET_CENTER)
|
||||
message_panel.position = Vector2(-100, 200)
|
||||
message_panel.custom_minimum_size = Vector2(200, 40)
|
||||
|
||||
message_label = Label.new()
|
||||
message_label.text = ""
|
||||
message_label.add_theme_font_size_override("font_size", 16)
|
||||
message_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
message_panel.add_child(message_label)
|
||||
message_panel.visible = false
|
||||
|
||||
# === CARD DETAIL PANEL (right side) ===
|
||||
card_detail_panel = _create_panel()
|
||||
root.add_child(card_detail_panel)
|
||||
card_detail_panel.set_anchors_preset(Control.PRESET_CENTER_RIGHT)
|
||||
card_detail_panel.offset_left = -220
|
||||
card_detail_panel.offset_right = -10
|
||||
card_detail_panel.offset_top = -150
|
||||
card_detail_panel.offset_bottom = 150
|
||||
card_detail_panel.visible = false
|
||||
|
||||
var detail_vbox = VBoxContainer.new()
|
||||
card_detail_panel.add_child(detail_vbox)
|
||||
detail_vbox.add_theme_constant_override("separation", 5)
|
||||
|
||||
detail_name_label = Label.new()
|
||||
detail_name_label.text = "Card Name"
|
||||
detail_name_label.add_theme_font_size_override("font_size", 18)
|
||||
detail_vbox.add_child(detail_name_label)
|
||||
|
||||
var separator = HSeparator.new()
|
||||
detail_vbox.add_child(separator)
|
||||
|
||||
detail_type_label = Label.new()
|
||||
detail_type_label.text = "Type: Forward"
|
||||
detail_vbox.add_child(detail_type_label)
|
||||
|
||||
detail_element_label = Label.new()
|
||||
detail_element_label.text = "Element: Fire"
|
||||
detail_vbox.add_child(detail_element_label)
|
||||
|
||||
detail_cost_label = Label.new()
|
||||
detail_cost_label.text = "Cost: 3"
|
||||
detail_vbox.add_child(detail_cost_label)
|
||||
|
||||
detail_power_label = Label.new()
|
||||
detail_power_label.text = "Power: 7000"
|
||||
detail_vbox.add_child(detail_power_label)
|
||||
|
||||
var separator2 = HSeparator.new()
|
||||
detail_vbox.add_child(separator2)
|
||||
|
||||
var ability_header = Label.new()
|
||||
ability_header.text = "Abilities:"
|
||||
ability_header.add_theme_font_size_override("font_size", 12)
|
||||
detail_vbox.add_child(ability_header)
|
||||
|
||||
detail_ability_label = Label.new()
|
||||
detail_ability_label.text = ""
|
||||
detail_ability_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
detail_ability_label.custom_minimum_size.x = 180
|
||||
detail_vbox.add_child(detail_ability_label)
|
||||
|
||||
# Message timer
|
||||
message_timer = Timer.new()
|
||||
add_child(message_timer)
|
||||
message_timer.one_shot = true
|
||||
message_timer.timeout.connect(_on_message_timer_timeout)
|
||||
|
||||
func _create_panel() -> PanelContainer:
|
||||
var panel = PanelContainer.new()
|
||||
|
||||
# Create stylebox
|
||||
var style = StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.1, 0.1, 0.15, 0.9)
|
||||
style.border_color = Color(0.3, 0.3, 0.4)
|
||||
style.set_border_width_all(2)
|
||||
style.set_corner_radius_all(5)
|
||||
style.set_content_margin_all(8)
|
||||
|
||||
panel.add_theme_stylebox_override("panel", style)
|
||||
return panel
|
||||
|
||||
func _connect_signals() -> void:
|
||||
end_phase_button.pressed.connect(_on_end_phase_pressed)
|
||||
pass_button.pressed.connect(_on_pass_priority_pressed)
|
||||
|
||||
# Connect to GameManager signals
|
||||
if GameManager:
|
||||
GameManager.turn_changed.connect(_on_turn_changed)
|
||||
GameManager.phase_changed.connect(_on_phase_changed)
|
||||
GameManager.message.connect(show_message)
|
||||
|
||||
func _on_end_phase_pressed() -> void:
|
||||
end_phase_pressed.emit()
|
||||
if GameManager:
|
||||
GameManager.pass_priority()
|
||||
|
||||
func _on_pass_priority_pressed() -> void:
|
||||
pass_priority_pressed.emit()
|
||||
if GameManager:
|
||||
GameManager.pass_priority()
|
||||
|
||||
func _on_turn_changed(player_name: String, turn_number: int) -> void:
|
||||
turn_label.text = player_name + " - Turn " + str(turn_number)
|
||||
|
||||
func _on_phase_changed(phase_name: String) -> void:
|
||||
phase_label.text = phase_name
|
||||
|
||||
## Update CP display
|
||||
func update_cp_display(cp_pool: CPPool) -> void:
|
||||
if not cp_pool:
|
||||
cp_label.text = "0 CP"
|
||||
return
|
||||
|
||||
var total = cp_pool.get_total_cp()
|
||||
var text = str(total) + " CP"
|
||||
|
||||
# Show element breakdown if any specific element CP
|
||||
var elements_text = []
|
||||
for element in Enums.Element.values():
|
||||
var amount = cp_pool.get_cp(element)
|
||||
if amount > 0:
|
||||
var elem_name = Enums.element_to_string(element)
|
||||
elements_text.append(elem_name + ":" + str(amount))
|
||||
|
||||
if elements_text.size() > 0:
|
||||
text += "\n" + ", ".join(elements_text)
|
||||
|
||||
cp_label.text = text
|
||||
|
||||
## Show a message
|
||||
func show_message(text: String) -> void:
|
||||
message_queue.append(text)
|
||||
if not message_timer.is_stopped():
|
||||
return
|
||||
_show_next_message()
|
||||
|
||||
func _show_next_message() -> void:
|
||||
if message_queue.is_empty():
|
||||
message_panel.visible = false
|
||||
return
|
||||
|
||||
var text = message_queue.pop_front()
|
||||
message_label.text = text
|
||||
message_panel.visible = true
|
||||
message_timer.start(MESSAGE_DISPLAY_TIME)
|
||||
|
||||
func _on_message_timer_timeout() -> void:
|
||||
_show_next_message()
|
||||
|
||||
## Show card detail panel
|
||||
func show_card_detail(card: CardInstance) -> void:
|
||||
if not card or not card.card_data:
|
||||
card_detail_panel.visible = false
|
||||
return
|
||||
|
||||
var data = card.card_data
|
||||
detail_name_label.text = data.name
|
||||
detail_type_label.text = "Type: " + Enums.card_type_to_string(data.type)
|
||||
detail_cost_label.text = "Cost: " + str(data.cost)
|
||||
|
||||
# Element
|
||||
var element_strs = []
|
||||
for elem in data.elements:
|
||||
element_strs.append(Enums.element_to_string(elem))
|
||||
detail_element_label.text = "Element: " + "/".join(element_strs)
|
||||
|
||||
# Power
|
||||
if data.type == Enums.CardType.FORWARD or data.power > 0:
|
||||
detail_power_label.text = "Power: " + str(card.get_power())
|
||||
detail_power_label.visible = true
|
||||
else:
|
||||
detail_power_label.visible = false
|
||||
|
||||
# Abilities
|
||||
var ability_text = ""
|
||||
for ability in data.abilities:
|
||||
if ability_text != "":
|
||||
ability_text += "\n\n"
|
||||
|
||||
var type_str = ""
|
||||
match ability.type:
|
||||
Enums.AbilityType.FIELD: type_str = "[Field]"
|
||||
Enums.AbilityType.AUTO: type_str = "[Auto]"
|
||||
Enums.AbilityType.ACTION: type_str = "[Action]"
|
||||
Enums.AbilityType.SPECIAL: type_str = "[Special]"
|
||||
|
||||
ability_text += type_str
|
||||
if ability.name != "":
|
||||
ability_text += " " + ability.name
|
||||
ability_text += "\n" + ability.effect
|
||||
|
||||
detail_ability_label.text = ability_text if ability_text != "" else "No abilities"
|
||||
|
||||
card_detail_panel.visible = true
|
||||
|
||||
## Hide card detail panel
|
||||
func hide_card_detail() -> void:
|
||||
card_detail_panel.visible = false
|
||||
|
||||
## Update button states based on game phase
|
||||
func update_button_states(can_end_phase: bool, can_pass: bool) -> void:
|
||||
end_phase_button.disabled = not can_end_phase
|
||||
pass_button.disabled = not can_pass
|
||||
238
scripts/ui/HandDisplay.gd
Normal file
238
scripts/ui/HandDisplay.gd
Normal file
@@ -0,0 +1,238 @@
|
||||
class_name HandDisplay
|
||||
extends Control
|
||||
|
||||
## HandDisplay - Shows the current player's hand in a fan layout
|
||||
|
||||
signal card_selected(card_instance: CardInstance)
|
||||
signal card_hovered(card_instance: CardInstance)
|
||||
signal card_unhovered
|
||||
|
||||
# Hand card visuals
|
||||
var hand_cards: Array[Control] = []
|
||||
var card_instances: Array[CardInstance] = []
|
||||
|
||||
# Layout settings
|
||||
const CARD_WIDTH: float = 100.0
|
||||
const CARD_HEIGHT: float = 140.0
|
||||
const CARD_OVERLAP: float = 70.0
|
||||
const FAN_ANGLE: float = 2.0 # Degrees per card from center
|
||||
const HOVER_LIFT: float = 25.0
|
||||
|
||||
# Current hover state
|
||||
var hovered_card_index: int = -1
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
# Connect to resized signal to re-layout when size changes
|
||||
resized.connect(_on_resized)
|
||||
|
||||
func _on_resized() -> void:
|
||||
_layout_cards()
|
||||
|
||||
## Update hand display with cards
|
||||
func update_hand(cards: Array) -> void:
|
||||
# Clear existing
|
||||
for card_ui in hand_cards:
|
||||
card_ui.queue_free()
|
||||
hand_cards.clear()
|
||||
card_instances.clear()
|
||||
|
||||
# Store card instances
|
||||
for card in cards:
|
||||
if card is CardInstance:
|
||||
card_instances.append(card)
|
||||
|
||||
# Create card UI elements
|
||||
_create_card_visuals()
|
||||
|
||||
# Defer layout to ensure size is set
|
||||
call_deferred("_layout_cards")
|
||||
|
||||
func _create_card_visuals() -> void:
|
||||
for i in range(card_instances.size()):
|
||||
var card = card_instances[i]
|
||||
var card_ui = _create_card_ui(card, i)
|
||||
add_child(card_ui)
|
||||
hand_cards.append(card_ui)
|
||||
|
||||
func _create_card_ui(card: CardInstance, index: int) -> Control:
|
||||
# Use Panel for better input handling
|
||||
var container = Panel.new()
|
||||
container.custom_minimum_size = Vector2(CARD_WIDTH, CARD_HEIGHT)
|
||||
container.size = Vector2(CARD_WIDTH, CARD_HEIGHT)
|
||||
container.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
|
||||
# Style the panel with element color
|
||||
var style = StyleBoxFlat.new()
|
||||
style.bg_color = _get_element_color(card.card_data.get_primary_element())
|
||||
style.border_color = Color.BLACK
|
||||
style.set_border_width_all(2)
|
||||
container.add_theme_stylebox_override("panel", style)
|
||||
|
||||
# Card background (keep for highlighting)
|
||||
var bg = ColorRect.new()
|
||||
bg.size = Vector2(CARD_WIDTH, CARD_HEIGHT)
|
||||
bg.color = _get_element_color(card.card_data.get_primary_element())
|
||||
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE # Let parent handle input
|
||||
container.add_child(bg)
|
||||
|
||||
# Card name
|
||||
var name_label = Label.new()
|
||||
name_label.text = card.card_data.name
|
||||
name_label.position = Vector2(5, 5)
|
||||
name_label.size = Vector2(CARD_WIDTH - 10, 50)
|
||||
name_label.add_theme_font_size_override("font_size", 11)
|
||||
name_label.add_theme_color_override("font_color", Color.BLACK)
|
||||
name_label.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
container.add_child(name_label)
|
||||
|
||||
# Cost indicator (top right)
|
||||
var cost_bg = ColorRect.new()
|
||||
cost_bg.size = Vector2(28, 28)
|
||||
cost_bg.position = Vector2(CARD_WIDTH - 32, 4)
|
||||
cost_bg.color = Color(0.2, 0.2, 0.2)
|
||||
cost_bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
container.add_child(cost_bg)
|
||||
|
||||
var cost_label = Label.new()
|
||||
cost_label.text = str(card.card_data.cost)
|
||||
cost_label.position = Vector2(CARD_WIDTH - 32, 4)
|
||||
cost_label.size = Vector2(28, 28)
|
||||
cost_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
cost_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
cost_label.add_theme_font_size_override("font_size", 16)
|
||||
cost_label.add_theme_color_override("font_color", Color.WHITE)
|
||||
cost_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
container.add_child(cost_label)
|
||||
|
||||
# Type indicator (bottom left)
|
||||
var type_label = Label.new()
|
||||
type_label.text = Enums.card_type_to_string(card.card_data.type)
|
||||
type_label.position = Vector2(5, CARD_HEIGHT - 22)
|
||||
type_label.add_theme_font_size_override("font_size", 10)
|
||||
type_label.add_theme_color_override("font_color", Color.BLACK)
|
||||
type_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
container.add_child(type_label)
|
||||
|
||||
# Power for forwards (bottom right)
|
||||
if card.card_data.type == Enums.CardType.FORWARD:
|
||||
var power_bg = ColorRect.new()
|
||||
power_bg.size = Vector2(45, 20)
|
||||
power_bg.position = Vector2(CARD_WIDTH - 50, CARD_HEIGHT - 24)
|
||||
power_bg.color = Color(0.3, 0.1, 0.1, 0.8)
|
||||
power_bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
container.add_child(power_bg)
|
||||
|
||||
var power_label = Label.new()
|
||||
power_label.text = str(card.card_data.power)
|
||||
power_label.position = Vector2(CARD_WIDTH - 50, CARD_HEIGHT - 24)
|
||||
power_label.size = Vector2(45, 20)
|
||||
power_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
power_label.add_theme_font_size_override("font_size", 12)
|
||||
power_label.add_theme_color_override("font_color", Color.WHITE)
|
||||
power_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
container.add_child(power_label)
|
||||
|
||||
# Connect signals
|
||||
container.gui_input.connect(_on_card_gui_input.bind(index))
|
||||
container.mouse_entered.connect(_on_card_mouse_entered.bind(index))
|
||||
container.mouse_exited.connect(_on_card_mouse_exited.bind(index))
|
||||
|
||||
return container
|
||||
|
||||
func _get_element_color(element: Enums.Element) -> Color:
|
||||
var color = Enums.get_element_color(element)
|
||||
# Lighten for card background
|
||||
return color.lightened(0.4)
|
||||
|
||||
func _layout_cards() -> void:
|
||||
var card_count = hand_cards.size()
|
||||
if card_count == 0:
|
||||
return
|
||||
|
||||
# Get actual display width - use size if available, otherwise viewport
|
||||
var display_width = size.x
|
||||
|
||||
if display_width <= 0:
|
||||
var viewport = get_viewport()
|
||||
if viewport:
|
||||
display_width = viewport.get_visible_rect().size.x - 100 # Leave margins
|
||||
|
||||
if display_width <= 0:
|
||||
display_width = 1820 # Fallback
|
||||
|
||||
# Calculate total width needed
|
||||
var total_width = CARD_WIDTH + (card_count - 1) * CARD_OVERLAP
|
||||
var start_x = (display_width - total_width) / 2.0
|
||||
|
||||
# Position each card
|
||||
for i in range(card_count):
|
||||
var card_ui = hand_cards[i]
|
||||
|
||||
# X position with overlap
|
||||
var x = start_x + i * CARD_OVERLAP
|
||||
|
||||
# Y position (near top of container, with hover lift)
|
||||
var y = 10.0
|
||||
if i == hovered_card_index:
|
||||
y -= HOVER_LIFT
|
||||
|
||||
# Rotation based on position in fan
|
||||
var center_offset = i - (card_count - 1) / 2.0
|
||||
var angle = center_offset * FAN_ANGLE
|
||||
|
||||
card_ui.position = Vector2(x, y)
|
||||
card_ui.rotation = deg_to_rad(angle)
|
||||
card_ui.z_index = i
|
||||
if i == hovered_card_index:
|
||||
card_ui.z_index = 100
|
||||
|
||||
func _on_card_gui_input(event: InputEvent, index: int) -> void:
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||||
print("HandDisplay: Card clicked, index=", index)
|
||||
if index >= 0 and index < card_instances.size():
|
||||
print("HandDisplay: Emitting card_selected for ", card_instances[index].card_data.name)
|
||||
card_selected.emit(card_instances[index])
|
||||
|
||||
func _on_card_mouse_entered(index: int) -> void:
|
||||
hovered_card_index = index
|
||||
_layout_cards()
|
||||
if index >= 0 and index < card_instances.size():
|
||||
card_hovered.emit(card_instances[index])
|
||||
|
||||
func _on_card_mouse_exited(index: int) -> void:
|
||||
if hovered_card_index == index:
|
||||
hovered_card_index = -1
|
||||
_layout_cards()
|
||||
card_unhovered.emit()
|
||||
|
||||
## Highlight playable cards
|
||||
func highlight_playable(predicate: Callable) -> void:
|
||||
for i in range(hand_cards.size()):
|
||||
var card_ui = hand_cards[i]
|
||||
var card = card_instances[i]
|
||||
|
||||
var bg = card_ui.get_child(0) as ColorRect
|
||||
if bg:
|
||||
if predicate.call(card):
|
||||
# Add glow effect - brighter
|
||||
bg.color = _get_element_color(card.card_data.get_primary_element()).lightened(0.2)
|
||||
else:
|
||||
# Dim non-playable
|
||||
bg.color = _get_element_color(card.card_data.get_primary_element()).darkened(0.4)
|
||||
|
||||
## Clear all highlights
|
||||
func clear_highlights() -> void:
|
||||
for i in range(hand_cards.size()):
|
||||
var card_ui = hand_cards[i]
|
||||
var card = card_instances[i]
|
||||
|
||||
var bg = card_ui.get_child(0) as ColorRect
|
||||
if bg:
|
||||
bg.color = _get_element_color(card.card_data.get_primary_element())
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_RESIZED:
|
||||
_layout_cards()
|
||||
94
scripts/ui/MainMenu.gd
Normal file
94
scripts/ui/MainMenu.gd
Normal file
@@ -0,0 +1,94 @@
|
||||
class_name MainMenu
|
||||
extends CanvasLayer
|
||||
|
||||
## MainMenu - Main menu screen
|
||||
|
||||
signal start_game
|
||||
signal quit_game
|
||||
|
||||
# UI Components
|
||||
var title_label: Label
|
||||
var start_button: Button
|
||||
var options_button: Button
|
||||
var quit_button: Button
|
||||
var version_label: Label
|
||||
|
||||
func _ready() -> void:
|
||||
_create_menu()
|
||||
|
||||
func _create_menu() -> void:
|
||||
# Background
|
||||
var bg = ColorRect.new()
|
||||
add_child(bg)
|
||||
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
bg.color = Color(0.08, 0.08, 0.12)
|
||||
|
||||
# Center container
|
||||
var center = CenterContainer.new()
|
||||
add_child(center)
|
||||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
|
||||
var vbox = VBoxContainer.new()
|
||||
center.add_child(vbox)
|
||||
vbox.add_theme_constant_override("separation", 20)
|
||||
|
||||
# Title
|
||||
title_label = Label.new()
|
||||
title_label.text = "FF-TCG Digital"
|
||||
title_label.add_theme_font_size_override("font_size", 48)
|
||||
title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
vbox.add_child(title_label)
|
||||
|
||||
# Subtitle
|
||||
var subtitle = Label.new()
|
||||
subtitle.text = "Final Fantasy Trading Card Game"
|
||||
subtitle.add_theme_font_size_override("font_size", 18)
|
||||
subtitle.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
subtitle.add_theme_color_override("font_color", Color(0.6, 0.6, 0.7))
|
||||
vbox.add_child(subtitle)
|
||||
|
||||
# Spacer
|
||||
var spacer = Control.new()
|
||||
spacer.custom_minimum_size = Vector2(0, 40)
|
||||
vbox.add_child(spacer)
|
||||
|
||||
# Start button
|
||||
start_button = _create_menu_button("Start Game")
|
||||
vbox.add_child(start_button)
|
||||
start_button.pressed.connect(_on_start_pressed)
|
||||
|
||||
# Options button (placeholder)
|
||||
options_button = _create_menu_button("Options")
|
||||
options_button.disabled = true
|
||||
vbox.add_child(options_button)
|
||||
|
||||
# Quit button
|
||||
quit_button = _create_menu_button("Quit")
|
||||
vbox.add_child(quit_button)
|
||||
quit_button.pressed.connect(_on_quit_pressed)
|
||||
|
||||
# Version label at bottom
|
||||
version_label = Label.new()
|
||||
version_label.text = "v0.1.0 - Development Build"
|
||||
version_label.add_theme_font_size_override("font_size", 12)
|
||||
version_label.add_theme_color_override("font_color", Color(0.4, 0.4, 0.5))
|
||||
add_child(version_label)
|
||||
version_label.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||
version_label.offset_left = -200
|
||||
version_label.offset_top = -30
|
||||
version_label.offset_right = -10
|
||||
version_label.offset_bottom = -10
|
||||
|
||||
func _create_menu_button(text: String) -> Button:
|
||||
var button = Button.new()
|
||||
button.text = text
|
||||
button.custom_minimum_size = Vector2(200, 50)
|
||||
button.add_theme_font_size_override("font_size", 20)
|
||||
return button
|
||||
|
||||
func _on_start_pressed() -> void:
|
||||
start_game.emit()
|
||||
|
||||
func _on_quit_pressed() -> void:
|
||||
quit_game.emit()
|
||||
get_tree().quit()
|
||||
111
scripts/ui/PauseMenu.gd
Normal file
111
scripts/ui/PauseMenu.gd
Normal file
@@ -0,0 +1,111 @@
|
||||
class_name PauseMenu
|
||||
extends CanvasLayer
|
||||
|
||||
## PauseMenu - In-game pause/escape menu
|
||||
|
||||
signal resume_game
|
||||
signal restart_game
|
||||
signal return_to_menu
|
||||
|
||||
# UI Components
|
||||
var panel: PanelContainer
|
||||
var resume_button: Button
|
||||
var restart_button: Button
|
||||
var menu_button: Button
|
||||
|
||||
var menu_is_visible: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
_create_menu()
|
||||
hide_menu()
|
||||
|
||||
func _create_menu() -> void:
|
||||
# Semi-transparent background
|
||||
var bg = ColorRect.new()
|
||||
add_child(bg)
|
||||
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
bg.color = Color(0, 0, 0, 0.7)
|
||||
bg.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
|
||||
# Center container
|
||||
var center = CenterContainer.new()
|
||||
add_child(center)
|
||||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
|
||||
# Panel
|
||||
panel = PanelContainer.new()
|
||||
center.add_child(panel)
|
||||
|
||||
var style = StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.15, 0.15, 0.2)
|
||||
style.border_color = Color(0.3, 0.3, 0.4)
|
||||
style.set_border_width_all(2)
|
||||
style.set_corner_radius_all(10)
|
||||
style.set_content_margin_all(30)
|
||||
panel.add_theme_stylebox_override("panel", style)
|
||||
|
||||
var vbox = VBoxContainer.new()
|
||||
panel.add_child(vbox)
|
||||
vbox.add_theme_constant_override("separation", 15)
|
||||
|
||||
# Title
|
||||
var title = Label.new()
|
||||
title.text = "Game Paused"
|
||||
title.add_theme_font_size_override("font_size", 28)
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
vbox.add_child(title)
|
||||
|
||||
# Spacer
|
||||
var spacer = Control.new()
|
||||
spacer.custom_minimum_size = Vector2(0, 10)
|
||||
vbox.add_child(spacer)
|
||||
|
||||
# Resume button
|
||||
resume_button = _create_menu_button("Resume")
|
||||
vbox.add_child(resume_button)
|
||||
resume_button.pressed.connect(_on_resume_pressed)
|
||||
|
||||
# Restart button
|
||||
restart_button = _create_menu_button("Restart Game")
|
||||
vbox.add_child(restart_button)
|
||||
restart_button.pressed.connect(_on_restart_pressed)
|
||||
|
||||
# Return to menu button
|
||||
menu_button = _create_menu_button("Main Menu")
|
||||
vbox.add_child(menu_button)
|
||||
menu_button.pressed.connect(_on_menu_pressed)
|
||||
|
||||
func _create_menu_button(text: String) -> Button:
|
||||
var button = Button.new()
|
||||
button.text = text
|
||||
button.custom_minimum_size = Vector2(180, 45)
|
||||
button.add_theme_font_size_override("font_size", 18)
|
||||
return button
|
||||
|
||||
func show_menu() -> void:
|
||||
visible = true
|
||||
menu_is_visible = true
|
||||
get_tree().paused = true
|
||||
|
||||
func hide_menu() -> void:
|
||||
visible = false
|
||||
menu_is_visible = false
|
||||
get_tree().paused = false
|
||||
|
||||
func toggle_menu() -> void:
|
||||
if menu_is_visible:
|
||||
hide_menu()
|
||||
else:
|
||||
show_menu()
|
||||
|
||||
func _on_resume_pressed() -> void:
|
||||
hide_menu()
|
||||
resume_game.emit()
|
||||
|
||||
func _on_restart_pressed() -> void:
|
||||
hide_menu()
|
||||
restart_game.emit()
|
||||
|
||||
func _on_menu_pressed() -> void:
|
||||
hide_menu()
|
||||
return_to_menu.emit()
|
||||
193
scripts/visual/CardVisual.gd
Normal file
193
scripts/visual/CardVisual.gd
Normal file
@@ -0,0 +1,193 @@
|
||||
class_name CardVisual
|
||||
extends Node3D
|
||||
|
||||
## CardVisual - 3D visual representation of a card
|
||||
|
||||
signal clicked(card_visual: CardVisual)
|
||||
signal hovered(card_visual: CardVisual)
|
||||
signal unhovered(card_visual: CardVisual)
|
||||
|
||||
# Card dimensions (standard card ratio ~2.5:3.5)
|
||||
const CARD_WIDTH: float = 0.63
|
||||
const CARD_HEIGHT: float = 0.88
|
||||
const CARD_THICKNESS: float = 0.02
|
||||
|
||||
# Associated card instance
|
||||
var card_instance: CardInstance = null
|
||||
|
||||
# Visual state
|
||||
var is_highlighted: bool = false
|
||||
var is_selected: bool = false
|
||||
var is_hovered: bool = false
|
||||
|
||||
# Animation
|
||||
var target_position: Vector3 = Vector3.ZERO
|
||||
var target_rotation: Vector3 = Vector3.ZERO
|
||||
var move_speed: float = 10.0
|
||||
var is_animating: bool = false
|
||||
|
||||
# Components
|
||||
var mesh_instance: MeshInstance3D
|
||||
var collision_shape: CollisionShape3D
|
||||
var static_body: StaticBody3D
|
||||
var material: StandardMaterial3D
|
||||
|
||||
# Colors
|
||||
var normal_color: Color = Color.WHITE
|
||||
var highlight_color: Color = Color(1.2, 1.2, 0.8)
|
||||
var selected_color: Color = Color(0.8, 1.2, 0.8)
|
||||
var dull_tint: Color = Color(0.7, 0.7, 0.7)
|
||||
|
||||
func _ready() -> void:
|
||||
_create_card_mesh()
|
||||
_setup_collision()
|
||||
|
||||
func _create_card_mesh() -> void:
|
||||
mesh_instance = MeshInstance3D.new()
|
||||
add_child(mesh_instance)
|
||||
|
||||
# Create box mesh for card
|
||||
var box = BoxMesh.new()
|
||||
box.size = Vector3(CARD_WIDTH, CARD_THICKNESS, CARD_HEIGHT)
|
||||
mesh_instance.mesh = box
|
||||
|
||||
# Create material
|
||||
material = StandardMaterial3D.new()
|
||||
material.albedo_color = Color.WHITE
|
||||
mesh_instance.material_override = material
|
||||
|
||||
func _setup_collision() -> void:
|
||||
static_body = StaticBody3D.new()
|
||||
add_child(static_body)
|
||||
|
||||
collision_shape = CollisionShape3D.new()
|
||||
var shape = BoxShape3D.new()
|
||||
shape.size = Vector3(CARD_WIDTH, CARD_THICKNESS * 2, CARD_HEIGHT)
|
||||
collision_shape.shape = shape
|
||||
static_body.add_child(collision_shape)
|
||||
|
||||
# Connect input
|
||||
static_body.input_event.connect(_on_input_event)
|
||||
static_body.mouse_entered.connect(_on_mouse_entered)
|
||||
static_body.mouse_exited.connect(_on_mouse_exited)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# Animate position
|
||||
if is_animating:
|
||||
position = position.lerp(target_position, move_speed * delta)
|
||||
rotation = rotation.lerp(target_rotation, move_speed * delta)
|
||||
|
||||
if position.distance_to(target_position) < 0.01 and rotation.distance_to(target_rotation) < 0.01:
|
||||
position = target_position
|
||||
rotation = target_rotation
|
||||
is_animating = false
|
||||
|
||||
# Update dull visual
|
||||
_update_visual_state()
|
||||
|
||||
## Initialize with a card instance
|
||||
func setup(card: CardInstance) -> void:
|
||||
card_instance = card
|
||||
_load_card_texture()
|
||||
_update_visual_state()
|
||||
|
||||
## Load the card's texture
|
||||
func _load_card_texture() -> void:
|
||||
if not card_instance or not card_instance.card_data:
|
||||
return
|
||||
|
||||
var texture = CardDatabase.get_card_texture(card_instance.card_data)
|
||||
if texture:
|
||||
material.albedo_texture = texture
|
||||
else:
|
||||
# Use element color as fallback
|
||||
var element_color = Enums.element_to_color(card_instance.get_element())
|
||||
material.albedo_color = element_color
|
||||
|
||||
## Update visual state based on card state
|
||||
func _update_visual_state() -> void:
|
||||
if not material:
|
||||
return
|
||||
|
||||
var color = normal_color
|
||||
|
||||
if is_selected:
|
||||
color = selected_color
|
||||
elif is_highlighted:
|
||||
color = highlight_color
|
||||
|
||||
# Apply dull tint
|
||||
if card_instance and card_instance.is_dull():
|
||||
color = color * dull_tint
|
||||
|
||||
material.albedo_color = color
|
||||
|
||||
## Set card as highlighted
|
||||
func set_highlighted(highlighted: bool) -> void:
|
||||
is_highlighted = highlighted
|
||||
_update_visual_state()
|
||||
|
||||
## Set card as selected
|
||||
func set_selected(selected: bool) -> void:
|
||||
is_selected = selected
|
||||
_update_visual_state()
|
||||
|
||||
## Move card to position with animation
|
||||
func move_to(pos: Vector3, rot: Vector3 = Vector3.ZERO) -> void:
|
||||
target_position = pos
|
||||
target_rotation = rot
|
||||
is_animating = true
|
||||
|
||||
## Move card instantly
|
||||
func set_position_instant(pos: Vector3, rot: Vector3 = Vector3.ZERO) -> void:
|
||||
position = pos
|
||||
rotation = rot
|
||||
target_position = pos
|
||||
target_rotation = rot
|
||||
is_animating = false
|
||||
|
||||
## Set dull rotation (90 degrees)
|
||||
func set_dull_visual(is_dull: bool) -> void:
|
||||
if is_dull:
|
||||
target_rotation.y = deg_to_rad(90)
|
||||
else:
|
||||
target_rotation.y = 0
|
||||
is_animating = true
|
||||
|
||||
## Input handlers
|
||||
func _on_input_event(_camera: Node, event: InputEvent, _position: Vector3, _normal: Vector3, _shape_idx: int) -> void:
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||||
clicked.emit(self)
|
||||
|
||||
func _on_mouse_entered() -> void:
|
||||
is_hovered = true
|
||||
hovered.emit(self)
|
||||
# Slight raise on hover
|
||||
if not is_animating:
|
||||
target_position.y = position.y + 0.1
|
||||
is_animating = true
|
||||
|
||||
func _on_mouse_exited() -> void:
|
||||
is_hovered = false
|
||||
unhovered.emit(self)
|
||||
# Return to normal height
|
||||
if not is_animating and card_instance:
|
||||
target_position.y = position.y - 0.1
|
||||
is_animating = true
|
||||
|
||||
## Get display info for UI
|
||||
func get_card_info() -> Dictionary:
|
||||
if not card_instance or not card_instance.card_data:
|
||||
return {}
|
||||
|
||||
var data = card_instance.card_data
|
||||
return {
|
||||
"name": data.name,
|
||||
"type": Enums.card_type_to_string(data.type),
|
||||
"element": Enums.element_to_string(data.get_primary_element()),
|
||||
"cost": data.cost,
|
||||
"power": data.power if data.type == Enums.CardType.FORWARD else 0,
|
||||
"job": data.job,
|
||||
"is_dull": card_instance.is_dull()
|
||||
}
|
||||
53
scripts/visual/TableCamera.gd
Normal file
53
scripts/visual/TableCamera.gd
Normal file
@@ -0,0 +1,53 @@
|
||||
class_name TableCamera
|
||||
extends Camera3D
|
||||
|
||||
## TableCamera - Isometric camera for the game table
|
||||
|
||||
# Camera settings
|
||||
@export var camera_distance: float = 18.0
|
||||
@export var camera_angle: float = 55.0 # Degrees from horizontal (higher = more top-down)
|
||||
@export var camera_height_offset: float = 0.0 # Offset to look slightly above center
|
||||
|
||||
# Smooth movement
|
||||
@export var smooth_speed: float = 5.0
|
||||
var target_position: Vector3 = Vector3.ZERO
|
||||
|
||||
func _ready() -> void:
|
||||
_setup_isometric_view()
|
||||
|
||||
func _setup_isometric_view() -> void:
|
||||
# Set perspective projection
|
||||
projection = PROJECTION_PERSPECTIVE
|
||||
fov = 50.0
|
||||
|
||||
# Calculate position - camera is positioned behind Player 1's side
|
||||
var angle_rad = deg_to_rad(camera_angle)
|
||||
|
||||
# Height based on angle
|
||||
var height = camera_distance * sin(angle_rad)
|
||||
# Distance along Z axis (positive Z is toward Player 1)
|
||||
var z_offset = camera_distance * cos(angle_rad)
|
||||
|
||||
# Position camera behind Player 1 (positive Z), looking toward center/opponent
|
||||
position = Vector3(0, height, z_offset)
|
||||
|
||||
# Look at center of table
|
||||
look_at(Vector3(0, camera_height_offset, 0), Vector3.UP)
|
||||
|
||||
target_position = position
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# Smooth camera movement if needed
|
||||
if position.distance_to(target_position) > 0.01:
|
||||
position = position.lerp(target_position, smooth_speed * delta)
|
||||
look_at(Vector3(0, camera_height_offset, 0), Vector3.UP)
|
||||
|
||||
## Set camera to look at a specific point
|
||||
func focus_on(point: Vector3) -> void:
|
||||
var angle_rad = deg_to_rad(camera_angle)
|
||||
target_position = point + Vector3(0, camera_distance * sin(angle_rad),
|
||||
camera_distance * cos(angle_rad))
|
||||
|
||||
## Reset to default position
|
||||
func reset_position() -> void:
|
||||
_setup_isometric_view()
|
||||
258
scripts/visual/TableSetup.gd
Normal file
258
scripts/visual/TableSetup.gd
Normal file
@@ -0,0 +1,258 @@
|
||||
class_name TableSetup
|
||||
extends Node3D
|
||||
|
||||
## TableSetup - Sets up the 3D game table with all zones
|
||||
|
||||
signal card_clicked(card_instance: CardInstance, zone_type: Enums.ZoneType, player_index: int)
|
||||
|
||||
# Zone visuals for each player
|
||||
var player_zones: Array[Dictionary] = [{}, {}]
|
||||
|
||||
# Deck indicators (simple card-back representation)
|
||||
var deck_indicators: Array[MeshInstance3D] = [null, null]
|
||||
var deck_count_labels: Array[Label3D] = [null, null]
|
||||
|
||||
# Table dimensions
|
||||
const TABLE_WIDTH: float = 16.0
|
||||
const TABLE_DEPTH: float = 12.0
|
||||
|
||||
# Zone positions (relative to table center)
|
||||
const ZONE_POSITIONS = {
|
||||
"deck": Vector3(5.5, 0.1, 3.5),
|
||||
"damage": Vector3(3.5, 0.1, 3.5),
|
||||
"backups": Vector3(0.0, 0.1, 3.5),
|
||||
"break": Vector3(-3.5, 0.1, 3.5),
|
||||
"forwards": Vector3(0.0, 0.1, 1.5),
|
||||
"hand": Vector3(0.0, 0.5, 5.5)
|
||||
}
|
||||
|
||||
# Components
|
||||
var table_mesh: MeshInstance3D
|
||||
var camera: TableCamera
|
||||
|
||||
func _ready() -> void:
|
||||
_create_table()
|
||||
_create_camera()
|
||||
_create_lighting()
|
||||
_create_zones()
|
||||
_create_deck_indicators()
|
||||
|
||||
func _create_table() -> void:
|
||||
table_mesh = MeshInstance3D.new()
|
||||
add_child(table_mesh)
|
||||
|
||||
var plane = PlaneMesh.new()
|
||||
plane.size = Vector2(TABLE_WIDTH, TABLE_DEPTH)
|
||||
table_mesh.mesh = plane
|
||||
|
||||
# Create table material
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.15, 0.2, 0.15) # Dark green felt
|
||||
|
||||
# Try to load playmat texture
|
||||
if ResourceLoader.exists("res://assets/table/playmat.webp"):
|
||||
var texture = load("res://assets/table/playmat.webp")
|
||||
if texture:
|
||||
mat.albedo_texture = texture
|
||||
|
||||
table_mesh.material_override = mat
|
||||
# PlaneMesh in Godot 4 is already horizontal (facing up), no rotation needed
|
||||
|
||||
func _create_camera() -> void:
|
||||
camera = TableCamera.new()
|
||||
add_child(camera)
|
||||
camera.current = true
|
||||
|
||||
func _create_lighting() -> void:
|
||||
# Main directional light
|
||||
var dir_light = DirectionalLight3D.new()
|
||||
add_child(dir_light)
|
||||
dir_light.position = Vector3(5, 10, 5)
|
||||
dir_light.rotation = Vector3(deg_to_rad(-45), deg_to_rad(45), 0)
|
||||
dir_light.light_energy = 0.8
|
||||
dir_light.shadow_enabled = true
|
||||
|
||||
# Ambient light
|
||||
var env = WorldEnvironment.new()
|
||||
add_child(env)
|
||||
var environment = Environment.new()
|
||||
environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
environment.ambient_light_color = Color(0.3, 0.3, 0.35)
|
||||
environment.ambient_light_energy = 0.5
|
||||
environment.background_mode = Environment.BG_COLOR
|
||||
environment.background_color = Color(0.1, 0.1, 0.15)
|
||||
env.environment = environment
|
||||
|
||||
func _create_zones() -> void:
|
||||
# Create zones for both players
|
||||
for player_idx in range(2):
|
||||
var flip = 1 if player_idx == 0 else -1
|
||||
var rot = 0 if player_idx == 0 else 180
|
||||
|
||||
# Field - Forwards
|
||||
var forwards_zone = _create_zone(
|
||||
Enums.ZoneType.FIELD_FORWARDS, player_idx,
|
||||
ZONE_POSITIONS["forwards"] * Vector3(1, 1, flip), rot
|
||||
)
|
||||
player_zones[player_idx]["forwards"] = forwards_zone
|
||||
|
||||
# Field - Backups
|
||||
var backups_zone = _create_zone(
|
||||
Enums.ZoneType.FIELD_BACKUPS, player_idx,
|
||||
ZONE_POSITIONS["backups"] * Vector3(1, 1, flip), rot
|
||||
)
|
||||
player_zones[player_idx]["backups"] = backups_zone
|
||||
|
||||
# Damage Zone
|
||||
var damage_zone = _create_zone(
|
||||
Enums.ZoneType.DAMAGE, player_idx,
|
||||
ZONE_POSITIONS["damage"] * Vector3(flip, 1, flip), rot
|
||||
)
|
||||
player_zones[player_idx]["damage"] = damage_zone
|
||||
|
||||
# Break Zone
|
||||
var break_zone = _create_zone(
|
||||
Enums.ZoneType.BREAK, player_idx,
|
||||
ZONE_POSITIONS["break"] * Vector3(flip, 1, flip), rot
|
||||
)
|
||||
player_zones[player_idx]["break"] = break_zone
|
||||
|
||||
func _create_deck_indicators() -> void:
|
||||
# Create simple card-back boxes for deck representation
|
||||
for player_idx in range(2):
|
||||
var flip = 1 if player_idx == 0 else -1
|
||||
var pos = ZONE_POSITIONS["deck"] * Vector3(flip, 1, flip)
|
||||
|
||||
# Card back mesh (simple colored box)
|
||||
var deck_mesh = MeshInstance3D.new()
|
||||
add_child(deck_mesh)
|
||||
|
||||
var box = BoxMesh.new()
|
||||
box.size = Vector3(0.63, 0.3, 0.88) # Card size with thickness for deck
|
||||
deck_mesh.mesh = box
|
||||
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.15, 0.1, 0.3) # Dark blue for card back
|
||||
deck_mesh.material_override = mat
|
||||
|
||||
deck_mesh.position = pos + Vector3(0, 0.15, 0) # Raise above table
|
||||
|
||||
deck_indicators[player_idx] = deck_mesh
|
||||
|
||||
# Card count label
|
||||
var label = Label3D.new()
|
||||
add_child(label)
|
||||
label.text = "50"
|
||||
label.font_size = 64
|
||||
label.position = pos + Vector3(0, 0.35, 0)
|
||||
label.rotation.x = deg_to_rad(-90) # Face up
|
||||
if player_idx == 1:
|
||||
label.rotation.z = deg_to_rad(180) # Flip for opponent
|
||||
deck_count_labels[player_idx] = label
|
||||
|
||||
func _create_zone(zone_type: Enums.ZoneType, player_idx: int, pos: Vector3, rot: float) -> ZoneVisual:
|
||||
var zone = ZoneVisual.new()
|
||||
add_child(zone)
|
||||
|
||||
zone.zone_type = zone_type
|
||||
zone.player_index = player_idx
|
||||
zone.zone_position = pos
|
||||
zone.zone_rotation = rot
|
||||
|
||||
# Configure based on zone type
|
||||
match zone_type:
|
||||
Enums.ZoneType.FIELD_FORWARDS, Enums.ZoneType.FIELD_BACKUPS:
|
||||
zone.card_spacing = 0.8
|
||||
Enums.ZoneType.DAMAGE, Enums.ZoneType.BREAK:
|
||||
zone.stack_offset = 0.02
|
||||
|
||||
zone.card_clicked.connect(_on_zone_card_clicked.bind(zone_type, player_idx))
|
||||
|
||||
return zone
|
||||
|
||||
## Get a zone visual
|
||||
func get_zone(player_idx: int, zone_name: String) -> ZoneVisual:
|
||||
if player_idx >= 0 and player_idx < player_zones.size():
|
||||
return player_zones[player_idx].get(zone_name)
|
||||
return null
|
||||
|
||||
## Sync visual state with game state
|
||||
func sync_with_game_state(game_state: GameState) -> void:
|
||||
for player_idx in range(2):
|
||||
var player = game_state.get_player(player_idx)
|
||||
if not player:
|
||||
continue
|
||||
|
||||
# Sync forwards
|
||||
_sync_zone(player_zones[player_idx]["forwards"], player.field_forwards)
|
||||
|
||||
# Sync backups
|
||||
_sync_zone(player_zones[player_idx]["backups"], player.field_backups)
|
||||
|
||||
# Update deck count (don't sync individual cards)
|
||||
_update_deck_indicator(player_idx, player.deck.get_count())
|
||||
|
||||
# Sync damage
|
||||
_sync_zone(player_zones[player_idx]["damage"], player.damage_zone)
|
||||
|
||||
# Sync break zone
|
||||
_sync_zone(player_zones[player_idx]["break"], player.break_zone)
|
||||
|
||||
## Update deck indicator
|
||||
func _update_deck_indicator(player_idx: int, count: int) -> void:
|
||||
if deck_count_labels[player_idx]:
|
||||
deck_count_labels[player_idx].text = str(count)
|
||||
|
||||
# Scale deck thickness based on count
|
||||
if deck_indicators[player_idx]:
|
||||
var thickness = max(0.05, count * 0.006) # Min thickness, scale with cards
|
||||
var mesh = deck_indicators[player_idx].mesh as BoxMesh
|
||||
if mesh:
|
||||
mesh.size.y = thickness
|
||||
deck_indicators[player_idx].position.y = 0.1 + thickness / 2
|
||||
|
||||
func _sync_zone(visual_zone: ZoneVisual, game_zone: Zone) -> void:
|
||||
if not visual_zone or not game_zone:
|
||||
return
|
||||
|
||||
# Get current visual cards
|
||||
var visual_instances = {}
|
||||
for cv in visual_zone.card_visuals:
|
||||
if cv.card_instance:
|
||||
visual_instances[cv.card_instance.instance_id] = cv
|
||||
|
||||
# Get game zone cards
|
||||
var game_cards = game_zone.get_cards()
|
||||
|
||||
# Add missing cards
|
||||
for card in game_cards:
|
||||
if not visual_instances.has(card.instance_id):
|
||||
visual_zone.add_card(card)
|
||||
|
||||
# Remove cards no longer in zone
|
||||
var game_ids = {}
|
||||
for card in game_cards:
|
||||
game_ids[card.instance_id] = true
|
||||
|
||||
for instance_id in visual_instances:
|
||||
if not game_ids.has(instance_id):
|
||||
visual_zone.remove_card(visual_instances[instance_id])
|
||||
|
||||
## Card click handler
|
||||
func _on_zone_card_clicked(card_visual: CardVisual, zone_type: Enums.ZoneType, player_idx: int) -> void:
|
||||
if card_visual.card_instance:
|
||||
card_clicked.emit(card_visual.card_instance, zone_type, player_idx)
|
||||
|
||||
## Highlight cards in a zone based on a condition
|
||||
func highlight_zone_cards(player_idx: int, zone_name: String, predicate: Callable) -> void:
|
||||
var zone = get_zone(player_idx, zone_name)
|
||||
if zone:
|
||||
zone.highlight_interactive(predicate)
|
||||
|
||||
## Clear all highlights
|
||||
func clear_all_highlights() -> void:
|
||||
for player_idx in range(2):
|
||||
for zone_name in player_zones[player_idx]:
|
||||
var zone = player_zones[player_idx][zone_name]
|
||||
if zone:
|
||||
zone.clear_highlights()
|
||||
184
scripts/visual/ZoneVisual.gd
Normal file
184
scripts/visual/ZoneVisual.gd
Normal file
@@ -0,0 +1,184 @@
|
||||
class_name ZoneVisual
|
||||
extends Node3D
|
||||
|
||||
## ZoneVisual - Visual representation of a card zone
|
||||
|
||||
signal card_clicked(card_visual: CardVisual)
|
||||
|
||||
# Zone configuration
|
||||
@export var zone_type: Enums.ZoneType = Enums.ZoneType.HAND
|
||||
@export var player_index: int = 0
|
||||
|
||||
# Layout settings
|
||||
@export var card_spacing: float = 0.7
|
||||
@export var stack_offset: float = 0.02 # Vertical offset for stacked cards
|
||||
@export var max_visible_cards: int = 10
|
||||
@export var fan_angle: float = 5.0 # Degrees for hand fan
|
||||
|
||||
# Position settings
|
||||
@export var zone_position: Vector3 = Vector3.ZERO
|
||||
@export var zone_rotation: float = 0.0 # Y rotation in degrees
|
||||
|
||||
# Card visuals in this zone
|
||||
var card_visuals: Array[CardVisual] = []
|
||||
|
||||
# Zone indicator (optional visual for empty zones)
|
||||
var zone_indicator: MeshInstance3D = null
|
||||
|
||||
func _ready() -> void:
|
||||
position = zone_position
|
||||
rotation.y = deg_to_rad(zone_rotation)
|
||||
_create_zone_indicator()
|
||||
|
||||
func _create_zone_indicator() -> void:
|
||||
zone_indicator = MeshInstance3D.new()
|
||||
add_child(zone_indicator)
|
||||
|
||||
var plane = PlaneMesh.new()
|
||||
plane.size = Vector2(CardVisual.CARD_WIDTH * 1.1, CardVisual.CARD_HEIGHT * 1.1)
|
||||
zone_indicator.mesh = plane
|
||||
|
||||
var mat = StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(0.2, 0.2, 0.3, 0.3)
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
zone_indicator.material_override = mat
|
||||
|
||||
zone_indicator.rotation.x = deg_to_rad(-90) # Lay flat
|
||||
zone_indicator.visible = true
|
||||
|
||||
## Add a card to this zone
|
||||
func add_card(card_instance: CardInstance) -> CardVisual:
|
||||
var card_visual = CardVisual.new()
|
||||
add_child(card_visual)
|
||||
card_visual.setup(card_instance)
|
||||
|
||||
# Connect signals
|
||||
card_visual.clicked.connect(_on_card_clicked)
|
||||
|
||||
card_visuals.append(card_visual)
|
||||
_arrange_cards()
|
||||
|
||||
return card_visual
|
||||
|
||||
## Remove a card from this zone
|
||||
func remove_card(card_visual: CardVisual) -> void:
|
||||
var index = card_visuals.find(card_visual)
|
||||
if index >= 0:
|
||||
card_visuals.remove_at(index)
|
||||
card_visual.queue_free()
|
||||
_arrange_cards()
|
||||
|
||||
## Remove card by instance
|
||||
func remove_card_instance(card_instance: CardInstance) -> CardVisual:
|
||||
for card_visual in card_visuals:
|
||||
if card_visual.card_instance == card_instance:
|
||||
remove_card(card_visual)
|
||||
return card_visual
|
||||
return null
|
||||
|
||||
## Find card visual by instance
|
||||
func find_card_visual(card_instance: CardInstance) -> CardVisual:
|
||||
for card_visual in card_visuals:
|
||||
if card_visual.card_instance == card_instance:
|
||||
return card_visual
|
||||
return null
|
||||
|
||||
## Arrange cards based on zone type
|
||||
func _arrange_cards() -> void:
|
||||
match zone_type:
|
||||
Enums.ZoneType.HAND:
|
||||
_arrange_hand()
|
||||
Enums.ZoneType.DECK, Enums.ZoneType.DAMAGE, Enums.ZoneType.BREAK:
|
||||
_arrange_stack()
|
||||
Enums.ZoneType.FIELD_FORWARDS, Enums.ZoneType.FIELD_BACKUPS:
|
||||
_arrange_field()
|
||||
_:
|
||||
_arrange_row()
|
||||
|
||||
# Update zone indicator visibility
|
||||
zone_indicator.visible = card_visuals.size() == 0
|
||||
|
||||
## Arrange as a fan (for hand)
|
||||
func _arrange_hand() -> void:
|
||||
var count = card_visuals.size()
|
||||
if count == 0:
|
||||
return
|
||||
|
||||
var total_width = (count - 1) * card_spacing
|
||||
var start_x = -total_width / 2
|
||||
|
||||
for i in range(count):
|
||||
var card = card_visuals[i]
|
||||
var x = start_x + i * card_spacing
|
||||
var angle = (i - (count - 1) / 2.0) * fan_angle
|
||||
|
||||
card.move_to(
|
||||
Vector3(x, i * 0.01, 0), # Slight y offset for overlap
|
||||
Vector3(0, 0, deg_to_rad(-angle))
|
||||
)
|
||||
|
||||
## Arrange as a stack (for deck, damage, break zone)
|
||||
func _arrange_stack() -> void:
|
||||
for i in range(card_visuals.size()):
|
||||
var card = card_visuals[i]
|
||||
card.move_to(Vector3(0, i * stack_offset, 0))
|
||||
|
||||
## Arrange in a row (for field zones)
|
||||
func _arrange_field() -> void:
|
||||
var count = card_visuals.size()
|
||||
if count == 0:
|
||||
return
|
||||
|
||||
var total_width = (count - 1) * card_spacing
|
||||
var start_x = -total_width / 2
|
||||
|
||||
for i in range(count):
|
||||
var card = card_visuals[i]
|
||||
var x = start_x + i * card_spacing
|
||||
|
||||
# Apply dull rotation if card is dull
|
||||
var rot_y = 0.0
|
||||
if card.card_instance and card.card_instance.is_dull():
|
||||
rot_y = deg_to_rad(90)
|
||||
|
||||
card.move_to(Vector3(x, 0, 0), Vector3(0, rot_y, 0))
|
||||
|
||||
## Arrange in a simple row
|
||||
func _arrange_row() -> void:
|
||||
var count = card_visuals.size()
|
||||
if count == 0:
|
||||
return
|
||||
|
||||
var total_width = (count - 1) * card_spacing
|
||||
var start_x = -total_width / 2
|
||||
|
||||
for i in range(count):
|
||||
var card = card_visuals[i]
|
||||
card.move_to(Vector3(start_x + i * card_spacing, 0, 0))
|
||||
|
||||
## Clear all cards
|
||||
func clear() -> void:
|
||||
for card_visual in card_visuals:
|
||||
card_visual.queue_free()
|
||||
card_visuals.clear()
|
||||
zone_indicator.visible = true
|
||||
|
||||
## Get card count
|
||||
func get_card_count() -> int:
|
||||
return card_visuals.size()
|
||||
|
||||
## Highlight all cards that can be interacted with
|
||||
func highlight_interactive(predicate: Callable) -> void:
|
||||
for card_visual in card_visuals:
|
||||
var can_interact = predicate.call(card_visual.card_instance)
|
||||
card_visual.set_highlighted(can_interact)
|
||||
|
||||
## Clear all highlights
|
||||
func clear_highlights() -> void:
|
||||
for card_visual in card_visuals:
|
||||
card_visual.set_highlighted(false)
|
||||
card_visual.set_selected(false)
|
||||
|
||||
## Card click handler
|
||||
func _on_card_clicked(card_visual: CardVisual) -> void:
|
||||
card_clicked.emit(card_visual)
|
||||
Reference in New Issue
Block a user