From bba93eb3e56696cfe278e718db6309c9cc20bd1d Mon Sep 17 00:00:00 2001 From: Voyager Date: Sat, 25 Jul 2026 11:05:37 -0400 Subject: [PATCH] Add minimal Bluegill fishing loop --- fish/bluegill.tres | 8 ++ fish/fish_data.gd | 6 + fish/fish_data.gd.uid | 1 + fishing/fishing_spot.gd | 207 ++++++++++++++++++++++++++++++++ fishing/fishing_spot.gd.uid | 1 + fishing/fishing_spot.tscn | 44 +++++++ inventory/fish_inventory.gd | 21 ++++ inventory/fish_inventory.gd.uid | 1 + main/main.gd | 13 ++ main/main.gd.uid | 1 + main/main.tscn | 14 ++- player/player.gd | 46 ++++++- player/player.tscn | 8 +- ui/game_ui.gd | 35 ++++++ ui/game_ui.gd.uid | 1 + ui/game_ui.tscn | 57 +++++++++ 16 files changed, 461 insertions(+), 3 deletions(-) create mode 100644 fish/bluegill.tres create mode 100644 fish/fish_data.gd create mode 100644 fish/fish_data.gd.uid create mode 100644 fishing/fishing_spot.gd create mode 100644 fishing/fishing_spot.gd.uid create mode 100644 fishing/fishing_spot.tscn create mode 100644 inventory/fish_inventory.gd create mode 100644 inventory/fish_inventory.gd.uid create mode 100644 main/main.gd create mode 100644 main/main.gd.uid create mode 100644 ui/game_ui.gd create mode 100644 ui/game_ui.gd.uid create mode 100644 ui/game_ui.tscn diff --git a/fish/bluegill.tres b/fish/bluegill.tres new file mode 100644 index 0000000..b44c46a --- /dev/null +++ b/fish/bluegill.tres @@ -0,0 +1,8 @@ +[gd_resource type="Resource" load_steps=2 format=3] + +[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] + +[resource] +script = ExtResource("1_fish_data") +id = &"bluegill" +display_name = "Bluegill" diff --git a/fish/fish_data.gd b/fish/fish_data.gd new file mode 100644 index 0000000..cff104a --- /dev/null +++ b/fish/fish_data.gd @@ -0,0 +1,6 @@ +class_name FishData +extends Resource + +@export var id: StringName +@export var display_name: String +@export var icon: Texture2D diff --git a/fish/fish_data.gd.uid b/fish/fish_data.gd.uid new file mode 100644 index 0000000..1bca6ed --- /dev/null +++ b/fish/fish_data.gd.uid @@ -0,0 +1 @@ +uid://h7hg45b7wmst diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd new file mode 100644 index 0000000..6b4d3d3 --- /dev/null +++ b/fishing/fishing_spot.gd @@ -0,0 +1,207 @@ +class_name FishingSpot +extends Area3D + +const FishDataType = preload("res://fish/fish_data.gd") +const PlayerType = preload("res://player/player.gd") + +signal status_changed(status: String) +signal reel_progress_changed(progress: float, visible: bool) + +enum FishingState { + READY, + WAITING_FOR_BITE, + BITE_ACTIVE, + REELING, + COOLDOWN, +} + +@export_category("Timing") +@export_range(0.1, 30.0, 0.1) var wait_time: float = 2.0 +@export_range(0.1, 10.0, 0.1) var bite_window_duration: float = 1.5 +@export_range(0.1, 10.0, 0.1) var cooldown_duration: float = 1.0 +@export_range(0.01, 5.0, 0.01) var reel_gain_rate: float = 0.67 +@export_range(0.01, 5.0, 0.01) var reel_decay_rate: float = 0.2 +@export_range(0.0, 0.95, 0.05) var hooked_starting_progress: float = 0.25 + +@export_category("Catch") +@export var catchable_fish: FishDataType + +var state: FishingState = FishingState.READY +var _nearby_player: PlayerType +var _active_player: PlayerType +var _state_time_remaining: float = 0.0 +var _reel_progress: float = 0.0 +var _reel_input_held: bool = false +var _cooldown_status: String = "" + + +func _ready() -> void: + body_entered.connect(_on_body_entered) + body_exited.connect(_on_body_exited) + + +func _process(delta: float) -> void: + match state: + FishingState.WAITING_FOR_BITE: + _state_time_remaining -= delta + if _state_time_remaining <= 0.0: + _activate_bite() + FishingState.BITE_ACTIVE: + _state_time_remaining -= delta + if _state_time_remaining <= 0.0: + _miss_bite() + FishingState.REELING: + if _reel_input_held and not Input.is_action_pressed("fish_primary"): + _reel_input_held = false + _update_reel_progress(delta) + FishingState.COOLDOWN: + _state_time_remaining -= delta + if _state_time_remaining <= 0.0: + _return_to_ready() + + +func _unhandled_input(event: InputEvent) -> void: + if _nearby_player == null or not _nearby_player.is_local_control_enabled(): + return + if not event.is_action("fish_primary"): + return + + if event.is_pressed(): + match state: + FishingState.READY: + _begin_fishing(_nearby_player) + FishingState.BITE_ACTIVE: + _confirm_hook() + FishingState.REELING: + _reel_input_held = true + _: + return + get_viewport().set_input_as_handled() + elif state == FishingState.REELING: + _reel_input_held = false + get_viewport().set_input_as_handled() + + +func _begin_fishing(player: PlayerType) -> void: + if state != FishingState.READY or _active_player != null: + return + + _active_player = player + _active_player.set_movement_enabled(false) + state = FishingState.WAITING_FOR_BITE + _state_time_remaining = wait_time + status_changed.emit("Waiting for a bite...") + + +func _activate_bite() -> void: + if state != FishingState.WAITING_FOR_BITE: + return + + state = FishingState.BITE_ACTIVE + _state_time_remaining = bite_window_duration + status_changed.emit("Bite! Left click to hook") + + +func _confirm_hook() -> void: + if state != FishingState.BITE_ACTIVE or _active_player == null: + return + + state = FishingState.REELING + _state_time_remaining = 0.0 + _reel_progress = clampf(hooked_starting_progress, 0.0, 0.95) + _reel_input_held = true + status_changed.emit("Hold left click to reel") + reel_progress_changed.emit(_reel_progress, true) + + +func _update_reel_progress(delta: float) -> void: + if state != FishingState.REELING: + return + + if _reel_input_held: + _reel_progress = minf(_reel_progress + reel_gain_rate * delta, 1.0) + else: + _reel_progress = maxf(_reel_progress - reel_decay_rate * delta, 0.0) + reel_progress_changed.emit(_reel_progress, true) + if is_equal_approx(_reel_progress, 1.0): + _resolve_catch() + elif is_zero_approx(_reel_progress): + _resolve_escape() + + +func _resolve_catch() -> void: + if state != FishingState.REELING or _active_player == null or catchable_fish == null: + _cancel_attempt() + return + + _active_player.inventory.add_fish(catchable_fish) + _cleanup_attempt("Caught %s!" % catchable_fish.display_name) + + +func _resolve_escape() -> void: + if state != FishingState.REELING: + return + + var fish_name: String = "The fish" + if catchable_fish != null and not catchable_fish.display_name.is_empty(): + fish_name = "The %s" % catchable_fish.display_name + _cleanup_attempt("%s got away!" % fish_name) + + +func _miss_bite() -> void: + if state != FishingState.BITE_ACTIVE: + return + _cleanup_attempt("The fish got away.") + + +func _cleanup_attempt(cooldown_message: String = "") -> void: + if _active_player != null: + _active_player.set_movement_enabled(true) + _active_player = null + _reel_input_held = false + _reel_progress = 0.0 + reel_progress_changed.emit(0.0, false) + + if cooldown_message.is_empty(): + state = FishingState.READY + _state_time_remaining = 0.0 + _cooldown_status = "" + if _nearby_player != null: + status_changed.emit("Left click to cast") + else: + status_changed.emit("") + else: + state = FishingState.COOLDOWN + _state_time_remaining = cooldown_duration + _cooldown_status = cooldown_message + status_changed.emit(_cooldown_status) + + +func _return_to_ready() -> void: + _cleanup_attempt() + + +func _cancel_attempt() -> void: + _cleanup_attempt() + + +func _on_body_entered(body: Node3D) -> void: + if body is not PlayerType or not body.is_local_control_enabled(): + return + if _nearby_player != null: + return + + _nearby_player = body + if state == FishingState.READY: + status_changed.emit("Left click to cast") + + +func _on_body_exited(body: Node3D) -> void: + if body != _nearby_player: + return + + _nearby_player = null + if body == _active_player: + _cancel_attempt() + else: + status_changed.emit("") diff --git a/fishing/fishing_spot.gd.uid b/fishing/fishing_spot.gd.uid new file mode 100644 index 0000000..79fe1f5 --- /dev/null +++ b/fishing/fishing_spot.gd.uid @@ -0,0 +1 @@ +uid://c3s0ib16vmyne diff --git a/fishing/fishing_spot.tscn b/fishing/fishing_spot.tscn new file mode 100644 index 0000000..13a22b0 --- /dev/null +++ b/fishing/fishing_spot.tscn @@ -0,0 +1,44 @@ +[gd_scene load_steps=7 format=3] + +[ext_resource type="Script" path="res://fishing/fishing_spot.gd" id="1_spot"] +[ext_resource type="Resource" path="res://fish/bluegill.tres" id="2_bluegill"] + +[sub_resource type="SphereShape3D" id="RangeShape"] +radius = 3.5 + +[sub_resource type="CylinderMesh" id="MarkerPostMesh"] +top_radius = 0.12 +bottom_radius = 0.12 +height = 2.0 + +[sub_resource type="TorusMesh" id="RangeMarkerMesh"] +inner_radius = 3.35 +outer_radius = 3.5 +rings = 32 + +[sub_resource type="StandardMaterial3D" id="MarkerMaterial"] +albedo_color = Color(0.95, 0.82, 0.18, 1) +emission_enabled = true +emission = Color(0.3, 0.2, 0.01, 1) +emission_energy_multiplier = 0.7 + +[node name="FishingSpot" type="Area3D"] +collision_layer = 0 +collision_mask = 2 +monitoring = true +monitorable = false +script = ExtResource("1_spot") +catchable_fish = ExtResource("2_bluegill") + +[node name="RangeCollision" type="CollisionShape3D" parent="."] +shape = SubResource("RangeShape") + +[node name="MarkerPost" type="MeshInstance3D" parent="."] +position = Vector3(0, 1, 0) +mesh = SubResource("MarkerPostMesh") +material_override = SubResource("MarkerMaterial") + +[node name="RangeMarker" type="MeshInstance3D" parent="."] +position = Vector3(0, 0.03, 0) +mesh = SubResource("RangeMarkerMesh") +material_override = SubResource("MarkerMaterial") diff --git a/inventory/fish_inventory.gd b/inventory/fish_inventory.gd new file mode 100644 index 0000000..6fe7456 --- /dev/null +++ b/inventory/fish_inventory.gd @@ -0,0 +1,21 @@ +class_name FishInventory +extends Node + +const FishDataType = preload("res://fish/fish_data.gd") + +signal contents_changed(fish_id: StringName, count: int) + +var _fish_counts: Dictionary[StringName, int] = {} + + +func add_fish(fish: FishDataType, amount: int = 1) -> void: + if fish == null or fish.id.is_empty() or amount <= 0: + return + + var new_count: int = get_count(fish.id) + amount + _fish_counts[fish.id] = new_count + contents_changed.emit(fish.id, new_count) + + +func get_count(fish_id: StringName) -> int: + return _fish_counts.get(fish_id, 0) diff --git a/inventory/fish_inventory.gd.uid b/inventory/fish_inventory.gd.uid new file mode 100644 index 0000000..3428f74 --- /dev/null +++ b/inventory/fish_inventory.gd.uid @@ -0,0 +1 @@ +uid://t4q3l16td1yw diff --git a/main/main.gd b/main/main.gd new file mode 100644 index 0000000..0944cd7 --- /dev/null +++ b/main/main.gd @@ -0,0 +1,13 @@ +extends Node3D + +const FishingSpotType = preload("res://fishing/fishing_spot.gd") +const GameUIType = preload("res://ui/game_ui.gd") +const PlayerType = preload("res://player/player.gd") + +@onready var _player: PlayerType = %Player +@onready var _fishing_spot: FishingSpotType = %FishingSpot +@onready var _game_ui: GameUIType = %GameUI + + +func _ready() -> void: + _game_ui.setup(_player.inventory, _fishing_spot) diff --git a/main/main.gd.uid b/main/main.gd.uid new file mode 100644 index 0000000..4183ea9 --- /dev/null +++ b/main/main.gd.uid @@ -0,0 +1 @@ +uid://c7u6jpr3f33t3 diff --git a/main/main.tscn b/main/main.tscn index 2b62ed9..067c9a5 100644 --- a/main/main.tscn +++ b/main/main.tscn @@ -1,13 +1,25 @@ -[gd_scene load_steps=3 format=3] +[gd_scene load_steps=6 format=3] [ext_resource type="PackedScene" path="res://world/test_world.tscn" id="1_world"] [ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"] +[ext_resource type="Script" path="res://main/main.gd" id="3_main"] +[ext_resource type="PackedScene" path="res://fishing/fishing_spot.tscn" id="4_spot"] +[ext_resource type="PackedScene" path="res://ui/game_ui.tscn" id="5_ui"] [node name="Main" type="Node3D"] +script = ExtResource("3_main") [node name="TestWorld" parent="." instance=ExtResource("1_world")] [node name="Players" type="Node3D" parent="."] [node name="Player" parent="Players" instance=ExtResource("2_player")] +unique_name_in_owner = true transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 5) + +[node name="FishingSpot" parent="." instance=ExtResource("4_spot")] +unique_name_in_owner = true +position = Vector3(0, 1.02, -18) + +[node name="GameUI" parent="." instance=ExtResource("5_ui")] +unique_name_in_owner = true diff --git a/player/player.gd b/player/player.gd index 3389b8a..9b36022 100644 --- a/player/player.gd +++ b/player/player.gd @@ -1,6 +1,8 @@ class_name Player extends CharacterBody3D +const FishInventoryType = preload("res://inventory/fish_inventory.gd") + @export_category("Movement") @export var walk_speed: float = 5.0 @export var sprint_speed: float = 8.0 @@ -9,6 +11,9 @@ extends CharacterBody3D @export var jump_velocity: float = 6.0 @export var body_rotation_speed: float = 12.0 +@export_category("Control") +@export var local_control_enabled: bool = true + @export_category("Camera") @export var mouse_sensitivity: float = 0.005 @export var controller_camera_speed: float = 2.5 @@ -24,23 +29,37 @@ extends CharacterBody3D @onready var _camera_yaw: Node3D = %CameraYaw @onready var _camera_pitch: Node3D = %CameraPitch @onready var _spring_arm: SpringArm3D = %SpringArm3D +@onready var _camera: Camera3D = %Camera3D +@onready var inventory: FishInventoryType = %Inventory var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity")) var _camera_dragging: bool = false +var _movement_enabled: bool = true var _target_zoom: float = 5.0 func _ready() -> void: _target_zoom = clampf(_spring_arm.spring_length, minimum_zoom, maximum_zoom) _spring_arm.spring_length = _target_zoom + _camera.current = local_control_enabled func _physics_process(delta: float) -> void: if not is_on_floor(): velocity.y -= _gravity * delta - elif Input.is_action_just_pressed("jump"): + elif ( + local_control_enabled + and _movement_enabled + and Input.is_action_just_pressed("jump") + ): velocity.y = jump_velocity + if not local_control_enabled or not _movement_enabled: + velocity.x = 0.0 + velocity.z = 0.0 + move_and_slide() + return + var input_vector: Vector2 = Input.get_vector( "move_left", "move_right", @@ -69,6 +88,9 @@ func _physics_process(delta: float) -> void: func _process(delta: float) -> void: + if not local_control_enabled: + return + if _camera_dragging and not Input.is_action_pressed("camera_drag"): _camera_dragging = false @@ -88,6 +110,9 @@ func _process(delta: float) -> void: func _unhandled_input(event: InputEvent) -> void: + if not local_control_enabled: + return + if event.is_action("camera_drag"): _camera_dragging = event.is_pressed() get_viewport().set_input_as_handled() @@ -127,3 +152,22 @@ func _rotate_camera(delta_rotation: Vector2) -> void: func _set_target_zoom(value: float) -> void: _target_zoom = clampf(value, minimum_zoom, maximum_zoom) + + +func set_local_control(enabled: bool) -> void: + local_control_enabled = enabled + if is_node_ready(): + _camera.current = enabled + if not enabled: + _camera_dragging = false + + +func is_local_control_enabled() -> bool: + return local_control_enabled + + +func set_movement_enabled(enabled: bool) -> void: + _movement_enabled = enabled + if not enabled: + velocity.x = 0.0 + velocity.z = 0.0 diff --git a/player/player.tscn b/player/player.tscn index 7b9259a..9d31fb0 100644 --- a/player/player.tscn +++ b/player/player.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=7 format=3] +[gd_scene load_steps=8 format=3] [ext_resource type="Script" path="res://player/player.gd" id="1_script"] +[ext_resource type="Script" path="res://inventory/fish_inventory.gd" id="2_inventory"] [sub_resource type="CapsuleShape3D" id="PlayerShape"] radius = 0.45 @@ -41,6 +42,10 @@ position = Vector3(0, 1.0, -0.55) mesh = SubResource("FacingMarkerMesh") material_override = SubResource("FacingMarkerMaterial") +[node name="Inventory" type="Node" parent="."] +unique_name_in_owner = true +script = ExtResource("2_inventory") + [node name="CameraYaw" type="Node3D" parent="."] unique_name_in_owner = true position = Vector3(0, 1.35, 0) @@ -56,4 +61,5 @@ margin = 0.15 collision_mask = 1 [node name="Camera3D" type="Camera3D" parent="CameraYaw/CameraPitch/SpringArm3D"] +unique_name_in_owner = true current = true diff --git a/ui/game_ui.gd b/ui/game_ui.gd new file mode 100644 index 0000000..fde6f27 --- /dev/null +++ b/ui/game_ui.gd @@ -0,0 +1,35 @@ +class_name GameUI +extends CanvasLayer + +const FishInventoryType = preload("res://inventory/fish_inventory.gd") +const FishingSpotType = preload("res://fishing/fishing_spot.gd") + +@onready var _status_label: Label = %StatusLabel +@onready var _bluegill_count_label: Label = %BluegillCountLabel +@onready var _reel_progress: ProgressBar = %ReelProgress + + +func setup(inventory: FishInventoryType, fishing_spot: FishingSpotType) -> void: + inventory.contents_changed.connect(_on_inventory_contents_changed) + fishing_spot.status_changed.connect(_on_fishing_status_changed) + fishing_spot.reel_progress_changed.connect(_on_reel_progress_changed) + _update_bluegill_count(inventory.get_count(&"bluegill")) + + +func _on_inventory_contents_changed(fish_id: StringName, count: int) -> void: + if fish_id == &"bluegill": + _update_bluegill_count(count) + + +func _on_fishing_status_changed(status: String) -> void: + _status_label.text = status + _status_label.visible = not status.is_empty() + + +func _on_reel_progress_changed(progress: float, visible: bool) -> void: + _reel_progress.value = progress * 100.0 + _reel_progress.visible = visible + + +func _update_bluegill_count(count: int) -> void: + _bluegill_count_label.text = "Bluegill: %d" % count diff --git a/ui/game_ui.gd.uid b/ui/game_ui.gd.uid new file mode 100644 index 0000000..b646859 --- /dev/null +++ b/ui/game_ui.gd.uid @@ -0,0 +1 @@ +uid://bhxprtqbieqvc diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn new file mode 100644 index 0000000..da0d97c --- /dev/null +++ b/ui/game_ui.tscn @@ -0,0 +1,57 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"] + +[node name="GameUI" type="CanvasLayer"] +script = ExtResource("1_ui") + +[node name="InventoryPanel" type="PanelContainer" parent="."] +offset_left = 16.0 +offset_top = 16.0 +offset_right = 176.0 +offset_bottom = 62.0 + +[node name="MarginContainer" type="MarginContainer" parent="InventoryPanel"] +theme_override_constants/margin_left = 12 +theme_override_constants/margin_top = 8 +theme_override_constants/margin_right = 12 +theme_override_constants/margin_bottom = 8 + +[node name="BluegillCountLabel" type="Label" parent="InventoryPanel/MarginContainer"] +unique_name_in_owner = true +text = "Bluegill: 0" + +[node name="FishingPanel" type="PanelContainer" parent="."] +anchors_preset = 7 +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = -160.0 +offset_top = -100.0 +offset_right = 160.0 +offset_bottom = -20.0 +grow_horizontal = 2 +grow_vertical = 0 +mouse_filter = 2 + +[node name="MarginContainer" type="MarginContainer" parent="FishingPanel"] +theme_override_constants/margin_left = 12 +theme_override_constants/margin_top = 8 +theme_override_constants/margin_right = 12 +theme_override_constants/margin_bottom = 8 + +[node name="VBoxContainer" type="VBoxContainer" parent="FishingPanel/MarginContainer"] + +[node name="StatusLabel" type="Label" parent="FishingPanel/MarginContainer/VBoxContainer"] +unique_name_in_owner = true +visible = false +text = "Left click to cast" +horizontal_alignment = 1 + +[node name="ReelProgress" type="ProgressBar" parent="FishingPanel/MarginContainer/VBoxContainer"] +unique_name_in_owner = true +visible = false +custom_minimum_size = Vector2(0, 18) +value = 0.0 +show_percentage = false