diff --git a/fishing/fishing_presentation.gd b/fishing/fishing_presentation.gd index 699da74..0ec9297 100644 --- a/fishing/fishing_presentation.gd +++ b/fishing/fishing_presentation.gd @@ -71,6 +71,7 @@ var _cast_position: Vector3 var _pickup_position: Vector3 var _active_tween: Tween var _rod_tween: Tween +var _bite_tween: Tween func _ready() -> void: @@ -179,29 +180,30 @@ func show_bite() -> void: if _mode != VisualMode.FISHING: return - _kill_active_tween() - var bite_position: Vector3 = _with_water_height(_bobber.global_position) - _bobber.global_position = bite_position - _active_tween = create_tween() - _active_tween.set_trans(Tween.TRANS_QUAD) - _active_tween.tween_property( + _kill_bite_tween() + _bobber.scale = Vector3.ONE + _bite_tween = create_tween() + _bite_tween.set_trans(Tween.TRANS_QUAD) + _bite_tween.set_ease(Tween.EASE_IN_OUT) + _bite_tween.tween_property( _bobber, - "global_position:y", - get_water_surface_height() - 0.25, - 0.11 + "scale", + Vector3.ONE * 0.68, + 0.08 ) - _active_tween.tween_property( + _bite_tween.tween_property( _bobber, - "global_position:y", - get_water_surface_height() + 0.05, - 0.14 + "scale", + Vector3.ONE * 1.2, + 0.1 ) - _active_tween.tween_property( + _bite_tween.tween_property( _bobber, - "global_position:y", - bite_position.y, - 0.12 + "scale", + Vector3.ONE, + 0.1 ) + _bite_tween.finished.connect(_on_bite_tween_finished) func show_withdrawal_position(position: Vector3) -> void: @@ -225,7 +227,6 @@ func show_reel_position(position: Vector3, _input_held: bool) -> void: return _kill_active_tween() - _bobber.scale = Vector3.ONE _bobber.global_position = _with_water_height(position) @@ -235,6 +236,7 @@ func play_outcome(outcome: StringName) -> void: return _kill_active_tween() + _kill_bite_tween() _kill_rod_tween() _restore_rod_neutral() _mode = VisualMode.OUTCOME @@ -274,15 +276,6 @@ func play_outcome(outcome: StringName) -> void: get_water_surface_height() - 0.4, 0.2 ) - &"miss": - _active_tween.set_parallel(true) - _active_tween.tween_property( - _bobber, - "global_position:y", - get_water_surface_height() - 0.4, - 0.3 - ) - _active_tween.tween_property(_bobber, "scale", Vector3.ONE * 0.65, 0.3) &"invalid": var return_target: Vector3 = _bobber.global_position if _rod_tip != null: @@ -310,6 +303,7 @@ func play_outcome(outcome: StringName) -> void: func cleanup() -> void: _kill_active_tween() + _kill_bite_tween() _kill_rod_tween() _restore_rod_neutral() _mode = VisualMode.NONE @@ -752,6 +746,18 @@ func _kill_active_tween() -> void: _active_tween = null +func _kill_bite_tween() -> void: + if _bite_tween != null and _bite_tween.is_valid(): + _bite_tween.kill() + _bite_tween = null + if is_instance_valid(_bobber): + _bobber.scale = Vector3.ONE + + +func _on_bite_tween_finished() -> void: + _bite_tween = null + + func _kill_rod_tween() -> void: if _rod_tween != null and _rod_tween.is_valid(): _rod_tween.kill() diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index db70441..5a6d3c9 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -37,7 +37,6 @@ enum FishingState { AIMING_CAST, CASTING, WAITING_FOR_BITE, - BITE_ACTIVE, FIGHTING, SHOWING_CATCH, COOLDOWN, @@ -62,7 +61,6 @@ enum FishingState { @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_category("Selection") @@ -80,6 +78,7 @@ enum FishingState { @onready var _presentation: FishingPresentationType = %FishingPresentation var state: FishingState = FishingState.READY +var _external_input_blocked: bool = false var _local_player: PlayerType var _local_inventory: FishInventoryType var _local_collection_log: CollectionLogType @@ -127,12 +126,46 @@ func setup( func can_open_player_menu() -> bool: - return state in [ + return not _external_input_blocked and state in [ FishingState.READY, FishingState.WAITING_FOR_BITE, ] +func begin_water_recovery() -> void: + _external_input_blocked = true + if state in [ + FishingState.AIMING_CAST, + FishingState.CASTING, + FishingState.WAITING_FOR_BITE, + FishingState.FIGHTING, + ]: + _cancel_attempt() + elif state == FishingState.SHOWING_CATCH: + _secure_showcase_catch_for_recovery() + + +func end_water_recovery() -> void: + _external_input_blocked = false + + +func _secure_showcase_catch_for_recovery() -> void: + if ( + _pending_catch != null + and _local_inventory != null + and _local_collection_log != null + ): + _local_inventory.add_catch(_pending_catch) + _local_collection_log.mark_discovered(_pending_catch.fish_id) + _pending_catch = null + _showcase_ready = false + _put_away_press_armed = false + showcase_changed.emit("", "", 0.0, false) + if _active_player != null: + _active_player.end_catch_showcase(Callable(), true) + _cleanup_attempt() + + func _exit_tree() -> void: _showcase_restore_generation += 1 if ( @@ -174,10 +207,6 @@ func _process(delta: float) -> void: _confirm_cast() FishingState.WAITING_FOR_BITE: _update_waiting_for_bite(delta) - FishingState.BITE_ACTIVE: - _state_time_remaining -= delta - if _state_time_remaining <= 0.0: - _miss_bite() FishingState.FIGHTING: if not Input.is_action_pressed("fish_primary"): _catch_controller.set_reel_input(false) @@ -188,6 +217,8 @@ func _process(delta: float) -> void: func _unhandled_input(event: InputEvent) -> void: + if _external_input_blocked: + return if ( event.is_action_pressed("ui_cancel") and state == FishingState.SHOWING_CATCH @@ -202,7 +233,6 @@ func _unhandled_input(event: InputEvent) -> void: FishingState.AIMING_CAST, FishingState.CASTING, FishingState.WAITING_FOR_BITE, - FishingState.BITE_ACTIVE, FishingState.FIGHTING, ] ): @@ -226,8 +256,6 @@ func _unhandled_input(event: InputEvent) -> void: _presentation.set_line_mode( FishingPresentationType.LineMode.TAUT ) - FishingState.BITE_ACTIVE: - _confirm_hook() FishingState.FIGHTING: _catch_controller.handle_primary_pressed() _presentation.set_line_mode( @@ -442,19 +470,8 @@ func _activate_bite() -> void: if state != FishingState.WAITING_FOR_BITE: return - state = FishingState.BITE_ACTIVE - _state_time_remaining = bite_window_duration - _withdrawal_input_held = false - bite_activated.emit() - status_changed.emit("Bite! Left click to hook") - _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT) - _presentation.show_bite() - - -func _confirm_hook() -> void: if ( - state != FishingState.BITE_ACTIVE - or _active_player == null + _active_player == null or _selected_fish == null or _selected_fish.catch_profile == null ): @@ -463,8 +480,10 @@ func _confirm_hook() -> void: state = FishingState.FIGHTING _state_time_remaining = 0.0 + _withdrawal_input_held = false _fight_start_position = _bobber_water_position - status_changed.emit("Hold left click to reel") + bite_activated.emit() + status_changed.emit("Fish on!") _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT) _presentation.begin_reeling() _catch_controller.start_encounter( @@ -472,7 +491,10 @@ func _confirm_hook() -> void: _active_player.reel_speed, _active_player.click_power ) - _catch_controller.set_reel_input(true) + _catch_controller.set_reel_input( + Input.is_action_pressed("fish_primary") + ) + _presentation.show_bite() func _on_catch_encounter_updated( @@ -613,12 +635,6 @@ func _finish_showcase_put_away( _cleanup_attempt(cooldown_message) -func _miss_bite() -> void: - if state != FishingState.BITE_ACTIVE: - return - _cleanup_attempt("The fish got away.", &"miss") - - func _cleanup_attempt( cooldown_message: String = "", visual_outcome: StringName = &"", diff --git a/main/main.gd b/main/main.gd index da8cf15..cef837d 100644 --- a/main/main.gd +++ b/main/main.gd @@ -5,13 +5,19 @@ const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd") const FishPoolType = preload("res://fish/fish_pool.gd") const GameUIType = preload("res://ui/game_ui.gd") const PlayerType = preload("res://player/player.gd") +const TestWorldType = preload("res://world/test_world.gd") +const WaterRecoveryControllerType = preload( + "res://world/water_recovery_controller.gd" +) @export var fish_catalog: FishPoolType @export var pelican_buyer_profile: FishBuyerProfileType +@onready var _test_world: TestWorldType = $TestWorld @onready var _player: PlayerType = %Player @onready var _fishing_spot: FishingSpotType = %FishingSpot @onready var _game_ui: GameUIType = %GameUI +@onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery func _ready() -> void: @@ -34,3 +40,11 @@ func _ready() -> void: fish_catalog, _fishing_spot ) + _water_recovery.setup( + _player, + _fishing_spot, + _game_ui, + _game_ui.get_screen_fade(), + _test_world.get_player_water_trigger(), + _test_world.get_safe_respawn_points() + ) diff --git a/main/main.tscn b/main/main.tscn index 32fcc36..27ea42b 100644 --- a/main/main.tscn +++ b/main/main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=8 format=3] +[gd_scene load_steps=9 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"] @@ -7,6 +7,7 @@ [ext_resource type="PackedScene" path="res://ui/game_ui.tscn" id="5_ui"] [ext_resource type="Resource" path="res://fish/pools/test_water_pool.tres" id="6_pool"] [ext_resource type="Resource" path="res://economy/buyers/pelicans.tres" id="7_pelicans"] +[ext_resource type="Script" path="res://world/water_recovery_controller.gd" id="8_recovery"] [node name="Main" type="Node3D"] script = ExtResource("3_main") @@ -25,5 +26,9 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 5) unique_name_in_owner = true position = Vector3(0, 1.02, -18) +[node name="WaterRecovery" type="Node" parent="."] +unique_name_in_owner = true +script = ExtResource("8_recovery") + [node name="GameUI" parent="." instance=ExtResource("5_ui")] unique_name_in_owner = true diff --git a/player/player.gd b/player/player.gd index bd0c68c..50f6e06 100644 --- a/player/player.gd +++ b/player/player.gd @@ -24,7 +24,10 @@ class ShowcaseCameraSnapshot: @export var sprint_speed: float = 8.0 @export var sneak_speed: float = 2.0 @export var slow_walk_speed: float = 3.25 -@export var jump_velocity: float = 6.0 +@export_range(0.1, 20.0, 0.1) var jump_velocity: float = 4.6 +@export_range(0.1, 5.0, 0.05) var upward_gravity_multiplier: float = 1.35 +@export_range(0.1, 5.0, 0.05) var fall_gravity_multiplier: float = 2.1 +@export_range(0.1, 3.0, 0.05) var body_center_height: float = 0.9 @export var body_rotation_speed: float = 12.0 @export_category("Control") @@ -73,6 +76,7 @@ var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_grav var _camera_dragging: bool = false var _camera_input_enabled: bool = true var _movement_enabled: bool = true +var _water_recovery_active: bool = false var _target_zoom: float = 5.0 var _showcase_rod_visibility: bool = true var _showcase_rod_state_stored: bool = false @@ -92,8 +96,17 @@ func _ready() -> void: func _physics_process(delta: float) -> void: + if _water_recovery_active: + velocity = Vector3.ZERO + return + if not is_on_floor(): - velocity.y -= _gravity * delta + var gravity_multiplier: float = ( + upward_gravity_multiplier + if velocity.y > 0.0 + else fall_gravity_multiplier + ) + velocity.y -= _gravity * gravity_multiplier * delta elif ( local_control_enabled and _movement_enabled @@ -248,6 +261,19 @@ func is_camera_input_enabled() -> bool: return _camera_input_enabled +func set_water_recovery_active(active: bool) -> void: + _water_recovery_active = active + velocity = Vector3.ZERO + + +func get_body_center_position() -> Vector3: + return global_position + Vector3.UP * body_center_height + + +func get_body_center_height() -> float: + return body_center_height + + func get_fishing_rod_tip() -> Marker3D: return _fishing_rod_tip diff --git a/ui/game_theme.tres b/ui/game_theme.tres new file mode 100644 index 0000000..a8a88fa --- /dev/null +++ b/ui/game_theme.tres @@ -0,0 +1,174 @@ +[gd_resource type="Theme" load_steps=12 format=3] + +[sub_resource type="StyleBoxFlat" id="Panel"] +bg_color = Color(0.075, 0.145, 0.17, 0.96) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.2, 0.55, 0.59, 0.65) +corner_radius_top_left = 10 +corner_radius_top_right = 10 +corner_radius_bottom_right = 10 +corner_radius_bottom_left = 10 + +[sub_resource type="StyleBoxFlat" id="ButtonNormal"] +content_margin_left = 12.0 +content_margin_top = 7.0 +content_margin_right = 12.0 +content_margin_bottom = 7.0 +bg_color = Color(0.13, 0.27, 0.31, 1) +corner_radius_top_left = 7 +corner_radius_top_right = 7 +corner_radius_bottom_right = 7 +corner_radius_bottom_left = 7 + +[sub_resource type="StyleBoxFlat" id="ButtonHover"] +content_margin_left = 12.0 +content_margin_top = 7.0 +content_margin_right = 12.0 +content_margin_bottom = 7.0 +bg_color = Color(0.14, 0.48, 0.52, 1) +corner_radius_top_left = 7 +corner_radius_top_right = 7 +corner_radius_bottom_right = 7 +corner_radius_bottom_left = 7 + +[sub_resource type="StyleBoxFlat" id="ButtonPressed"] +content_margin_left = 12.0 +content_margin_top = 7.0 +content_margin_right = 12.0 +content_margin_bottom = 7.0 +bg_color = Color(0.208, 0.725, 0.78, 1) +corner_radius_top_left = 7 +corner_radius_top_right = 7 +corner_radius_bottom_right = 7 +corner_radius_bottom_left = 7 + +[sub_resource type="StyleBoxFlat" id="ButtonDisabled"] +content_margin_left = 12.0 +content_margin_top = 7.0 +content_margin_right = 12.0 +content_margin_bottom = 7.0 +bg_color = Color(0.12, 0.18, 0.2, 0.8) +corner_radius_top_left = 7 +corner_radius_top_right = 7 +corner_radius_bottom_right = 7 +corner_radius_bottom_left = 7 + +[sub_resource type="StyleBoxFlat" id="DangerNormal"] +content_margin_left = 12.0 +content_margin_top = 7.0 +content_margin_right = 12.0 +content_margin_bottom = 7.0 +bg_color = Color(0.55, 0.17, 0.19, 1) +corner_radius_top_left = 7 +corner_radius_top_right = 7 +corner_radius_bottom_right = 7 +corner_radius_bottom_left = 7 + +[sub_resource type="StyleBoxFlat" id="DangerHover"] +content_margin_left = 12.0 +content_margin_top = 7.0 +content_margin_right = 12.0 +content_margin_bottom = 7.0 +bg_color = Color(0.82, 0.26, 0.29, 1) +corner_radius_top_left = 7 +corner_radius_top_right = 7 +corner_radius_bottom_right = 7 +corner_radius_bottom_left = 7 + +[sub_resource type="StyleBoxFlat" id="CardNormal"] +content_margin_left = 7.0 +content_margin_top = 7.0 +content_margin_right = 7.0 +content_margin_bottom = 7.0 +bg_color = Color(0.105, 0.205, 0.235, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.2, 0.36, 0.4, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="CardHover"] +content_margin_left = 7.0 +content_margin_top = 7.0 +content_margin_right = 7.0 +content_margin_bottom = 7.0 +bg_color = Color(0.13, 0.28, 0.32, 1) +border_width_left = 2 +border_width_top = 2 +border_width_right = 2 +border_width_bottom = 2 +border_color = Color(0.208, 0.725, 0.78, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="CardPressed"] +content_margin_left = 7.0 +content_margin_top = 7.0 +content_margin_right = 7.0 +content_margin_bottom = 7.0 +bg_color = Color(0.11, 0.36, 0.4, 1) +border_width_left = 3 +border_width_top = 3 +border_width_right = 3 +border_width_bottom = 3 +border_color = Color(1, 0.82, 0.4, 1) +corner_radius_top_left = 8 +corner_radius_top_right = 8 +corner_radius_bottom_right = 8 +corner_radius_bottom_left = 8 + +[sub_resource type="StyleBoxFlat" id="LineEdit"] +content_margin_left = 8.0 +content_margin_top = 5.0 +content_margin_right = 8.0 +content_margin_bottom = 5.0 +bg_color = Color(0.08, 0.16, 0.19, 1) +border_width_left = 1 +border_width_top = 1 +border_width_right = 1 +border_width_bottom = 1 +border_color = Color(0.2, 0.45, 0.5, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 + +[resource] +default_font_size = 15 +Label/colors/font_color = Color(0.956, 0.972, 0.976, 1) +Label/colors/font_shadow_color = Color(0, 0, 0, 0) +PanelContainer/styles/panel = SubResource("Panel") +Button/colors/font_color = Color(0.956, 0.972, 0.976, 1) +Button/colors/font_hover_color = Color(1, 1, 1, 1) +Button/colors/font_pressed_color = Color(0.04, 0.12, 0.14, 1) +Button/colors/font_disabled_color = Color(0.48, 0.55, 0.58, 1) +Button/styles/normal = SubResource("ButtonNormal") +Button/styles/hover = SubResource("ButtonHover") +Button/styles/pressed = SubResource("ButtonPressed") +Button/styles/focus = SubResource("ButtonHover") +Button/styles/disabled = SubResource("ButtonDisabled") +OptionButton/styles/normal = SubResource("ButtonNormal") +OptionButton/styles/hover = SubResource("ButtonHover") +OptionButton/styles/pressed = SubResource("ButtonPressed") +OptionButton/styles/focus = SubResource("ButtonHover") +DangerButton/base_type = &"Button" +DangerButton/styles/normal = SubResource("DangerNormal") +DangerButton/styles/hover = SubResource("DangerHover") +DangerButton/styles/pressed = SubResource("DangerHover") +DangerButton/styles/focus = SubResource("DangerHover") +CardButton/base_type = &"Button" +CardButton/styles/normal = SubResource("CardNormal") +CardButton/styles/hover = SubResource("CardHover") +CardButton/styles/pressed = SubResource("CardPressed") +CardButton/styles/focus = SubResource("CardHover") +CardButton/colors/font_pressed_color = Color(0.956, 0.972, 0.976, 1) +LineEdit/styles/normal = SubResource("LineEdit") diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 58a8c24..b8e1275 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -21,8 +21,10 @@ const PlayerWalletType = preload("res://economy/player_wallet.gd") @onready var _showcase_details: Label = %ShowcaseDetails @onready var _fishing_panel: PanelContainer = %FishingPanel @onready var _player_menu: PlayerMenuType = %PlayerMenu +@onready var _screen_fade: ScreenFade = %ScreenFade var _showcase_active: bool = false +var _player_menu_open: bool = false func setup( @@ -53,11 +55,36 @@ func setup( ) +func close_player_menu() -> void: + _player_menu.close_menu() + + +func get_screen_fade() -> ScreenFade: + return _screen_fade + + func _on_fishing_status_changed(status: String) -> void: if _showcase_active: return - _status_label.text = status - _status_label.visible = not status.is_empty() + _set_fishing_status(status) + + +func _set_fishing_status(text: String) -> void: + var normalized_text: String = text.strip_edges() + _status_label.text = normalized_text + _status_label.visible = not normalized_text.is_empty() + _refresh_fishing_panel_visibility() + + +func _refresh_fishing_panel_visibility() -> void: + var has_content: bool = ( + not _status_label.text.strip_edges().is_empty() + or not _showcase_details.text.strip_edges().is_empty() + or _catch_track.visible + or _barrier_summary.visible + or _barrier_health.visible + ) + _fishing_panel.visible = has_content and not _player_menu_open func _on_catch_display_changed( @@ -78,6 +105,7 @@ func _on_catch_display_changed( _clear_barrier_markers() _barrier_summary.text = "" _barrier_health.text = "" + _refresh_fishing_panel_visibility() return _update_barrier_markers( @@ -128,6 +156,7 @@ func _on_catch_display_changed( visible and (active_barrier_index >= 0 or chase_gap <= 0.05) ) + _refresh_fishing_panel_visibility() func _update_barrier_markers( @@ -142,9 +171,13 @@ func _update_barrier_markers( barrier_index < health.size() and health[barrier_index] <= 0 ) - marker.color = Color(0.35, 0.38, 0.42, 0.8) if defeated else Color(1.0, 0.82, 0.2, 1.0) + marker.color = ( + UIPalette.DISABLED + if defeated + else UIPalette.SECONDARY + ) if barrier_index == active_index: - marker.color = Color(1.0, 1.0, 1.0, 1.0) + marker.color = UIPalette.TEXT marker.set_anchors_preset(Control.PRESET_TOP_LEFT) marker.anchor_left = positions[barrier_index] marker.anchor_right = positions[barrier_index] @@ -169,23 +202,22 @@ func _on_showcase_changed( ) -> void: _showcase_active = visible if not visible: - _status_label.text = "" - _status_label.visible = false _showcase_details.text = "" _showcase_details.visible = false + _set_fishing_status("") return _catch_track.visible = false _barrier_summary.visible = false _barrier_health.visible = false _clear_barrier_markers() - _status_label.text = "You caught a %s!" % fish_name - _status_label.visible = true _showcase_details.text = ( "%.1f lb • %s\nLeft click or Escape to put away" % [weight_lb, rarity_name] ) _showcase_details.visible = true + _set_fishing_status("You caught a %s!" % fish_name) func _on_player_menu_visibility_changed(is_open: bool) -> void: - _fishing_panel.visible = not is_open + _player_menu_open = is_open + _refresh_fishing_panel_visibility() diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index b0b60a6..94dd4ef 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -1,16 +1,30 @@ -[gd_scene load_steps=7 format=3] +[gd_scene load_steps=9 format=3] [ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"] [ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"] +[ext_resource type="Theme" path="res://ui/game_theme.tres" id="3_theme"] +[ext_resource type="Script" path="res://ui/screen_fade.gd" id="4_fade"] [sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"] -bg_color = Color(0.08, 0.08, 0.1, 0.9) +bg_color = Color(0.055, 0.105, 0.125, 1) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 [sub_resource type="StyleBoxFlat" id="StyleBox_chase_fill"] -bg_color = Color(0.85, 0.12, 0.12, 0.95) +bg_color = Color(0.937, 0.357, 0.384, 0.95) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 [sub_resource type="StyleBoxFlat" id="StyleBox_catch_fill"] -bg_color = Color(0.16, 0.82, 0.28, 0.9) +bg_color = Color(0.275, 0.784, 0.471, 0.92) +corner_radius_top_left = 6 +corner_radius_top_right = 6 +corner_radius_bottom_right = 6 +corner_radius_bottom_left = 6 [sub_resource type="StyleBoxFlat" id="StyleBox_transparent"] bg_color = Color(0, 0, 0, 0) @@ -20,32 +34,36 @@ script = ExtResource("1_ui") [node name="FishingPanel" type="PanelContainer" parent="."] unique_name_in_owner = true +visible = false 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 +offset_left = -210.0 +offset_top = -130.0 +offset_right = 210.0 +offset_bottom = -24.0 grow_horizontal = 2 grow_vertical = 0 mouse_filter = 2 +theme = ExtResource("3_theme") [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 +theme_override_constants/margin_left = 18 +theme_override_constants/margin_top = 12 +theme_override_constants/margin_right = 18 +theme_override_constants/margin_bottom = 12 [node name="VBoxContainer" type="VBoxContainer" parent="FishingPanel/MarginContainer"] +theme_override_constants/separation = 6 [node name="StatusLabel" type="Label" parent="FishingPanel/MarginContainer/VBoxContainer"] unique_name_in_owner = true visible = false text = "" horizontal_alignment = 1 +theme_override_font_sizes/font_size = 17 [node name="ShowcaseDetails" type="Label" parent="FishingPanel/MarginContainer/VBoxContainer"] unique_name_in_owner = true @@ -55,7 +73,7 @@ horizontal_alignment = 1 [node name="CatchTrack" type="Control" parent="FishingPanel/MarginContainer/VBoxContainer"] unique_name_in_owner = true visible = false -custom_minimum_size = Vector2(0, 18) +custom_minimum_size = Vector2(0, 22) [node name="GreenCatchProgress" type="ProgressBar" parent="FishingPanel/MarginContainer/VBoxContainer/CatchTrack"] unique_name_in_owner = true @@ -107,3 +125,16 @@ horizontal_alignment = 1 [node name="PlayerMenu" parent="." instance=ExtResource("2_menu")] unique_name_in_owner = true + +[node name="ScreenFade" type="ColorRect" parent="."] +unique_name_in_owner = true +visible = false +z_index = 100 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 0 +color = Color(0, 0, 0, 1) +script = ExtResource("4_fade") diff --git a/ui/player_menu.gd b/ui/player_menu.gd index baa42b8..7932886 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -328,37 +328,103 @@ func _compare_catches(left: FishCatchType, right: FishCatchType) -> bool: func _create_inventory_card(fish_catch: FishCatchType) -> Button: var card := Button.new() - card.custom_minimum_size = Vector2(170.0, 160.0) + card.theme_type_variation = &"CardButton" + card.custom_minimum_size = Vector2(152.0, 140.0) card.toggle_mode = true card.button_pressed = fish_catch.catch_id == _selected_catch_id - card.tooltip_text = String(fish_catch.catch_id) card.pressed.connect(_select_catch.bind(fish_catch.catch_id)) + _apply_inventory_card_styles( + card, + UIPalette.get_rarity_color(fish_catch.fish.rarity) + ) var content := VBoxContainer.new() content.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT, Control.PRESET_MODE_MINSIZE, 8) + content.add_theme_constant_override("separation", 3) content.mouse_filter = Control.MOUSE_FILTER_IGNORE card.add_child(content) - content.add_child(_create_texture_frame(fish_catch.fish.display_texture, Vector2(138.0, 82.0))) + content.add_child(_create_texture_frame( + fish_catch.fish.display_texture, + Vector2(124.0, 78.0) + )) var name_label := Label.new() name_label.text = fish_catch.fish.display_name name_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER name_label.mouse_filter = Control.MOUSE_FILTER_IGNORE content.add_child(name_label) - var data_label := Label.new() - data_label.text = "%.2f lb • %s\nCatch #%d" % [ - fish_catch.weight_lb, - fish_catch.fish.get_rarity_name(), - fish_catch.catch_sequence, - ] - data_label.text += "\nBase value: $%d" % fish_catch.sale_value + var weight_label := Label.new() + weight_label.text = "%.2f lb" % fish_catch.weight_lb + weight_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + weight_label.add_theme_color_override( + "font_color", + UIPalette.MUTED_TEXT + ) + weight_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + content.add_child(weight_label) if fish_catch.is_favorited: - data_label.text += " • ★" - data_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - data_label.mouse_filter = Control.MOUSE_FILTER_IGNORE - content.add_child(data_label) + var favorite_marker := Label.new() + favorite_marker.text = "★" + favorite_marker.add_theme_color_override( + "font_color", + UIPalette.SECONDARY + ) + favorite_marker.add_theme_font_size_override("font_size", 18) + favorite_marker.set_anchors_preset(Control.PRESET_TOP_RIGHT) + favorite_marker.offset_left = -28.0 + favorite_marker.offset_top = 5.0 + favorite_marker.offset_right = -8.0 + favorite_marker.offset_bottom = 29.0 + favorite_marker.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + favorite_marker.mouse_filter = Control.MOUSE_FILTER_IGNORE + card.add_child(favorite_marker) return card +func _apply_inventory_card_styles( + card: Button, + rarity_color: Color, +) -> void: + card.add_theme_stylebox_override( + "normal", + _create_inventory_card_style(rarity_color, 0) + ) + card.add_theme_stylebox_override( + "hover", + _create_inventory_card_style(rarity_color, 1) + ) + card.add_theme_stylebox_override( + "focus", + _create_inventory_card_style(rarity_color, 1) + ) + card.add_theme_stylebox_override( + "pressed", + _create_inventory_card_style(rarity_color, 2) + ) + + +func _create_inventory_card_style( + rarity_color: Color, + state_index: int, +) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + match state_index: + 1: + style.bg_color = UIPalette.ELEVATED_PANEL.lightened(0.1) + 2: + style.bg_color = UIPalette.PRIMARY.darkened(0.48) + _: + style.bg_color = UIPalette.ELEVATED_PANEL + style.border_color = rarity_color + var border_width: int = 4 if state_index == 2 else 2 + style.set_border_width_all(border_width) + style.set_corner_radius_all(8) + style.content_margin_left = 7.0 + style.content_margin_top = 7.0 + style.content_margin_right = 7.0 + style.content_margin_bottom = 7.0 + return style + + func _select_catch(catch_id: StringName) -> void: if _inventory == null: return @@ -395,13 +461,11 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void: _default_buyer.display_name, buyer_offer, ] - _detail_data.text = "%.2f lb\n%s\nBase value: $%d\n%s\nCatch #%d\n%s" % [ + _detail_data.text = "%.2f lb\n%s\nBase value: $%d\n%s" % [ fish_catch.weight_lb, fish_catch.fish.get_rarity_name(), fish_catch.sale_value, buyer_offer_text, - fish_catch.catch_sequence, - String(fish_catch.catch_id), ] _favorite_button.disabled = false _favorite_button.text = ( @@ -601,16 +665,16 @@ func _create_logbook_card(fish: FishDataType) -> Control: and _collection_log.has_discovered(fish.id) ) var card := PanelContainer.new() - card.custom_minimum_size = Vector2(190.0, 170.0) + card.custom_minimum_size = Vector2(158.0, 154.0) var content := VBoxContainer.new() content.add_theme_constant_override("separation", 4) card.add_child(content) var texture_frame: TextureRect = _create_texture_frame( fish.display_texture, - Vector2(170.0, 92.0) + Vector2(142.0, 82.0) ) if not discovered: - texture_frame.modulate = Color(0.08, 0.1, 0.12, 1.0) + texture_frame.modulate = UIPalette.PANEL content.add_child(texture_frame) var name_label := Label.new() name_label.text = fish.display_name if discovered else "???" diff --git a/ui/player_menu.tscn b/ui/player_menu.tscn index 30dccb6..fb2f660 100644 --- a/ui/player_menu.tscn +++ b/ui/player_menu.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=2 format=3] +[gd_scene load_steps=3 format=3] [ext_resource type="Script" path="res://ui/player_menu.gd" id="1_menu"] +[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] [node name="PlayerMenu" type="Control"] process_mode = 3 @@ -12,6 +13,7 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 mouse_filter = 0 +theme = ExtResource("2_theme") script = ExtResource("1_menu") [node name="Dimmer" type="ColorRect" parent="."] @@ -26,59 +28,60 @@ color = Color(0.015, 0.02, 0.03, 0.72) [node name="MenuPanel" type="PanelContainer" parent="."] layout_mode = 1 -anchors_preset = 8 -anchor_left = 0.5 -anchor_top = 0.5 -anchor_right = 0.5 -anchor_bottom = 0.5 -offset_left = -480.0 -offset_top = -310.0 -offset_right = 480.0 -offset_bottom = 310.0 +anchor_left = 0.055 +anchor_top = 0.045 +anchor_right = 0.945 +anchor_bottom = 0.955 grow_horizontal = 2 grow_vertical = 2 [node name="Margin" type="MarginContainer" parent="MenuPanel"] layout_mode = 2 -theme_override_constants/margin_left = 18 -theme_override_constants/margin_top = 14 -theme_override_constants/margin_right = 18 -theme_override_constants/margin_bottom = 16 +theme_override_constants/margin_left = 22 +theme_override_constants/margin_top = 18 +theme_override_constants/margin_right = 22 +theme_override_constants/margin_bottom = 20 [node name="Layout" type="VBoxContainer" parent="MenuPanel/Margin"] layout_mode = 2 -theme_override_constants/separation = 10 +theme_override_constants/separation = 12 [node name="Header" type="HBoxContainer" parent="MenuPanel/Margin/Layout"] layout_mode = 2 +theme_override_constants/separation = 8 [node name="Title" type="Label" parent="MenuPanel/Margin/Layout/Header"] layout_mode = 2 size_flags_horizontal = 3 -theme_override_font_sizes/font_size = 24 +theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) +theme_override_font_sizes/font_size = 26 text = "Player Menu" [node name="WalletBalance" type="Label" parent="MenuPanel/Margin/Layout/Header"] unique_name_in_owner = true layout_mode = 2 text = "Wallet: $0" +theme_override_colors/font_color = Color(1, 0.82, 0.4, 1) [node name="InventoryTab" type="Button" parent="MenuPanel/Margin/Layout/Header"] unique_name_in_owner = true layout_mode = 2 toggle_mode = true text = "Inventory" +custom_minimum_size = Vector2(104, 36) [node name="LogbookTab" type="Button" parent="MenuPanel/Margin/Layout/Header"] unique_name_in_owner = true layout_mode = 2 toggle_mode = true text = "Logbook" +custom_minimum_size = Vector2(104, 36) [node name="CloseButton" type="Button" parent="MenuPanel/Margin/Layout/Header"] unique_name_in_owner = true layout_mode = 2 text = "Close" +custom_minimum_size = Vector2(76, 36) [node name="Separator" type="HSeparator" parent="MenuPanel/Margin/Layout"] layout_mode = 2 @@ -88,9 +91,11 @@ unique_name_in_owner = true layout_mode = 2 text = "" horizontal_alignment = 1 +theme_override_colors/font_color = Color(1, 0.82, 0.4, 1) +theme_override_font_sizes/font_size = 16 [node name="Content" type="Control" parent="MenuPanel/Margin/Layout"] -custom_minimum_size = Vector2(0, 530) +custom_minimum_size = Vector2(0, 390) layout_mode = 2 size_flags_vertical = 3 @@ -102,9 +107,11 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +theme_override_constants/separation = 10 [node name="SortBar" type="HBoxContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection"] layout_mode = 2 +theme_override_constants/separation = 8 [node name="SortLabel" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] layout_mode = 2 @@ -113,12 +120,12 @@ text = "Sort:" [node name="SortOption" type="OptionButton" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] unique_name_in_owner = true layout_mode = 2 -custom_minimum_size = Vector2(160, 0) +custom_minimum_size = Vector2(145, 36) [node name="SortDirection" type="Button" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] unique_name_in_owner = true layout_mode = 2 -custom_minimum_size = Vector2(130, 0) +custom_minimum_size = Vector2(125, 36) text = "Newest first" [node name="SortSpacer" type="Control" parent="MenuPanel/Margin/Layout/Content/InventorySection/SortBar"] @@ -129,25 +136,28 @@ size_flags_horizontal = 3 unique_name_in_owner = true layout_mode = 2 text = "Held fish base value: $0" +theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1) [node name="InventoryBody" type="HSplitContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection"] layout_mode = 2 size_flags_vertical = 3 -split_offset = 620 +split_offset = 560 +theme_override_constants/separation = 12 [node name="InventoryList" type="PanelContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody"] -custom_minimum_size = Vector2(610, 0) +custom_minimum_size = Vector2(400, 0) layout_mode = 2 [node name="InventoryListMargin" type="MarginContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList"] layout_mode = 2 -theme_override_constants/margin_left = 8 -theme_override_constants/margin_top = 8 -theme_override_constants/margin_right = 8 -theme_override_constants/margin_bottom = 8 +theme_override_constants/margin_left = 12 +theme_override_constants/margin_top = 12 +theme_override_constants/margin_right = 12 +theme_override_constants/margin_bottom = 12 [node name="InventoryStack" type="VBoxContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin"] layout_mode = 2 +theme_override_constants/separation = 10 [node name="InventoryEmpty" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/InventoryList/InventoryListMargin/InventoryStack"] unique_name_in_owner = true @@ -164,20 +174,20 @@ horizontal_scroll_mode = 0 unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 -theme_override_constants/h_separation = 8 -theme_override_constants/v_separation = 8 +theme_override_constants/h_separation = 10 +theme_override_constants/v_separation = 10 columns = 3 [node name="DetailPanel" type="PanelContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody"] -custom_minimum_size = Vector2(270, 0) +custom_minimum_size = Vector2(240, 0) layout_mode = 2 [node name="DetailMargin" type="MarginContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel"] layout_mode = 2 -theme_override_constants/margin_left = 12 -theme_override_constants/margin_top = 12 -theme_override_constants/margin_right = 12 -theme_override_constants/margin_bottom = 12 +theme_override_constants/margin_left = 16 +theme_override_constants/margin_top = 14 +theme_override_constants/margin_right = 16 +theme_override_constants/margin_bottom = 14 [node name="DetailStack" type="VBoxContainer" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin"] layout_mode = 2 @@ -185,7 +195,7 @@ theme_override_constants/separation = 10 [node name="DetailTexture" type="TextureRect" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"] unique_name_in_owner = true -custom_minimum_size = Vector2(240, 180) +custom_minimum_size = Vector2(208, 140) layout_mode = 2 expand_mode = 1 stretch_mode = 5 @@ -194,6 +204,7 @@ texture_filter = 1 [node name="DetailName" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"] unique_name_in_owner = true layout_mode = 2 +theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) theme_override_font_sizes/font_size = 22 horizontal_alignment = 1 @@ -214,6 +225,7 @@ unique_name_in_owner = true layout_mode = 2 disabled = true text = "Sell" +theme_type_variation = &"DangerButton" [node name="SaleUnavailable" type="Label" parent="MenuPanel/Margin/Layout/Content/InventorySection/InventoryBody/DetailPanel/DetailMargin/DetailStack"] unique_name_in_owner = true @@ -221,6 +233,7 @@ visible = false layout_mode = 2 autowrap_mode = 2 horizontal_alignment = 1 +theme_override_colors/font_color = Color(1, 0.702, 0.278, 1) [node name="LogbookSection" type="VBoxContainer" parent="MenuPanel/Margin/Layout/Content"] unique_name_in_owner = true @@ -231,6 +244,7 @@ anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 +theme_override_constants/separation = 10 [node name="LogbookEmpty" type="Label" parent="MenuPanel/Margin/Layout/Content/LogbookSection"] unique_name_in_owner = true @@ -247,8 +261,8 @@ horizontal_scroll_mode = 0 unique_name_in_owner = true layout_mode = 2 size_flags_horizontal = 3 -theme_override_constants/h_separation = 12 -theme_override_constants/v_separation = 12 +theme_override_constants/h_separation = 10 +theme_override_constants/v_separation = 10 columns = 4 [node name="SaleConfirmation" type="PanelContainer" parent="MenuPanel/Margin/Layout/Content"] @@ -260,19 +274,19 @@ anchor_left = 0.5 anchor_top = 0.5 anchor_right = 0.5 anchor_bottom = 0.5 -offset_left = -220.0 -offset_top = -90.0 -offset_right = 220.0 -offset_bottom = 90.0 +offset_left = -230.0 +offset_top = -105.0 +offset_right = 230.0 +offset_bottom = 105.0 grow_horizontal = 2 grow_vertical = 2 [node name="ConfirmationMargin" type="MarginContainer" parent="MenuPanel/Margin/Layout/Content/SaleConfirmation"] layout_mode = 2 -theme_override_constants/margin_left = 18 -theme_override_constants/margin_top = 16 -theme_override_constants/margin_right = 18 -theme_override_constants/margin_bottom = 16 +theme_override_constants/margin_left = 22 +theme_override_constants/margin_top = 20 +theme_override_constants/margin_right = 22 +theme_override_constants/margin_bottom = 20 [node name="ConfirmationLayout" type="VBoxContainer" parent="MenuPanel/Margin/Layout/Content/SaleConfirmation/ConfirmationMargin"] layout_mode = 2 @@ -292,8 +306,11 @@ alignment = 1 unique_name_in_owner = true layout_mode = 2 text = "Confirm" +theme_type_variation = &"DangerButton" +custom_minimum_size = Vector2(120, 38) [node name="CancelSaleButton" type="Button" parent="MenuPanel/Margin/Layout/Content/SaleConfirmation/ConfirmationMargin/ConfirmationLayout/ConfirmationButtons"] unique_name_in_owner = true layout_mode = 2 text = "Cancel" +custom_minimum_size = Vector2(120, 38) diff --git a/ui/screen_fade.gd b/ui/screen_fade.gd new file mode 100644 index 0000000..35a464a --- /dev/null +++ b/ui/screen_fade.gd @@ -0,0 +1,65 @@ +class_name ScreenFade +extends ColorRect + +signal transition_completed(generation: int, faded_to_black: bool) + +@export_range(0.01, 3.0, 0.01) var fade_out_duration: float = 0.4 +@export_range(0.01, 3.0, 0.01) var fade_in_duration: float = 0.45 + +var _fade_tween: Tween + + +func _ready() -> void: + modulate.a = 0.0 + visible = false + + +func fade_to_black(generation: int) -> void: + _start_fade(1.0, fade_out_duration, generation, true) + + +func fade_from_black(generation: int) -> void: + visible = true + modulate.a = 1.0 + _start_fade(0.0, fade_in_duration, generation, false) + + +func reset_immediately() -> void: + if _fade_tween != null and _fade_tween.is_valid(): + _fade_tween.kill() + _fade_tween = null + modulate.a = 0.0 + visible = false + + +func _start_fade( + target_alpha: float, + duration: float, + generation: int, + faded_to_black: bool, +) -> void: + if _fade_tween != null and _fade_tween.is_valid(): + _fade_tween.kill() + visible = true + _fade_tween = create_tween() + _fade_tween.set_trans(Tween.TRANS_QUAD) + _fade_tween.set_ease( + Tween.EASE_IN if faded_to_black else Tween.EASE_OUT + ) + _fade_tween.tween_property( + self, + "modulate:a", + target_alpha, + maxf(duration, 0.01) + ) + _fade_tween.finished.connect( + _on_fade_finished.bind(generation, faded_to_black), + CONNECT_ONE_SHOT + ) + + +func _on_fade_finished(generation: int, faded_to_black: bool) -> void: + _fade_tween = null + if not faded_to_black: + visible = false + transition_completed.emit(generation, faded_to_black) diff --git a/ui/screen_fade.gd.uid b/ui/screen_fade.gd.uid new file mode 100644 index 0000000..ae116ca --- /dev/null +++ b/ui/screen_fade.gd.uid @@ -0,0 +1 @@ +uid://cypipneoh61j3 diff --git a/ui/ui_palette.gd b/ui/ui_palette.gd new file mode 100644 index 0000000..e2e7604 --- /dev/null +++ b/ui/ui_palette.gd @@ -0,0 +1,33 @@ +class_name UIPalette +extends RefCounted + +const PRIMARY := Color("35b9c7") +const SECONDARY := Color("ffd166") +const SUCCESS := Color("46c878") +const WARNING := Color("ffb347") +const DANGER := Color("ef5b62") +const TEXT := Color("f4f7f8") +const MUTED_TEXT := Color("aebbc2") +const DISABLED := Color("66747b") +const PANEL := Color("19313a") +const ELEVATED_PANEL := Color("244550") + +const RARITY_COMMON := Color("e8eef0") +const RARITY_UNCOMMON := Color("58d279") +const RARITY_RARE := Color("5596f6") +const RARITY_EPIC := Color("b176e8") +const RARITY_LEGENDARY := Color("f276b3") + + +static func get_rarity_color(rarity: int) -> Color: + match rarity: + 1: + return RARITY_UNCOMMON + 2: + return RARITY_RARE + 3: + return RARITY_EPIC + 4: + return RARITY_LEGENDARY + _: + return RARITY_COMMON diff --git a/ui/ui_palette.gd.uid b/ui/ui_palette.gd.uid new file mode 100644 index 0000000..b863768 --- /dev/null +++ b/ui/ui_palette.gd.uid @@ -0,0 +1 @@ +uid://d4g6ks171n65j diff --git a/world/player_water_trigger.gd b/world/player_water_trigger.gd new file mode 100644 index 0000000..3f2dc8c --- /dev/null +++ b/world/player_water_trigger.gd @@ -0,0 +1,46 @@ +class_name PlayerWaterTrigger +extends Area3D + +signal recovery_requested(player: Player, surface_height: float) + +@export var surface_height: float = 0.2 +@export_range(0.0, 2.0, 0.05) var entry_depth_threshold: float = 0.35 + +var _tracked_players: Array[Player] = [] +var _triggered_players: Dictionary[StringName, bool] = {} + + +func _ready() -> void: + body_entered.connect(_on_body_entered) + body_exited.connect(_on_body_exited) + + +func _physics_process(_delta: float) -> void: + for player: Player in _tracked_players.duplicate(): + if not is_instance_valid(player): + _tracked_players.erase(player) + continue + var player_key: StringName = StringName(str(player.get_instance_id())) + if _triggered_players.has(player_key): + continue + if ( + player.get_body_center_position().y + <= surface_height - entry_depth_threshold + ): + _triggered_players[player_key] = true + recovery_requested.emit(player, surface_height) + + +func _on_body_entered(body: Node3D) -> void: + var player: Player = body as Player + if player == null or player in _tracked_players: + return + _tracked_players.append(player) + + +func _on_body_exited(body: Node3D) -> void: + var player: Player = body as Player + if player == null: + return + _tracked_players.erase(player) + _triggered_players.erase(StringName(str(player.get_instance_id()))) diff --git a/world/player_water_trigger.gd.uid b/world/player_water_trigger.gd.uid new file mode 100644 index 0000000..42c0c61 --- /dev/null +++ b/world/player_water_trigger.gd.uid @@ -0,0 +1 @@ +uid://dnlbnlimgxoc2 diff --git a/world/safe_respawn_point.gd b/world/safe_respawn_point.gd new file mode 100644 index 0000000..d9d4380 --- /dev/null +++ b/world/safe_respawn_point.gd @@ -0,0 +1,9 @@ +class_name SafeRespawnPoint +extends Marker3D + +@export var enabled: bool = true + + +func get_horizontal_distance_squared(world_position: Vector3) -> float: + var offset: Vector3 = global_position - world_position + return Vector2(offset.x, offset.z).length_squared() diff --git a/world/safe_respawn_point.gd.uid b/world/safe_respawn_point.gd.uid new file mode 100644 index 0000000..68f8c66 --- /dev/null +++ b/world/safe_respawn_point.gd.uid @@ -0,0 +1 @@ +uid://df2lsduguegxh diff --git a/world/test_world.gd b/world/test_world.gd new file mode 100644 index 0000000..066a227 --- /dev/null +++ b/world/test_world.gd @@ -0,0 +1,18 @@ +class_name TestWorld +extends Node3D + +@onready var _water_trigger: PlayerWaterTrigger = %PlayerWaterTrigger +@onready var _safe_respawn_points: Node3D = %SafeRespawnPoints + + +func get_player_water_trigger() -> PlayerWaterTrigger: + return _water_trigger + + +func get_safe_respawn_points() -> Array[SafeRespawnPoint]: + var points: Array[SafeRespawnPoint] = [] + for child: Node in _safe_respawn_points.get_children(): + var point: SafeRespawnPoint = child as SafeRespawnPoint + if point != null: + points.append(point) + return points diff --git a/world/test_world.gd.uid b/world/test_world.gd.uid new file mode 100644 index 0000000..55933d2 --- /dev/null +++ b/world/test_world.gd.uid @@ -0,0 +1 @@ +uid://bclc786v5xw8c diff --git a/world/test_world.tscn b/world/test_world.tscn index a8968a1..da755e5 100644 --- a/world/test_world.tscn +++ b/world/test_world.tscn @@ -1,7 +1,10 @@ -[gd_scene load_steps=14 format=3] +[gd_scene load_steps=18 format=3] [ext_resource type="Script" path="res://world/fishable_water_region.gd" id="1_water_region"] [ext_resource type="Resource" path="res://fish/pools/test_water_pool.tres" id="2_fish_pool"] +[ext_resource type="Script" path="res://world/test_world.gd" id="3_world"] +[ext_resource type="Script" path="res://world/player_water_trigger.gd" id="4_water_trigger"] +[ext_resource type="Script" path="res://world/safe_respawn_point.gd" id="5_safe_point"] [sub_resource type="BoxShape3D" id="GroundShape"] size = Vector3(30, 1, 30) @@ -30,6 +33,12 @@ size = Vector3(40, 0.16, 19.5) [sub_resource type="BoxShape3D" id="FishableShoreShape"] size = Vector3(17.5, 0.16, 5.5) +[sub_resource type="BoxShape3D" id="PlayerWaterShape"] +size = Vector3(40, 5, 19.5) + +[sub_resource type="BoxShape3D" id="PlayerWaterShoreShape"] +size = Vector3(17.5, 5, 5.5) + [sub_resource type="StandardMaterial3D" id="WaterMaterial"] transparency = 1 albedo_color = Color(0.1, 0.45, 0.7, 0.72) @@ -43,6 +52,7 @@ ambient_light_color = Color(0.65, 0.72, 0.8, 1) ambient_light_energy = 0.65 [node name="TestWorld" type="Node3D"] +script = ExtResource("3_world") [node name="WorldEnvironment" type="WorldEnvironment" parent="."] environment = SubResource("Environment") @@ -94,3 +104,41 @@ shape = SubResource("FishableShoreShape") [node name="ShoreRight" type="CollisionShape3D" parent="FishableWater"] position = Vector3(11.25, 0, 12.5) shape = SubResource("FishableShoreShape") + +[node name="PlayerWaterTrigger" type="Area3D" parent="."] +unique_name_in_owner = true +position = Vector3(0, -2.3, -29.75) +collision_layer = 8 +collision_mask = 2 +script = ExtResource("4_water_trigger") +surface_height = 0.2 + +[node name="DeepWater" type="CollisionShape3D" parent="PlayerWaterTrigger"] +shape = SubResource("PlayerWaterShape") + +[node name="ShoreLeft" type="CollisionShape3D" parent="PlayerWaterTrigger"] +position = Vector3(-11.25, 0, 12.5) +shape = SubResource("PlayerWaterShoreShape") + +[node name="ShoreRight" type="CollisionShape3D" parent="PlayerWaterTrigger"] +position = Vector3(11.25, 0, 12.5) +shape = SubResource("PlayerWaterShoreShape") + +[node name="SafeRespawnPoints" type="Node3D" parent="."] +unique_name_in_owner = true + +[node name="DockApproach" type="Marker3D" parent="SafeRespawnPoints"] +position = Vector3(0, 0.58, -9) +script = ExtResource("5_safe_point") + +[node name="WestShore" type="Marker3D" parent="SafeRespawnPoints"] +position = Vector3(-11.5, 0.58, -11.5) +script = ExtResource("5_safe_point") + +[node name="EastShore" type="Marker3D" parent="SafeRespawnPoints"] +position = Vector3(11.5, 0.58, -11.5) +script = ExtResource("5_safe_point") + +[node name="DockEnd" type="Marker3D" parent="SafeRespawnPoints"] +position = Vector3(0, 1.08, -17) +script = ExtResource("5_safe_point") diff --git a/world/water_recovery_controller.gd b/world/water_recovery_controller.gd new file mode 100644 index 0000000..fe0d37a --- /dev/null +++ b/world/water_recovery_controller.gd @@ -0,0 +1,149 @@ +class_name WaterRecoveryController +extends Node + +enum RecoveryState { + IDLE, + BOBBING, + FADING_OUT, + FADING_IN, +} + +@export_range(-1.0, 1.0, 0.01) var water_center_offset: float = 0.0 +@export_range(0.0, 0.5, 0.005) var bob_amplitude: float = 0.08 +@export_range(0.1, 10.0, 0.1) var bob_speed: float = 2.4 +@export_range(0.0, 5.0, 0.05) var pre_fade_bob_duration: float = 1.0 +@export_range(0.0, 1.0, 0.01) var respawn_height_offset: float = 0.08 + +var state: RecoveryState = RecoveryState.IDLE +var _player: Player +var _fishing_spot: FishingSpot +var _game_ui: GameUI +var _screen_fade: ScreenFade +var _water_trigger: PlayerWaterTrigger +var _safe_points: Array[SafeRespawnPoint] = [] +var _initial_spawn_transform: Transform3D +var _entry_position: Vector3 +var _bob_base_position: Vector3 +var _bob_elapsed: float = 0.0 +var _prior_movement_enabled: bool = true +var _prior_camera_input_enabled: bool = true +var _generation: int = 0 + + +func setup( + player: Player, + fishing_spot: FishingSpot, + game_ui: GameUI, + screen_fade: ScreenFade, + water_trigger: PlayerWaterTrigger, + safe_points: Array[SafeRespawnPoint], +) -> void: + _player = player + _fishing_spot = fishing_spot + _game_ui = game_ui + _screen_fade = screen_fade + _water_trigger = water_trigger + _safe_points = safe_points + _initial_spawn_transform = player.global_transform + if not _water_trigger.recovery_requested.is_connected( + _on_recovery_requested + ): + _water_trigger.recovery_requested.connect(_on_recovery_requested) + if not _screen_fade.transition_completed.is_connected( + _on_fade_transition_completed + ): + _screen_fade.transition_completed.connect( + _on_fade_transition_completed + ) + + +func _process(delta: float) -> void: + if state != RecoveryState.BOBBING or _player == null: + return + _bob_elapsed += delta + var bob_offset: float = sin(_bob_elapsed * bob_speed * TAU) * bob_amplitude + _player.global_position = _bob_base_position + Vector3.UP * bob_offset + if _bob_elapsed >= pre_fade_bob_duration: + state = RecoveryState.FADING_OUT + _screen_fade.fade_to_black(_generation) + + +func _exit_tree() -> void: + _generation += 1 + if _screen_fade != null and is_instance_valid(_screen_fade): + _screen_fade.reset_immediately() + if _player != null and is_instance_valid(_player): + _player.set_water_recovery_active(false) + _player.set_movement_enabled(_prior_movement_enabled) + _player.set_camera_input_enabled(_prior_camera_input_enabled) + state = RecoveryState.IDLE + + +func _on_recovery_requested( + triggered_player: Player, + surface_height: float, +) -> void: + if ( + state != RecoveryState.IDLE + or triggered_player != _player + or not is_instance_valid(triggered_player) + ): + return + _generation += 1 + _entry_position = _player.global_position + _game_ui.close_player_menu() + _fishing_spot.begin_water_recovery() + _prior_movement_enabled = _player.is_movement_enabled() + _prior_camera_input_enabled = _player.is_camera_input_enabled() + _player.set_movement_enabled(false) + _player.set_camera_input_enabled(false) + _player.set_water_recovery_active(true) + _bob_base_position = _player.global_position + _bob_base_position.y = ( + surface_height + + water_center_offset + - _player.get_body_center_height() + ) + _player.global_position = _bob_base_position + _bob_elapsed = 0.0 + state = RecoveryState.BOBBING + + +func _on_fade_transition_completed( + generation: int, + faded_to_black: bool, +) -> void: + if generation != _generation: + return + if faded_to_black and state == RecoveryState.FADING_OUT: + _respawn_player() + state = RecoveryState.FADING_IN + _screen_fade.fade_from_black(_generation) + elif not faded_to_black and state == RecoveryState.FADING_IN: + _finish_recovery() + + +func _respawn_player() -> void: + var target_transform: Transform3D = _initial_spawn_transform + var nearest_distance: float = INF + for point: SafeRespawnPoint in _safe_points: + if point == null or not is_instance_valid(point) or not point.enabled: + continue + var distance: float = point.get_horizontal_distance_squared( + _entry_position + ) + if distance < nearest_distance: + nearest_distance = distance + target_transform = point.global_transform + target_transform.origin.y += respawn_height_offset + _player.global_transform = target_transform + _player.velocity = Vector3.ZERO + + +func _finish_recovery() -> void: + _player.set_water_recovery_active(false) + _player.set_movement_enabled(_prior_movement_enabled) + _player.set_camera_input_enabled(_prior_camera_input_enabled) + _fishing_spot.end_water_recovery() + state = RecoveryState.IDLE + _bob_elapsed = 0.0 diff --git a/world/water_recovery_controller.gd.uid b/world/water_recovery_controller.gd.uid new file mode 100644 index 0000000..496e027 --- /dev/null +++ b/world/water_recovery_controller.gd.uid @@ -0,0 +1 @@ +uid://dvv0ublaoj4gr