From e3bdb21cd2ab1191358a40589b309054a9c9e8ac Mon Sep 17 00:00:00 2001 From: itdominator <1itdominator@gmail.com> Date: Mon, 10 Aug 2026 21:22:52 -0500 Subject: [PATCH] feat(game): add free ball mode and improve ball state handling * add Free Ball game mode and lobby selection * refactor base mode ball state tracking for reaped/sunk balls * add player ownership tracking and colored-ball support * add initial 9-ball mode implementation * update 8-ball and snooker modes to use base ball handling * replicate ball linear velocity for multiplayer synchronization * improve ball collision sound propagation and physics settings * reenable game music and initialize game signals * improve lobby input focus and mode selection behavior * adjust cue impulse origin and default game mode --- billiards.gd | 10 +++-- controllers/game/modes/8_ball.gd | 14 +++--- controllers/game/modes/9_ball.gd | 32 ++++++++++++++ controllers/game/modes/base_mode.gd | 44 ++++++++++++++++--- controllers/game/modes/free_ball.gd | 25 +++++++++++ controllers/game/modes/free_ball.gd.uid | 1 + controllers/game/modes/free_ball.tscn | 6 +++ controllers/game/modes/game_manager.gd | 19 ++++---- controllers/game/modes/snooker.gd | 30 +++++++------ scenes/data_bridge/game_data.gd | 13 +++--- scenes/data_bridge/multiplayer_data.gd | 16 +++++-- scenes/game.gd | 8 ++-- scenes/individual_parts/balls/balls.gd | 25 ++++++++++- scenes/individual_parts/balls/balls_base.gd | 8 +++- .../cue/cue_tips/pool_cue_tip.gd | 2 +- scenes/screens/lobby_screen/lobby.gd | 12 ++++- scenes/screens/lobby_screen/lobby.tscn | 18 ++++++++ scenes/screens/lobby_screen/lobby_base.gd | 6 +-- scenes/ui_controls/lobby_screen/lobby_ui.tscn | 7 ++- 19 files changed, 234 insertions(+), 62 deletions(-) create mode 100644 controllers/game/modes/free_ball.gd create mode 100644 controllers/game/modes/free_ball.gd.uid create mode 100644 controllers/game/modes/free_ball.tscn diff --git a/billiards.gd b/billiards.gd index 97cfb7b..a48a346 100644 --- a/billiards.gd +++ b/billiards.gd @@ -70,10 +70,14 @@ func _set_game_mode(mode: String) -> void: Globals.game_data.game_mode = "snooker" Globals.game_data.SelectedGameMode = \ Globals.game_data.GameModeType.SNOOKER - _: - Globals.game_data.game_mode = "snooker" + "freeball": + Globals.game_data.game_mode = "freeball" Globals.game_data.SelectedGameMode = \ - Globals.game_data.GameModeType.SNOOKER + Globals.game_data.GameModeType.FREE_BALL + _: + Globals.game_data.game_mode = "freeball" + Globals.game_data.SelectedGameMode = \ + Globals.game_data.GameModeType.FREE_BALL func server_(arg: String) -> void: diff --git a/controllers/game/modes/8_ball.gd b/controllers/game/modes/8_ball.gd index 1b85676..a4ad1e0 100644 --- a/controllers/game/modes/8_ball.gd +++ b/controllers/game/modes/8_ball.gd @@ -2,24 +2,22 @@ class_name Ball8 extends BaseMode func reap_ball(ball: Node3D) -> void: - balls_reaped.append(ball.name) + super(ball) + if ball.name == "08_ball": # TODO: 8 ball must not be be destroyed. Handle rule... pass - ball_state[ball.name] = not ball_state[ball.name] - func process_ball(ball: Node3D) -> void: - balls_sunk.append(ball.name) + super(ball) + if ball.name == "08_ball": # TODO: 8 ball must be sunk last. Handle rule... pass - ball_state[ball.name] = not ball_state[ball.name] - func handle_switch_user() -> void: - push_warning("Balls reaped: ", balls_reaped.size()) - push_warning("Balls sunk: ", balls_sunk.size()) + push_warning("Balls reaped: ", balls_reaped.size() ) + push_warning("Balls sunk: ", balls_sunk.size() ) if not balls_reaped.is_empty(): # TODO: Handle points, etc diff --git a/controllers/game/modes/9_ball.gd b/controllers/game/modes/9_ball.gd index a51f204..dbb577c 100644 --- a/controllers/game/modes/9_ball.gd +++ b/controllers/game/modes/9_ball.gd @@ -1 +1,33 @@ class_name Ball9 extends BaseMode + + +func reap_ball(ball: Node3D) -> void: + super(ball) + + if ball.name == "09_ball": + # TODO: 9 ball must not be be destroyed. Handle rule... + pass + +func process_ball(ball: Node3D) -> void: + super(ball) + + if ball.name == "09_ball": + # TODO: 9 ball must be sunk last. Handle rule... + pass + +func handle_switch_user() -> void: + push_warning("Balls reaped: ", balls_reaped.size() ) + push_warning("Balls sunk: ", balls_sunk.size() ) + + if not balls_reaped.is_empty(): + # TODO: Handle points, etc + balls_reaped.clear() + + if balls_sunk.is_empty(): + Globals.game_state.switch_active_player() + return + + # TODO: Handle points, etc + # Globals.multiplayer_data.current_turn_player_id + + balls_sunk.clear() diff --git a/controllers/game/modes/base_mode.gd b/controllers/game/modes/base_mode.gd index 7a3316a..dd6dccc 100644 --- a/controllers/game/modes/base_mode.gd +++ b/controllers/game/modes/base_mode.gd @@ -4,26 +4,56 @@ class_name BaseMode extends Node # these signals to handle game related rules regarding ball sinks. +var player_1_points: int = 0 +var player_2_points: int = 0 + var ball_state := generate_ball_state() var balls_sunk := [] var balls_reaped := [] var STATE: Dictionary = { - "sunk": false + "sunk": false, + "reaped": false, + "player": "" } +var COLORED_BALLS: Array = [ + "yellow_ball", "green_ball", "brown_ball", + "blue_ball", "pink_ball", "black_ball" +] + func generate_ball_state() -> Dictionary: var state := {} - for i in range(1, 16): - state["%02d_ball" % i] = STATE + + for ball_name: String in COLORED_BALLS: + state[ball_name] = STATE.duplicate_deep() + + for i: int in range(1, 16): + state["%02d_ball" % i] = STATE.duplicate_deep() return state -func reap_ball(_ball: Node3D) -> void: - assert(false, "This method needs to be overridden...") +func is_colored_ball(ball_name: String) -> bool: + return ball_name in COLORED_BALLS -func process_ball(_ball: Node3D) -> void: - assert(false, "This method needs to be overridden...") +func which_player() -> String: + return \ + "Player 1" \ + if \ + Globals.game_data.client1 == Globals.multiplayer_data.current_turn_player_id \ + else \ + "Player 2" + +func reap_ball(ball: Node3D) -> void: + balls_reaped.append(ball.name) + ball_state[ball.name]["reaped"] = not ball_state[ball.name]["reaped"] + ball_state[ball.name]["player"] = which_player() + +func process_ball(ball: Node3D) -> void: + balls_sunk.append(ball.name) + + ball_state[ball.name]["sunk"] = not ball_state[ball.name]["sunk"] + ball_state[ball.name]["player"] = which_player() func handle_switch_user() -> void: assert(false, "This method needs to be overridden...") diff --git a/controllers/game/modes/free_ball.gd b/controllers/game/modes/free_ball.gd new file mode 100644 index 0000000..a4c6c20 --- /dev/null +++ b/controllers/game/modes/free_ball.gd @@ -0,0 +1,25 @@ +class_name FreeBall extends BaseMode + + +func reap_ball(ball: Node3D) -> void: + super(ball) + +func process_ball(ball: Node3D) -> void: + super(ball) + +func handle_switch_user() -> void: + push_warning("Balls reaped: ", balls_reaped.size() ) + push_warning("Balls sunk: ", balls_sunk.size() ) + + if not balls_reaped.is_empty(): + # TODO: Handle points, etc + balls_reaped.clear() + + if balls_sunk.is_empty(): + Globals.game_state.switch_active_player() + return + + # TODO: Handle points, etc + # Globals.multiplayer_data.current_turn_player_id + + balls_sunk.clear() diff --git a/controllers/game/modes/free_ball.gd.uid b/controllers/game/modes/free_ball.gd.uid new file mode 100644 index 0000000..4b02a13 --- /dev/null +++ b/controllers/game/modes/free_ball.gd.uid @@ -0,0 +1 @@ +uid://d3lmdgym2didk diff --git a/controllers/game/modes/free_ball.tscn b/controllers/game/modes/free_ball.tscn new file mode 100644 index 0000000..15e2e68 --- /dev/null +++ b/controllers/game/modes/free_ball.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://c5jdawgemyqu8"] + +[ext_resource type="Script" uid="uid://cnjaupry2q5aj" path="res://controllers/game/modes/snooker.gd" id="1_3fw5q"] + +[node name="free_ball" type="Node"] +script = ExtResource("1_3fw5q") diff --git a/controllers/game/modes/game_manager.gd b/controllers/game/modes/game_manager.gd index ac4a9c3..641358b 100644 --- a/controllers/game/modes/game_manager.gd +++ b/controllers/game/modes/game_manager.gd @@ -7,8 +7,17 @@ func _ready() -> void: func setup_signals() -> void: Globals.game_state.balls_stopped_moving.connect(_balls_stopped_moving) - Globals.game_state.process_ball.connect(_process_ball) Globals.game_state.reap_ball.connect(_reap_ball) + Globals.game_state.process_ball.connect(_process_ball) + + +func _reap_ball(ball: Node3D) -> void: + if ball.name == "white_ball": + Globals.game_state.white_ball_needs_hard_reset.emit() + return + + mode.reap_ball(ball) + rpc("reap_ball", str(ball.get_path())) func _process_ball(ball: Node3D) -> void: if ball.name == "white_ball": @@ -18,11 +27,3 @@ func _process_ball(ball: Node3D) -> void: mode.process_ball(ball) rpc("reap_ball", str(ball.get_path())) - -func _reap_ball(ball: Node3D) -> void: - if ball.name == "white_ball": - Globals.game_state.white_ball_needs_hard_reset.emit() - return - - mode.reap_ball(ball) - rpc("reap_ball", str(ball.get_path())) diff --git a/controllers/game/modes/snooker.gd b/controllers/game/modes/snooker.gd index e97d5c1..aa74007 100644 --- a/controllers/game/modes/snooker.gd +++ b/controllers/game/modes/snooker.gd @@ -1,25 +1,29 @@ class_name Snooker extends BaseMode -func reap_ball(ball: Node3D) -> void: - balls_reaped.append(ball.name) - if ball.name == "08_ball": - # TODO: 8 ball must not be be destroyed. Handle rule... - pass +# NOTE: The game is sometimes played with fewer red balls- commonly 6 or 10. +# Normally, 15 red balls (1 point) and 6 balls of different colors: +# yellow (2 points), green (3), brown (4), blue (5), pink (6), black (7). - ball_state[ball.name] = not ball_state[ball.name] + +func reap_ball(ball: Node3D) -> void: + super(ball) + + if is_colored_ball(ball.name): + # TODO: Colored ball must not be be destroyed. Handle rule... + pass func process_ball(ball: Node3D) -> void: - balls_sunk.append(ball.name) - if ball.name == "08_ball": - # TODO: 8 ball must be sunk last. Handle rule... + super(ball) + + if is_colored_ball(ball.name): + # TODO: Colored ball must not be be processed before red ball sunk. + # Handle rule... pass - ball_state[ball.name] = not ball_state[ball.name] - func handle_switch_user() -> void: - push_warning("Balls reaped: ", balls_reaped.size()) - push_warning("Balls sunk: ", balls_sunk.size()) + push_warning("Balls reaped: ", balls_reaped.size() ) + push_warning("Balls sunk: ", balls_sunk.size() ) if not balls_reaped.is_empty(): # TODO: Handle points, etc diff --git a/scenes/data_bridge/game_data.gd b/scenes/data_bridge/game_data.gd index 5ff979c..d04c6c3 100644 --- a/scenes/data_bridge/game_data.gd +++ b/scenes/data_bridge/game_data.gd @@ -7,7 +7,7 @@ var player_id: String = "" var game_address: String = "0.0.0.0" var game_port: int = 8080 -var game_mode: String = "snooker" +var game_mode: String = "freeball" enum PlayerType { Player1, @@ -17,22 +17,25 @@ enum PlayerType { enum GameModeType { BALL_8, BALL_9, - SNOOKER + SNOOKER, + FREE_BALL } const MATCH_TYPES: Dictionary = { "8ball": GameModeType.BALL_8, "9ball": GameModeType.BALL_9, - "snooker": GameModeType.SNOOKER + "snooker": GameModeType.SNOOKER, + "freeball": GameModeType.FREE_BALL } var GameMode: Dictionary = { GameModeType.BALL_8: Ball8, GameModeType.BALL_9: Ball9, - GameModeType.SNOOKER: Snooker + GameModeType.SNOOKER: Snooker, + GameModeType.FREE_BALL: FreeBall } -var SelectedGameMode: GameModeType = GameModeType.SNOOKER +var SelectedGameMode: GameModeType = GameModeType.FREE_BALL func _init() -> void: diff --git a/scenes/data_bridge/multiplayer_data.gd b/scenes/data_bridge/multiplayer_data.gd index f43feaa..e489fbd 100644 --- a/scenes/data_bridge/multiplayer_data.gd +++ b/scenes/data_bridge/multiplayer_data.gd @@ -104,11 +104,13 @@ func set_tracking_of_cue(path: String) -> void: func set_tracking_of_ball(path: String) -> void: path = str(path).trim_prefix(str(multiplayer_synchronizer.root_path) + "/") - var position_pth = NodePath( path + ":position" ) - var rotation_pth = NodePath( path + ":rotation" ) + var position_pth = NodePath( path + ":position" ) + var rotation_pth = NodePath( path + ":rotation" ) + var linear_velocity_pth = NodePath( path + ":linear_velocity" ) synchronizer_config.add_property(position_pth) synchronizer_config.add_property(rotation_pth) + synchronizer_config.add_property(linear_velocity_pth) synchronizer_config.property_set_replication_mode( position_pth, SceneReplicationConfig.REPLICATION_MODE_ON_CHANGE @@ -116,14 +118,20 @@ func set_tracking_of_ball(path: String) -> void: synchronizer_config.property_set_replication_mode( rotation_pth, SceneReplicationConfig.REPLICATION_MODE_ON_CHANGE ) + synchronizer_config.property_set_replication_mode( + linear_velocity_pth, SceneReplicationConfig.REPLICATION_MODE_ON_CHANGE + ) func remove_tracking_of_ball(path: String) -> void: path = str(path).trim_prefix(str(multiplayer_synchronizer.root_path) + "/") - var position_pth = NodePath(path + ":position") - var rotation_pth = NodePath(path + ":rotation") + var position_pth = NodePath( path + ":position" ) + var rotation_pth = NodePath( path + ":rotation" ) + var linear_velocity_pth = NodePath( path + ":linear_velocity" ) if synchronizer_config.has_property(position_pth): synchronizer_config.remove_property(position_pth) if synchronizer_config.has_property(rotation_pth): synchronizer_config.remove_property(rotation_pth) + if synchronizer_config.has_property(linear_velocity_pth): + synchronizer_config.remove_property(linear_velocity_pth) diff --git a/scenes/game.gd b/scenes/game.gd index ff5840f..18f5efd 100644 --- a/scenes/game.gd +++ b/scenes/game.gd @@ -6,12 +6,12 @@ class_name Game extends Node3D func _ready() -> void: - # TODO: Uncomment - #base_environment.game_music.stream = load("res://sounds/music/Lobo Loco - 04 (Mountain Bells22).mp3") - #base_environment.game_music.stream.loop = true - #base_environment.game_music.play() + base_environment.game_music.stream = load("res://sounds/music/Lobo Loco - 04 (Mountain Bells22).mp3") + base_environment.game_music.stream.loop = true + base_environment.game_music.play() game_manager.set_mode(Globals.game_data.SelectedGameMode) + setup_signals() setup_multiplayer() diff --git a/scenes/individual_parts/balls/balls.gd b/scenes/individual_parts/balls/balls.gd index e89c992..d7a054c 100644 --- a/scenes/individual_parts/balls/balls.gd +++ b/scenes/individual_parts/balls/balls.gd @@ -5,8 +5,20 @@ func _ready() -> void: for ball in bodies.get_children(): _set_ball_physics(ball) + setup_signals() setup_multiplayer() +func setup_signals() -> void: + # TODO: Need to refactor to not need server signal collision to + # then play sounds. Maybe 2nd collider on ball that triggers? + # Will that work since with clients we: + # set_deferred("freeze", true) + # set_physics_process(false) + # + # Gods forgive me for my sins.... + MessageBus.subscribe("balls_collide_signal", _on_balls_collide_signal) + + func _physics_process(_delta: float) -> void: if not Globals.multiplayer_data.can_do_request_server_only(): return @@ -28,8 +40,19 @@ func _on__ball_body_entered(ball: Node) -> void: if not Globals.multiplayer_data.can_do_request_server_only(): return - if ball.name == "pool_table": return + if ball.name == "pool_table": + # TODO: Get/generate some type of sound/noise like corse texture of table + return + _play_collide_sound(ball) + MessageBus.emit_propagation_clients_only.rpc( + "balls_collide_signal", str( ball.get_path() ) + ) + +func _on_balls_collide_signal(ball: String) -> void: + _play_collide_sound( get_node(ball) ) + +func _play_collide_sound(ball: Node) -> void: var speed = ball.linear_velocity.length() var speed_norm = clamp(speed / max_speed, 0.0, 1.0) var volume = remap( diff --git a/scenes/individual_parts/balls/balls_base.gd b/scenes/individual_parts/balls/balls_base.gd index df05124..357abe9 100644 --- a/scenes/individual_parts/balls/balls_base.gd +++ b/scenes/individual_parts/balls/balls_base.gd @@ -20,6 +20,7 @@ class_name BallsBase extends Node3D var balls_moving: bool = false + func _set_ball_physics(ball: RigidBody3D) -> void: var phy_mat = PhysicsMaterial.new() @@ -29,11 +30,16 @@ func _set_ball_physics(ball: RigidBody3D) -> void: ball.set_physics_material_override(phy_mat) - ball.axis_lock_linear_y = true + ball.axis_lock_linear_y = true ball.set_mass(ball_mass) ball.set_linear_damp(2.04) ball.set_angular_damp(1.44) + ball.set_use_continuous_collision_detection(true) + ball.contact_monitor = true + # NOTE: How many balls can surround a single ball, + # therefor worst case is 6 collisions. (think start of game) + ball.max_contacts_reported = 6 ball.body_entered.connect(_on__ball_body_entered) diff --git a/scenes/individual_parts/cue/cue_tips/pool_cue_tip.gd b/scenes/individual_parts/cue/cue_tips/pool_cue_tip.gd index 345e7d6..11260b5 100644 --- a/scenes/individual_parts/cue/cue_tips/pool_cue_tip.gd +++ b/scenes/individual_parts/cue/cue_tips/pool_cue_tip.gd @@ -23,7 +23,7 @@ func _on_body_entered(body: Node) -> void: body.apply_impulse( impulse, - global_position - body.global_position + body.global_position - $shape.global_position ) #body.apply_central_impulse(impulse) diff --git a/scenes/screens/lobby_screen/lobby.gd b/scenes/screens/lobby_screen/lobby.gd index c729ab5..127178d 100644 --- a/scenes/screens/lobby_screen/lobby.gd +++ b/scenes/screens/lobby_screen/lobby.gd @@ -4,6 +4,8 @@ class_name Lobby extends LobbyBase func _ready() -> void: setup_signals() + host_address_input.grab_focus() + func _exit_tree() -> void: # TODO: Maybe no longer needed... if Globals.multiplayer_data.is_multiplayer_active: return @@ -38,6 +40,7 @@ func _on_receive_match_list(matches_list: Array) -> void: _create_match_entry(match_entry) client.lobby_fully_connected = true + new_match_name_input.grab_focus() func _on_match_list_entry_activated(match_id: String) -> void: for match_entry in match_list.get_children(): @@ -48,6 +51,8 @@ func _on_match_list_entry_activated(match_id: String) -> void: match_entry.join_bttn.text = "Delete" match_entry.join_bttn.visible = true match_create_vbox.visible = false + match_search_input.visible = false + match_search_bttn.visible = false match_entry.join_bttn.pressed.disconnect(_join_match) match_entry.join_bttn.pressed.connect( @@ -62,8 +67,11 @@ func _on_match_list_entry_removed(match_id: String) -> void: push_warning("Client: Removed Match List Entry...", match_id) if active_match && (match_id == active_match.match_id): - active_match = null - match_create_vbox.visible = true + active_match = null + match_create_vbox.visible = true + match_search_input.visible = true + match_search_bttn.visible = false + new_match_name_input.grab_focus() for match_entry in match_list.get_children(): if not active_match: diff --git a/scenes/screens/lobby_screen/lobby.tscn b/scenes/screens/lobby_screen/lobby.tscn index c58311b..0ec1413 100644 --- a/scenes/screens/lobby_screen/lobby.tscn +++ b/scenes/screens/lobby_screen/lobby.tscn @@ -10,6 +10,23 @@ script = ExtResource("1_hhiik") [node name="ui" parent="." instance=ExtResource("2_lqt48")] +[node name="host_address_input" parent="ui/body/left_body/server_host_hbox" index="1"] +context_menu_enabled = false +select_all_on_focus = true + +[node name="host_port_range" parent="ui/body/left_body/server_host_hbox" index="2"] +select_all_on_focus = true + +[node name="match_search_input" parent="ui/body/left_body/search_vbox/search_hbox" index="0"] +context_menu_enabled = false + +[node name="right_body" parent="ui/body" index="1"] +visible = false + +[node name="new_match_name_input" parent="ui/body/right_body/create_match_hbox" index="0"] +context_menu_enabled = false +select_all_on_focus = true + [node name="rpc_signals" type="Node" parent="."] script = ExtResource("3_rurl6") @@ -24,5 +41,6 @@ script = ExtResource("4_glu7g") [connection signal="pressed" from="ui/body/right_body/vbox/8ball" to="." method="_on_mode_bttn_pressed" flags=18] [connection signal="pressed" from="ui/body/right_body/vbox/9ball" to="." method="_on_mode_bttn_pressed" flags=18] [connection signal="pressed" from="ui/body/right_body/vbox/snooker" to="." method="_on_mode_bttn_pressed" flags=18] +[connection signal="pressed" from="ui/body/right_body/vbox/free_ball" to="." method="_on_mode_bttn_pressed" flags=18] [editable path="ui"] diff --git a/scenes/screens/lobby_screen/lobby_base.gd b/scenes/screens/lobby_screen/lobby_base.gd index 81635d4..71ff18a 100644 --- a/scenes/screens/lobby_screen/lobby_base.gd +++ b/scenes/screens/lobby_screen/lobby_base.gd @@ -10,6 +10,7 @@ const MATCH_ENTRY = preload("res://scenes/screens/lobb @onready var search_vbox: VBoxContainer = $ui/body/left_body/search_vbox @onready var match_search_input: LineEdit = $ui/body/left_body/search_vbox/search_hbox/match_search_input +@onready var match_search_bttn: Button = $ui/body/left_body/search_vbox/search_hbox/match_search_bttn @onready var match_list: VBoxContainer = $ui/body/left_body/search_vbox/scroll_container/match_list @onready var match_create_vbox: VBoxContainer = $ui/body/right_body @@ -18,7 +19,7 @@ const MATCH_ENTRY = preload("res://scenes/screens/lobb @onready var rpc_signals: Node = $rpc_signals @onready var client: LobbyClient = $client -var active_mode: String = "snooker" +var active_mode: String = "freeball" var active_match: HBoxContainer = null @@ -56,8 +57,7 @@ func _on_match_create_bttn_pressed() -> void: "type": active_mode } - #MessageBus.emit_request.rpc_id(1, "match_list_add_entry", match_entry) MessageBus.emit_request.rpc("match_list_add_entry", match_entry) func _on_mode_bttn_pressed(button: Button) -> void: - active_mode = button.text.remove_chars("-").to_lower() + active_mode = button.name.remove_chars("_").to_lower() diff --git a/scenes/ui_controls/lobby_screen/lobby_ui.tscn b/scenes/ui_controls/lobby_screen/lobby_ui.tscn index 4c9dd4e..e6a4808 100644 --- a/scenes/ui_controls/lobby_screen/lobby_ui.tscn +++ b/scenes/ui_controls/lobby_screen/lobby_ui.tscn @@ -115,7 +115,6 @@ size_flags_horizontal = 3 size_flags_vertical = 3 [node name="right_body" type="VBoxContainer" parent="body"] -visible = false layout_mode = 2 size_flags_horizontal = 3 @@ -156,4 +155,10 @@ text = "9-Ball" [node name="snooker" type="Button" parent="body/right_body/vbox"] layout_mode = 2 mouse_default_cursor_shape = 2 +disabled = true text = "Snooker" + +[node name="free_ball" type="Button" parent="body/right_body/vbox"] +layout_mode = 2 +mouse_default_cursor_shape = 2 +text = "Free Ball"