init game files
This commit is contained in:
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