init game files

This commit is contained in:
2026-01-24 16:29:11 -05:00
commit ea2028cf13
171 changed files with 191733 additions and 0 deletions

View 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()
}

View 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()

View 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()

View 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)