Files
FFCardGame/scripts/visual/TableSetup.gd

363 lines
11 KiB
GDScript

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.3, 0.1, 2.1), # Left column, top (forwards row)
"deck": Vector3(7.3, 0.1, 2.0), # Right column, top (forwards row)
"break": Vector3(7.3, 0.1, 5.4), # Right column, centered in break zone box
"forwards": Vector3(0.0, 0.1, 1.9), # Center, front row (near table center)
"backups": Vector3(0.0, 0.1, 4.5), # Center, back row (pulled forward to stay visible above hand)
"hand": Vector3(0.0, 0.5, 7.5) # Not used on table (2D overlay)
}
# Background texture path
const BACKGROUND_TEXTURE_PATH: String = "res://assets/table/background_1.png"
const CARD_BACK_TEXTURE_PATH: String = "res://assets/cards/card_back.png"
# Components
var table_mesh: MeshInstance3D
var background_mesh: MeshInstance3D
var player_mat_meshes: Array[MeshInstance3D] = [null, null]
var camera: TableCamera
func _ready() -> void:
_create_background()
_create_table_base()
_create_camera()
_create_lighting()
_create_zones()
_create_deck_indicators()
_generate_mats()
func _load_background_texture() -> Texture2D:
var texture = load(BACKGROUND_TEXTURE_PATH)
if texture:
return texture
push_warning("Could not load background texture: " + BACKGROUND_TEXTURE_PATH)
return null
func _create_background() -> void:
# Large background plane that fills the view behind the table,
# similar to the wood desk surface in Pokemon TCG Online.
background_mesh = MeshInstance3D.new()
add_child(background_mesh)
var plane = PlaneMesh.new()
plane.size = Vector2(80.0, 80.0) # Large enough to fill camera view
background_mesh.mesh = plane
var mat = StandardMaterial3D.new()
var bg_texture = _load_background_texture()
if bg_texture:
mat.albedo_texture = bg_texture
# Tile the texture so the wood grain repeats naturally
mat.uv1_scale = Vector3(3.0, 3.0, 1.0)
else:
mat.albedo_color = Color(0.25, 0.15, 0.08) # Fallback wood-ish brown
mat.roughness = 0.9
background_mesh.material_override = mat
# Sit slightly below the table surface to avoid z-fighting
background_mesh.position = Vector3(0, -0.05, 0)
func _create_table_base() -> void:
# Base table surface with wood texture, slightly raised above background
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()
var bg_texture = _load_background_texture()
if bg_texture:
mat.albedo_texture = bg_texture
# Slightly darker tint so the table surface is distinct from the background
mat.albedo_color = Color(0.7, 0.65, 0.6)
else:
mat.albedo_color = Color(0.15, 0.1, 0.06)
mat.roughness = 0.85
table_mesh.material_override = mat
table_mesh.position.y = 0.0
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.35, 0.3, 0.25)
environment.ambient_light_energy = 0.6
environment.background_mode = Environment.BG_COLOR
environment.background_color = Color(0.12, 0.08, 0.04) # Dark brown to blend with wood
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:
var card_back_texture = load(CARD_BACK_TEXTURE_PATH)
for player_idx in range(2):
var flip = 1 if player_idx == 0 else -1
var pos = ZONE_POSITIONS["deck"] * Vector3(flip, 1, flip)
# Deck stack (thin box for thickness)
var deck_mesh = MeshInstance3D.new()
add_child(deck_mesh)
var box = BoxMesh.new()
box.size = Vector3(1.6, 0.3, 2.2)
deck_mesh.mesh = box
var side_mat = StandardMaterial3D.new()
side_mat.albedo_color = Color(0.08, 0.06, 0.12) # Dark edges
deck_mesh.material_override = side_mat
deck_mesh.position = pos + Vector3(0, 0.15, 0)
deck_indicators[player_idx] = deck_mesh
# Card back face on top of the deck
var top_card = MeshInstance3D.new()
add_child(top_card)
var plane = PlaneMesh.new()
plane.size = Vector2(1.6, 2.2)
top_card.mesh = plane
var top_mat = StandardMaterial3D.new()
if card_back_texture:
top_mat.albedo_texture = card_back_texture
else:
top_mat.albedo_color = Color(0.15, 0.1, 0.3)
top_card.material_override = top_mat
top_card.position = pos + Vector3(0, 0.31, 0)
if player_idx == 1:
top_card.rotation.y = deg_to_rad(180)
# 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)
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()
# Set properties BEFORE add_child so _ready() uses the correct values
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
add_child(zone)
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()
## Switch camera to a player's perspective
func switch_camera_to_player(player_index: int) -> void:
if camera:
camera.switch_to_player(player_index)