From c95cc511e6839006033b728e018fbbda23b9352b Mon Sep 17 00:00:00 2001 From: itdominator <1itdominator@gmail.com> Date: Wed, 12 Aug 2026 22:14:03 -0500 Subject: [PATCH] feat(game): refactor ball state and turn handling * Replace `valid_end` with `is_foul` for invalid ball sinks * Rename and centralize ball state tracking * Add ball finalization and win-condition hooks * Update turn switching to retain the player after valid sinks * Apply negative points to reaped balls * Add active-player indicator to the camera UI * Emit active-player updates when the turn changes --- controllers/game/modes/8_ball.gd | 35 +++++++++----- controllers/game/modes/9_ball.gd | 35 +++++++++----- controllers/game/modes/base_mode.gd | 53 +++++++++++++-------- controllers/game/modes/free_ball.gd | 27 +++++++---- controllers/game/modes/game_manager_base.gd | 2 +- controllers/game/modes/snooker.gd | 39 +++++++++------ scenes/data/multiplayer_data.gd | 2 + scenes/menus/camera/camera_controller.gd | 21 ++++++-- scenes/menus/camera/camera_controller.tscn | 29 +++++++++-- 9 files changed, 166 insertions(+), 77 deletions(-) diff --git a/controllers/game/modes/8_ball.gd b/controllers/game/modes/8_ball.gd index 6fcd999..78df48a 100644 --- a/controllers/game/modes/8_ball.gd +++ b/controllers/game/modes/8_ball.gd @@ -9,32 +9,43 @@ func reap_ball(ball: Node3D) -> void: if ball.name == "08_ball": # TODO: 8 ball must not be be destroyed. Handle rule... - ball_state[ball.name]["valid_end"] = false - pass + ball_states[ball.name]["is_foul"] = true func process_ball(ball: Node3D) -> void: super(ball) if ball.name == "08_ball": # TODO: 8 ball must be sunk last. Handle rule... - ball_state[ball.name]["valid_end"] = false - pass + ball_states[ball.name]["is_foul"] = true func handle_switch_user() -> void: push_warning("Balls reaped: ", balls_reaped.size() ) push_warning("Balls sunk: ", balls_sunk.size() ) + var switch_player: bool = true if not balls_reaped.is_empty(): - # TODO: Handle points, etc + for ball_name in balls_reaped: + ball_states[ball_name]["points"] = -ball_states[ball_name]["points"] + balls_reaped.clear() - if balls_sunk.is_empty(): - Globals.game_state.switch_active_player() - turn_count += 1 + if not balls_sunk.is_empty(): + # TODO: Handle points, etc, insure 8-ball not sunk unless last + # Insure we process stripped vs solid ball and apropriate points accordingly + # ball_states[ball.name]["is_foul"] = true + + switch_player = false + balls_sunk.clear() + + if ball_states_finalized(): + process_win_condition() return - # TODO: Handle points, etc - # Globals.multiplayer_data.current_turn_player_id + if switch_player: + Globals.game_state.switch_active_player() + turn_count += 1 + else: + turn_count += 1 - balls_sunk.clear() - turn_count += 1 +func process_win_condition() -> void: + pass diff --git a/controllers/game/modes/9_ball.gd b/controllers/game/modes/9_ball.gd index fd7a7b7..dfe6c3c 100644 --- a/controllers/game/modes/9_ball.gd +++ b/controllers/game/modes/9_ball.gd @@ -9,32 +9,43 @@ func reap_ball(ball: Node3D) -> void: if ball.name == "09_ball": # TODO: 9 ball must not be be destroyed. Handle rule... - ball_state[ball.name]["valid_end"] = false - pass + ball_states[ball.name]["is_foul"] = true func process_ball(ball: Node3D) -> void: super(ball) if ball.name == "09_ball": # TODO: 9 ball must be sunk last. Handle rule... - ball_state[ball.name]["valid_end"] = false - pass + ball_states[ball.name]["is_foul"] = true func handle_switch_user() -> void: push_warning("Balls reaped: ", balls_reaped.size() ) push_warning("Balls sunk: ", balls_sunk.size() ) + var switch_player: bool = true if not balls_reaped.is_empty(): - # TODO: Handle points, etc + for ball_name in balls_reaped: + ball_states[ball_name]["points"] = -ball_states[ball_name]["points"] + balls_reaped.clear() - if balls_sunk.is_empty(): - Globals.game_state.switch_active_player() - turn_count += 1 + if not balls_sunk.is_empty(): + # TODO: Handle points, etc, insure 9-ball not sunk unless last. + # Insure we process stripped vs solid ball and apropriate points accordingly. + # ball_states[ball.name]["is_foul"] = true + + switch_player = false + balls_sunk.clear() + + if ball_states_finalized(): + process_win_condition() return - # TODO: Handle points, etc - # Globals.multiplayer_data.current_turn_player_id + if switch_player: + Globals.game_state.switch_active_player() + turn_count += 1 + else: + turn_count += 1 - balls_sunk.clear() - turn_count += 1 +func process_win_condition() -> void: + pass diff --git a/controllers/game/modes/base_mode.gd b/controllers/game/modes/base_mode.gd index 35adf0d..b22457d 100644 --- a/controllers/game/modes/base_mode.gd +++ b/controllers/game/modes/base_mode.gd @@ -4,34 +4,34 @@ class_name BaseMode extends Node # these signals to handle game related rules regarding ball sinks. -var turn_count: int = 1 -var player_1_points: int = 0 -var player_2_points: int = 0 +var turn_count: int = 1 +var player_1_points: int = 0 +var player_2_points: int = 0 # NOTE: Kinda a super tracker of balls. Not all entries # generated are used depending on the game type. -var ball_state: Dictionary = {} +var ball_states: Dictionary = {} # NOTE: Used to track handoff logic and is cleared after eah switch accordingly. -var balls_sunk: Array = [] -var balls_reaped: Array = [] +var balls_sunk: Array = [] +var balls_reaped: Array = [] # NOTE: Kinda a super tracker of the state of a ball. # Not all entries are used depending on the game type. -var BALL_STATE: Dictionary = { - "turn": 0, - "player": "", - "sunk": false, - "reaped": false, - "valid_end": true, - "points": 0, +var BALL_STATE: Dictionary = { + "turn": 0, + "player": "", + "sunk": false, + "reaped": false, + "is_foul": false, + "points": 0, } func _init() -> void: pass -func generate_ball_state() -> Dictionary: +func generate_ball_states() -> Dictionary: var state := {} for i: int in range(1, 16): @@ -41,8 +41,19 @@ func generate_ball_state() -> Dictionary: return state +func ball_states_finalized() -> bool: + for ball_state in ball_states.values(): + if ball_state["reaped"] or ball_state["sunk"]: + continue -func which_player() -> String: + return false + + return true + +func process_win_condition() -> void: + assert(false, "This method needs to be overridden...") + +func get_player() -> String: return \ "Player 1" \ if \ @@ -53,16 +64,16 @@ func which_player() -> String: func reap_ball(ball: Node3D) -> void: balls_reaped.append(ball.name) - ball_state[ball.name]["turn"] = turn_count - ball_state[ball.name]["player"] = which_player() - ball_state[ball.name]["reaped"] = not ball_state[ball.name]["reaped"] + ball_states[ball.name]["turn"] = turn_count + ball_states[ball.name]["player"] = get_player() + ball_states[ball.name]["reaped"] = true func process_ball(ball: Node3D) -> void: balls_sunk.append(ball.name) - ball_state[ball.name]["turn"] = turn_count - ball_state[ball.name]["player"] = which_player() - ball_state[ball.name]["sunk"] = not ball_state[ball.name]["sunk"] + ball_states[ball.name]["turn"] = turn_count + ball_states[ball.name]["player"] = get_player() + ball_states[ball.name]["sunk"] = true 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 index 1968e73..6ff5d76 100644 --- a/controllers/game/modes/free_ball.gd +++ b/controllers/game/modes/free_ball.gd @@ -13,18 +13,29 @@ func process_ball(ball: Node3D) -> void: func handle_switch_user() -> void: push_warning("Balls reaped: ", balls_reaped.size() ) push_warning("Balls sunk: ", balls_sunk.size() ) + var switch_player: bool = true if not balls_reaped.is_empty(): - # TODO: Handle points, etc + for ball_name in balls_reaped: + ball_states[ball_name]["points"] = -ball_states[ball_name]["points"] + balls_reaped.clear() - if balls_sunk.is_empty(): - Globals.game_state.switch_active_player() - turn_count += 1 + if not balls_sunk.is_empty(): + # TODO: Handle points, etc + + switch_player = false + balls_sunk.clear() + + if ball_states_finalized(): + process_win_condition() return - # TODO: Handle points, etc - # Globals.multiplayer_data.current_turn_player_id + if switch_player: + Globals.game_state.switch_active_player() + turn_count += 1 + else: + turn_count += 1 - balls_sunk.clear() - turn_count += 1 +func process_win_condition() -> void: + pass diff --git a/controllers/game/modes/game_manager_base.gd b/controllers/game/modes/game_manager_base.gd index 468808f..d139bd3 100644 --- a/controllers/game/modes/game_manager_base.gd +++ b/controllers/game/modes/game_manager_base.gd @@ -22,4 +22,4 @@ func _balls_stopped_moving() -> void: func set_mode(game_mode) -> void: mode = Globals.game_data.GameMode.get(game_mode).new() - mode.ball_state = mode.generate_ball_state() + mode.ball_states = mode.generate_ball_states() diff --git a/controllers/game/modes/snooker.gd b/controllers/game/modes/snooker.gd index 82c7be2..4ffdd44 100644 --- a/controllers/game/modes/snooker.gd +++ b/controllers/game/modes/snooker.gd @@ -18,14 +18,14 @@ var COLORED_BALLS_POINTS: Array[int] = [ func _init() -> void: pass -func generate_ball_state() -> Dictionary: +func generate_ball_states() -> Dictionary: var state: Dictionary = {} for i in COLORED_BALLS.size(): var ball_name = COLORED_BALLS[i] var points = COLORED_BALLS_POINTS[i] - state[ball_name] = BALL_STATE.duplicate_deep() + state[ball_name] = ball_states.duplicate_deep() state[ball_name]["points"] = points # NOTE: Red balls @@ -34,7 +34,7 @@ func generate_ball_state() -> Dictionary: state[ball_name] = BALL_STATE.duplicate_deep() state[ball_name]["points"] = 1 - ball_state = state + ball_states = state return state func is_colored_ball(ball_name: String) -> bool: @@ -45,8 +45,7 @@ func reap_ball(ball: Node3D) -> void: if is_colored_ball(ball.name): # TODO: Colored ball must not be be destroyed. Handle rule... - ball_state[ball.name]["valid_end"] = false - pass + ball_states[ball.name]["is_foul"] = true func process_ball(ball: Node3D) -> void: super(ball) @@ -56,24 +55,34 @@ func process_ball(ball: Node3D) -> void: # Handle rule... var red_ball_sunk = true if not red_ball_sunk: - pass - pass + ball_states[ball.name]["is_foul"] = true func handle_switch_user() -> void: push_warning("Balls reaped: ", balls_reaped.size() ) push_warning("Balls sunk: ", balls_sunk.size() ) + var switch_player: bool = true if not balls_reaped.is_empty(): - # TODO: Handle points, etc + for ball_name in balls_reaped: + ball_states[ball_name]["points"] = -ball_states[ball_name]["points"] + balls_reaped.clear() - if balls_sunk.is_empty(): - Globals.game_state.switch_active_player() - turn_count += 1 + if not balls_sunk.is_empty(): + # TODO: Handle points, etc, insure color ball not sunk before red ball + + switch_player = false + balls_sunk.clear() + + if ball_states_finalized(): + process_win_condition() return - # TODO: Handle points, etc - # Globals.multiplayer_data.current_turn_player_id + if switch_player: + Globals.game_state.switch_active_player() + turn_count += 1 + else: + turn_count += 1 - balls_sunk.clear() - turn_count += 1 +func process_win_condition() -> void: + pass diff --git a/scenes/data/multiplayer_data.gd b/scenes/data/multiplayer_data.gd index e489fbd..94468dd 100644 --- a/scenes/data/multiplayer_data.gd +++ b/scenes/data/multiplayer_data.gd @@ -22,6 +22,8 @@ func set_active_player_id(id: int) -> void: push_warning("Server assigned active player: ", id) current_turn_player_id = id + MessageBus.emit_local("update_active_lbl", null) + func reset() -> void: multiplayer_synchronizer = MultiplayerSynchronizer.new() diff --git a/scenes/menus/camera/camera_controller.gd b/scenes/menus/camera/camera_controller.gd index 45f74ed..fb0ac5c 100644 --- a/scenes/menus/camera/camera_controller.gd +++ b/scenes/menus/camera/camera_controller.gd @@ -4,20 +4,19 @@ class_name CameraController extends Control @onready var player_lbl: Label = $margin_container/vbox/hbox/hbox/player_lbl @onready var prev_bttn: Button = $margin_container/vbox/hbox/hbox2/prev_bttn @onready var next_bttn: Button = $margin_container/vbox/hbox/hbox2/next_bttn -@onready var reset_bttn: Button = $margin_container/vbox/reset_bttn +@onready var active_lbl: Label = $margin_container/vbox/hbox2/hbox/active_lbl +@onready var reset_bttn: Button = $margin_container/vbox/hbox2/hbox2/reset_bttn @export var cameras: Array[Camera3D] + var cue_cam: Camera3D var current_cam: Camera3D var current_cam_index: int = 0 func _ready() -> void: - Globals.game_state.update_cue_cam_position.connect( - _on_update_cue_cam_position - ) - + setup_signals() setup_multiplayer() for camera in get_children(): @@ -27,6 +26,14 @@ func _ready() -> void: cue_cam = camera current_cam = cameras.get(current_cam_index) + MessageBus.emit_local("update_active_lbl", null) + + +func setup_signals() -> void: + Globals.game_state.update_cue_cam_position.connect( + _on_update_cue_cam_position + ) + MessageBus.subscribe("update_active_lbl", _on_update_active_lbl) func setup_multiplayer() -> void: player_lbl.text = Globals.game_data.player_id @@ -36,6 +43,10 @@ func setup_multiplayer() -> void: reset_bttn.pressed.disconnect(_on_reset_bttn_pressed) reset_bttn.visible = false +func _on_update_active_lbl() -> void: + active_lbl.visible = \ + Globals.multiplayer_data.current_turn_player_id == multiplayer.get_unique_id() + func _on_update_cue_cam_position(marker_rotation: Vector3, marker_position: Vector3) -> void: cue_cam.global_rotation = marker_rotation cue_cam.global_position = marker_position diff --git a/scenes/menus/camera/camera_controller.tscn b/scenes/menus/camera/camera_controller.tscn index 7fb4742..cff96c4 100644 --- a/scenes/menus/camera/camera_controller.tscn +++ b/scenes/menus/camera/camera_controller.tscn @@ -1,10 +1,13 @@ -[gd_scene load_steps=3 format=3 uid="uid://w3vh1gj6t2k2"] +[gd_scene load_steps=4 format=3 uid="uid://w3vh1gj6t2k2"] [ext_resource type="Script" uid="uid://bpf107mmjiswh" path="res://scenes/menus/camera/camera_controller.gd" id="1_mvpa7"] [sub_resource type="LabelSettings" id="LabelSettings_mvpa7"] font_size = 24 +[sub_resource type="LabelSettings" id="LabelSettings_duxs1"] +font_color = Color(0, 1, 0.14117648, 1) + [node name="camera_controller" type="Control"] layout_mode = 3 anchors_preset = 15 @@ -66,7 +69,27 @@ mouse_default_cursor_shape = 2 text = "" flat = true -[node name="reset_bttn" type="Button" parent="margin_container/vbox"] +[node name="hbox2" type="HBoxContainer" parent="margin_container/vbox"] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="hbox" type="HBoxContainer" parent="margin_container/vbox/hbox2"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="active_lbl" type="Label" parent="margin_container/vbox/hbox2/hbox"] +visible = false +layout_mode = 2 +text = "Active" +label_settings = SubResource("LabelSettings_duxs1") +uppercase = true + +[node name="hbox2" type="HBoxContainer" parent="margin_container/vbox/hbox2"] +clip_contents = true +custom_minimum_size = Vector2(149, 0) +layout_mode = 2 + +[node name="reset_bttn" type="Button" parent="margin_container/vbox/hbox2/hbox2"] layout_mode = 2 focus_mode = 0 mouse_default_cursor_shape = 2 @@ -76,4 +99,4 @@ alignment = 0 [connection signal="pressed" from="margin_container/vbox/hbox/hbox2/prev_bttn" to="." method="_on_prev_bttn_pressed"] [connection signal="pressed" from="margin_container/vbox/hbox/hbox2/next_bttn" to="." method="_on_next_bttn_pressed"] -[connection signal="pressed" from="margin_container/vbox/reset_bttn" to="." method="_on_reset_bttn_pressed"] +[connection signal="pressed" from="margin_container/vbox/hbox2/hbox2/reset_bttn" to="." method="_on_reset_bttn_pressed"]