class_name TableSetup extends Node3D ## TableSetup - Sets up the 3D game table with two play mats and 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 (scaled up for card readability) const TABLE_WIDTH: float = 20.0 const TABLE_DEPTH: float = 16.0 # Play mat dimensions (each player gets one) const MAT_WIDTH: float = 18.0 const MAT_DEPTH: float = 7.0 const MAT_MARGIN: float = 0.3 # Gap between mats and from table center # Zone positions (relative to table center, for Player 1) # Player 1 sits at +Z looking toward -Z # Player's left = +X, Player's right = -X const ZONE_POSITIONS = { "damage": Vector3(7.0, 0.1, 1.5), # Front-left (player's left) "deck": Vector3(-7.0, 0.1, 1.5), # Front-right (player's right) "break": Vector3(-7.0, 0.1, 4.0), # Right side, behind deck "forwards": Vector3(0.0, 0.1, 2.0), # Center, front row "backups": Vector3(0.0, 0.1, 5.0), # Center, back row "hand": Vector3(0.0, 0.5, 7.5) # Not used on table (2D overlay) } # Components var table_mesh: MeshInstance3D var player_mat_meshes: Array[MeshInstance3D] = [null, null] var camera: TableCamera func _ready() -> void: _create_table_base() _create_camera() _create_lighting() _create_zones() _create_deck_indicators() _generate_mats() func _create_table_base() -> void: # Base table surface (dark, slightly larger than mats) table_mesh = MeshInstance3D.new() add_child(table_mesh) var plane = PlaneMesh.new() plane.size = Vector2(TABLE_WIDTH, TABLE_DEPTH) table_mesh.mesh = plane var mat = StandardMaterial3D.new() mat.albedo_color = Color(0.08, 0.10, 0.08) # Very dark green base table_mesh.material_override = mat func _create_player_mat(player_idx: int, texture: ImageTexture) -> void: var mat_mesh = MeshInstance3D.new() add_child(mat_mesh) var plane = PlaneMesh.new() plane.size = Vector2(MAT_WIDTH, MAT_DEPTH) mat_mesh.mesh = plane var mat = StandardMaterial3D.new() mat.albedo_texture = texture mat_mesh.material_override = mat # Position each mat on its half of the table var mat_center_z = MAT_DEPTH / 2.0 + MAT_MARGIN if player_idx == 0: mat_mesh.position = Vector3(0, 0.01, mat_center_z) # Player 1: positive Z else: mat_mesh.position = Vector3(0, 0.01, -mat_center_z) # Player 2: negative Z mat_mesh.rotation.y = deg_to_rad(180) # Rotate 180 so labels face Player 2 player_mat_meshes[player_idx] = mat_mesh func _generate_mats() -> void: var renderer = PlaymatRenderer.new() add_child(renderer) var texture = await renderer.render() renderer.queue_free() # Apply texture to both player mats _create_player_mat(0, texture) _create_player_mat(1, texture) 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(1.26, 0.3, 1.76) # 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 = 1.5 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()