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 (doubled for readability, standard card ratio ~2.5:3.5) const CARD_WIDTH: float = 1.26 const CARD_HEIGHT: float = 1.76 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 base_position: Vector3 = Vector3.ZERO # Position set by zone arrangement (before hover offset) var move_speed: float = 10.0 var is_animating: bool = false const HOVER_LIFT: float = 0.15 # 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 a quad mesh for the card face (more appropriate for card textures) var quad = QuadMesh.new() quad.size = Vector2(CARD_WIDTH, CARD_HEIGHT) mesh_instance.mesh = quad # Rotate to lay flat on table (facing up) mesh_instance.rotation_degrees.x = -90 # Slight offset up so it's above the table mesh_instance.position.y = CARD_THICKNESS / 2 # Create material material = StandardMaterial3D.new() material.albedo_color = Color.WHITE material.cull_mode = BaseMaterial3D.CULL_DISABLED # Show both sides 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 material.albedo_color = Color.WHITE # Reset to white so texture shows properly else: # Use element color as fallback when no texture material.albedo_texture = null 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 # Only apply color tints if we don't have a texture, or for highlight/dull effects var has_texture = material.albedo_texture != null var base_color = Color.WHITE if has_texture else normal_color if is_selected: # Slight green tint for selection base_color = base_color * Color(0.9, 1.1, 0.9) elif is_highlighted: # Slight yellow tint for highlight base_color = base_color * Color(1.1, 1.1, 0.9) # Apply dull tint (darkening) if card_instance and card_instance.is_dull(): base_color = base_color * dull_tint material.albedo_color = base_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: base_position = pos target_position = pos if is_hovered: target_position.y = pos.y + HOVER_LIFT target_rotation = rot is_animating = true ## Move card instantly func set_position_instant(pos: Vector3, rot: Vector3 = Vector3.ZERO) -> void: base_position = pos 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) # Raise card above base position on hover target_position.y = base_position.y + HOVER_LIFT is_animating = true func _on_mouse_exited() -> void: is_hovered = false unhovered.emit(self) # Return to base height target_position.y = base_position.y 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() }