336 lines
10 KiB
GDScript
336 lines
10 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.0, 0.1, 1.5), # Front-left (player's left)
|
|
"deck": Vector3(-7.0, 0.1, 4.0), # Back-right (player's right, behind backups)
|
|
"break": Vector3(-7.0, 0.1, 1.5), # Front-right (player's right, front row)
|
|
"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)
|
|
}
|
|
|
|
# Background texture path
|
|
const BACKGROUND_TEXTURE_PATH: String = "res://assets/table/background_1.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:
|
|
# 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()
|