diff --git a/art/exported/characters/base/netfishing_base_character.glb b/art/exported/characters/base/netfishing_base_character.glb index 10b9d7d..a0ef899 100644 Binary files a/art/exported/characters/base/netfishing_base_character.glb and b/art/exported/characters/base/netfishing_base_character.glb differ diff --git a/art/source/characters/base/netfishing_base_character.blend b/art/source/characters/base/netfishing_base_character.blend index 8727e6f..ed0a7a9 100644 Binary files a/art/source/characters/base/netfishing_base_character.blend and b/art/source/characters/base/netfishing_base_character.blend differ diff --git a/art/source/characters/base/netfishing_base_character.blend1 b/art/source/characters/base/netfishing_base_character.blend1 new file mode 100644 index 0000000..e9ee0bb Binary files /dev/null and b/art/source/characters/base/netfishing_base_character.blend1 differ diff --git a/art/source/characters/base/netfishing_base_character.glb.import b/art/source/characters/base/netfishing_base_character.glb.import new file mode 100644 index 0000000..5a38b76 --- /dev/null +++ b/art/source/characters/base/netfishing_base_character.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://bfhnup14638r7" +path="res://.godot/imported/netfishing_base_character.glb-cbdc33eaa4fcd1d2d2a8f4a463b228d6.scn" + +[deps] + +source_file="res://art/source/characters/base/netfishing_base_character.glb" +dest_files=["res://.godot/imported/netfishing_base_character.glb-cbdc33eaa4fcd1d2d2a8f4a463b228d6.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/fishing/fishing_presentation.gd b/fishing/fishing_presentation.gd index 5ade317..01a10a2 100644 --- a/fishing/fishing_presentation.gd +++ b/fishing/fishing_presentation.gd @@ -23,20 +23,10 @@ enum LineMode { @export_range(0.1, 10.0, 0.1) var pickup_distance_from_player: float = 5.5 @export_category("Fishing Line") -@export_flags_3d_physics var line_obstacle_mask: int = 1 -@export_range(1.0, 30.0, 0.5) var routing_probe_height: float = 8.0 -@export_range(0.1, 2.0, 0.05) var obstruction_sample_spacing: float = 0.4 -@export_range(0.01, 1.0, 0.01) var terrain_clearance: float = 0.1 -@export_range(1, 3, 1) var maximum_obstruction_ranges: int = 1 -@export_range(2, 12, 1) var maximum_contact_anchors_per_range: int = 5 -@export_range(0.0, 3.0, 0.05) var route_lead_in_distance: float = 0.8 -@export_range(0.0, 3.0, 0.05) var route_lead_out_distance: float = 0.8 -@export_range(0.05, 2.0, 0.05) var fallback_arch_height: float = 0.35 @export_range(0.0, 0.5, 0.01) var slack_amount: float = 0.08 @export_range(0.0, 2.0, 0.01) var minimum_slack: float = 0.08 @export_range(0.0, 3.0, 0.01) var maximum_slack: float = 0.65 -@export_range(2, 20, 1) var clear_span_interpolation_count: int = 8 -@export_range(1, 8, 1) var contact_span_interpolation_count: int = 2 +@export_range(2, 20, 1) var sag_interpolation_count: int = 8 @export_range(0.1, 30.0, 0.1) var line_transition_speed: float = 7.0 @export_category("Cast") @@ -59,9 +49,6 @@ enum LineMode { var _mode: VisualMode = VisualMode.NONE var _line_mode: LineMode = LineMode.HIDDEN var _line_mesh: ImmediateMesh = ImmediateMesh.new() -var _route_points: PackedVector3Array = PackedVector3Array() -var _contact_start_anchor_index: int = -1 -var _contact_end_anchor_index: int = -1 var _current_sag: float = 0.0 var _rod: Node3D var _rod_tip: Marker3D @@ -109,9 +96,6 @@ func set_line_mode(mode: LineMode) -> void: _line.visible = mode != LineMode.HIDDEN if mode == LineMode.HIDDEN: _current_sag = 0.0 - _route_points.clear() - _contact_start_anchor_index = -1 - _contact_end_anchor_index = -1 _line_mesh.clear_surfaces() @@ -399,10 +383,10 @@ func _update_line(delta: float) -> void: ) var sag_weight: float = 1.0 - exp(-line_transition_speed * delta) _current_sag = lerpf(_current_sag, target_sag, sag_weight) - _route_points = _build_line_route(rod_tip_position, bobber_position) - var rendered_points: PackedVector3Array = _build_rendered_line_points( - _route_points, - _current_sag + var rendered_points: PackedVector3Array = _build_sagged_line_points( + rod_tip_position, + bobber_position, + _current_sag, ) _line_mesh.clear_surfaces() @@ -415,307 +399,21 @@ func _update_line(delta: float) -> void: _line_mesh.surface_end() -func _build_line_route(start: Vector3, end: Vector3) -> PackedVector3Array: - _contact_start_anchor_index = -1 - _contact_end_anchor_index = -1 - if _raycast_obstacle(start, end).is_empty(): - return PackedVector3Array([start, end]) - - var horizontal_delta := Vector2(end.x - start.x, end.z - start.z) - var horizontal_length: float = horizontal_delta.length() - if horizontal_length <= 0.001: - return _build_fallback_arch(start, end) - - var sample_count: int = clampi( - ceili(horizontal_length / obstruction_sample_spacing), - 2, - 64 - ) - var obstructed_samples := PackedByteArray() - for sample_index: int in range(sample_count + 1): - var progress: float = float(sample_index) / float(sample_count) - var base_point: Vector3 = start.lerp(end, progress) - var surface_hit: Dictionary = _probe_surface(base_point) - var is_obstructed: bool = false - if not surface_hit.is_empty(): - var surface_position: Vector3 = surface_hit["position"] - var required_height: float = surface_position.y + terrain_clearance - if required_height > base_point.y: - is_obstructed = true - obstructed_samples.append(1 if is_obstructed else 0) - - var obstruction_ranges: Array[Vector2i] = _find_obstruction_ranges( - obstructed_samples - ) - if obstruction_ranges.is_empty(): - return _build_fallback_arch(start, end) - if obstruction_ranges.size() > maximum_obstruction_ranges: - return _build_fallback_arch(start, end) - - var first_sample: int = obstruction_ranges[0].x - var last_sample: int = obstruction_ranges[obstruction_ranges.size() - 1].y - var first_progress: float = float(first_sample) / float(sample_count) - var last_progress: float = float(last_sample) / float(sample_count) - var lead_in_progress: float = maxf( - 0.0, - first_progress - route_lead_in_distance / horizontal_length - ) - var lead_out_progress: float = minf( - 1.0, - last_progress + route_lead_out_distance / horizontal_length - ) - - var route := PackedVector3Array([start]) - _append_unique_route_point(route, start.lerp(end, lead_in_progress)) - _contact_start_anchor_index = route.size() - 1 - _append_contact_anchors( - route, - start, - end, - first_sample, - last_sample, - sample_count - ) - _append_unique_route_point(route, start.lerp(end, lead_out_progress)) - _contact_end_anchor_index = route.size() - 1 - _append_unique_route_point(route, end) - route = _remove_near_duplicate_route_points(route) - _update_contact_indices_after_reduction(route) - return _validate_reduced_route(route, start, end) - - -func _build_rendered_line_points( - route: PackedVector3Array, +func _build_sagged_line_points( + start: Vector3, + end: Vector3, sag: float, ) -> PackedVector3Array: var points := PackedVector3Array() - if route.is_empty(): - return points - if ( - _contact_start_anchor_index < 0 - or _contact_end_anchor_index < _contact_start_anchor_index - or _contact_end_anchor_index >= route.size() - ): - _append_clear_span(points, route[0], route[route.size() - 1], sag) - return points - - _append_clear_span( - points, - route[0], - route[_contact_start_anchor_index], - sag - ) - for anchor_index: int in range( - _contact_start_anchor_index, - _contact_end_anchor_index - ): - _append_contact_span( - points, - route[anchor_index], - route[anchor_index + 1] - ) - _append_clear_span( - points, - route[_contact_end_anchor_index], - route[route.size() - 1], - sag - ) - return points - - -func _raycast_obstacle(from: Vector3, to: Vector3) -> Dictionary: - if from.is_equal_approx(to): - return {} - var query := PhysicsRayQueryParameters3D.create( - from, - to, - line_obstacle_mask - ) - query.collide_with_areas = false - query.collide_with_bodies = true - return get_world_3d().direct_space_state.intersect_ray(query) - - -func _find_obstruction_ranges( - obstructed_samples: PackedByteArray, -) -> Array[Vector2i]: - var ranges: Array[Vector2i] = [] - var range_start: int = -1 - for sample_index: int in range(obstructed_samples.size()): - var is_obstructed: bool = obstructed_samples[sample_index] != 0 - if is_obstructed and range_start < 0: - range_start = sample_index - elif not is_obstructed and range_start >= 0: - ranges.append(Vector2i(range_start, sample_index - 1)) - range_start = -1 - if range_start >= 0: - ranges.append(Vector2i(range_start, obstructed_samples.size() - 1)) - return ranges - - -func _append_contact_anchors( - route: PackedVector3Array, - start: Vector3, - end: Vector3, - first_sample: int, - last_sample: int, - sample_count: int, -) -> void: - var available_samples: int = last_sample - first_sample + 1 - var anchor_count: int = mini( - available_samples, - maximum_contact_anchors_per_range - ) - for anchor_offset: int in range(anchor_count): - var weight: float = 0.0 - if anchor_count > 1: - weight = float(anchor_offset) / float(anchor_count - 1) - var sample_index: int = roundi( - lerpf(float(first_sample), float(last_sample), weight) - ) - var progress: float = float(sample_index) / float(sample_count) - var contact: Vector3 = _raise_point_above_obstacle( - start.lerp(end, progress) - ) - _append_unique_route_point(route, contact) - - -func _append_unique_route_point( - route: PackedVector3Array, - point: Vector3, -) -> void: - if route.is_empty() or point.distance_to(route[route.size() - 1]) > 0.03: - route.append(point) - - -func _remove_near_duplicate_route_points( - route: PackedVector3Array, -) -> PackedVector3Array: - var cleaned := PackedVector3Array() - for point: Vector3 in route: - _append_unique_route_point(cleaned, point) - return cleaned - - -func _update_contact_indices_after_reduction(route: PackedVector3Array) -> void: - if route.size() <= 2: - _contact_start_anchor_index = -1 - _contact_end_anchor_index = -1 - return - _contact_start_anchor_index = 1 - _contact_end_anchor_index = route.size() - 2 - - -func _validate_reduced_route( - route: PackedVector3Array, - start: Vector3, - end: Vector3, -) -> PackedVector3Array: - var correction_index: int = -1 - var correction_hit: Dictionary - for segment_index: int in range(route.size() - 1): - var hit: Dictionary = _raycast_obstacle( - route[segment_index], - route[segment_index + 1] - ) - if not hit.is_empty(): - correction_index = segment_index + 1 - correction_hit = hit - break - if correction_index < 0: - return route - - var hit_position: Vector3 = correction_hit["position"] - var correction: Vector3 = _raise_point_above_obstacle(hit_position) - route.insert(correction_index, correction) - _contact_start_anchor_index = mini( - _contact_start_anchor_index, - correction_index - ) - _contact_end_anchor_index = maxi( - _contact_end_anchor_index + 1, - correction_index - ) - for segment_index: int in range(route.size() - 1): - if not _raycast_obstacle( - route[segment_index], - route[segment_index + 1] - ).is_empty(): - return _build_fallback_arch(start, end) - return route - - -func _append_clear_span( - points: PackedVector3Array, - start: Vector3, - end: Vector3, - sag: float, -) -> void: - if points.is_empty(): - points.append(start) - elif not points[points.size() - 1].is_equal_approx(start): - points.append(start) - var subdivisions: int = 1 if sag <= 0.001 else clear_span_interpolation_count + var subdivisions: int = 1 if sag <= 0.001 else sag_interpolation_count + points.append(start) for step: int in range(1, subdivisions + 1): var progress: float = float(step) / float(subdivisions) var point: Vector3 = start.lerp(end, progress) if step < subdivisions and sag > 0.0: point.y -= 4.0 * progress * (1.0 - progress) * sag - point.y = maxf( - point.y, - get_water_surface_height() + terrain_clearance * 0.25 - ) - point = _raise_point_above_obstacle(point) points.append(point) - - -func _append_contact_span( - points: PackedVector3Array, - start: Vector3, - end: Vector3, -) -> void: - if points.is_empty(): - points.append(start) - elif not points[points.size() - 1].is_equal_approx(start): - points.append(start) - for step: int in range(1, contact_span_interpolation_count + 1): - var progress: float = ( - float(step) / float(contact_span_interpolation_count) - ) - var point: Vector3 = start.lerp(end, progress) - if step < contact_span_interpolation_count: - point = _raise_point_above_obstacle(point) - points.append(point) - - -func _build_fallback_arch( - start: Vector3, - end: Vector3, -) -> PackedVector3Array: - var route := PackedVector3Array([start]) - for progress: float in [0.25, 0.5, 0.75]: - var point: Vector3 = start.lerp(end, progress) - point.y += sin(progress * PI) * fallback_arch_height - route.append(_raise_point_above_obstacle(point)) - route.append(end) - _contact_start_anchor_index = 1 - _contact_end_anchor_index = route.size() - 2 - return route - - -func _probe_surface(point: Vector3) -> Dictionary: - var probe_start: Vector3 = point + Vector3.UP * routing_probe_height - var probe_end: Vector3 = point - Vector3.UP * routing_probe_height - return _raycast_obstacle(probe_start, probe_end) - - -func _raise_point_above_obstacle(point: Vector3) -> Vector3: - var hit: Dictionary = _probe_surface(point) - if hit.is_empty(): - return point - var surface_position: Vector3 = hit["position"] - point.y = maxf(point.y, surface_position.y + terrain_clearance) - return point + return points func _begin_rod_release() -> void: diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index 6f2555f..1d93aba 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -909,7 +909,6 @@ func _on_outcome_completed(outcome: StringName) -> void: _pending_catch.weight_lb, true ) - status_changed.emit("left click or escape to put away") func _put_away_catch() -> void: @@ -921,7 +920,6 @@ func _put_away_catch() -> void: or _pending_catch == null ): return - var caught_name: String = _pending_catch.fish.display_name _local_inventory.add_catch(_pending_catch) _local_collection_log.mark_discovered(_pending_catch.fish_id) _pending_catch = null @@ -934,7 +932,7 @@ func _put_away_catch() -> void: _active_player.end_catch_showcase( _finish_showcase_put_away.bind( restore_generation, - "caught %s!" % caught_name + "" ) ) @@ -1018,9 +1016,9 @@ func _return_to_ready() -> void: func _cancel_attempt() -> void: if _network_fishing != null and _network_fishing.has_local_attempt(): - _network_fishing.cancel_local_attempt("Fishing cancelled.") + _network_fishing.cancel_local_attempt("") return - _cleanup_attempt("fishing cancelled.", &"cancel") + _cleanup_attempt("", &"cancel") func _cancel_from_withdrawal() -> void: @@ -1284,7 +1282,7 @@ func _on_network_attempt_ended( ]: return _cleanup_attempt( - message if not message.is_empty() else "Fishing attempt ended.", + message, &"escape" if outcome == &"escape" else &"cancel" ) diff --git a/network/network_fishing_service.gd b/network/network_fishing_service.gd index fb8a4be..38f33bc 100644 --- a/network/network_fishing_service.gd +++ b/network/network_fishing_service.gd @@ -320,7 +320,7 @@ func _update_waiting_attempt( flat_offset.length() - _fishing_spot.withdrawal_cancel_distance ) if withdrawable_distance <= 0.0: - _cancel_attempt(attempt.owner_peer_id, "Fishing cancelled.") + _cancel_attempt(attempt.owner_peer_id, "") return attempt.withdrawal_progress = minf( attempt.withdrawal_progress @@ -463,7 +463,7 @@ func submit_cancel_request(attempt_id: String) -> void: or attempt.attempt_id != attempt_id ): return - _cancel_attempt(sender_id, "Fishing cancelled.") + _cancel_attempt(sender_id, "") func _on_encounter_updated( diff --git a/player/player.gd b/player/player.gd index 22c587c..3367f74 100644 --- a/player/player.gd +++ b/player/player.gd @@ -18,6 +18,10 @@ const PlayerCoolerCapacityType = preload( "res://progression/player_cooler_capacity.gd" ) +const CHARACTER_IDLE_ANIMATION: StringName = &"idle" +const CHARACTER_WALKING_ANIMATION: StringName = &"walking" +const CHARACTER_SITTING_ANIMATION: StringName = &"sitting" + var appearance_snapshot: Dictionary = ( CharacterCustomizationCatalog.default_snapshot() ) @@ -81,6 +85,9 @@ class ShowcaseCameraSnapshot: @export var zoom_smoothing: float = 12.0 @onready var _visuals: Node3D = %Visuals +@onready var _character_animation_player: AnimationPlayer = ( + get_node_or_null("Visuals/CharacterRig/AnimationPlayer") as AnimationPlayer +) @onready var _camera_yaw: Node3D = %CameraYaw @onready var _camera_pitch: Node3D = %CameraPitch @onready var _spring_arm: SpringArm3D = %SpringArm3D @@ -133,6 +140,8 @@ var _network_target_position: Vector3 var _network_target_velocity: Vector3 var _network_target_visual_yaw: float = 0.0 var _network_snapshot_ready: bool = false +var _character_animation_name: StringName = &"" +var _sitting: bool = false func _ready() -> void: @@ -145,6 +154,9 @@ func _physics_process(delta: float) -> void: if _network_interpolation_enabled: _update_network_interpolation(delta) return + if _sitting: + velocity = Vector3.ZERO + return if _water_recovery_active: velocity = Vector3.ZERO return @@ -215,6 +227,7 @@ func _physics_process(delta: float) -> void: func _process(delta: float) -> void: + _update_character_animation() if _remote_recovery_presentation_active: _remote_recovery_elapsed += delta _visuals.position = ( @@ -258,6 +271,48 @@ func _process(delta: float) -> void: ) +func _update_character_animation() -> void: + if _character_animation_player == null: + return + var horizontal_speed_squared: float = ( + velocity.x * velocity.x + velocity.z * velocity.z + ) + var next_animation: StringName = ( + CHARACTER_SITTING_ANIMATION + if _sitting + else ( + CHARACTER_WALKING_ANIMATION + if horizontal_speed_squared > 0.0025 + else CHARACTER_IDLE_ANIMATION + ) + ) + if not _character_animation_player.has_animation(next_animation): + return + if _character_animation_name == next_animation: + return + _character_animation_player.play(next_animation) + _character_animation_name = next_animation + + +func toggle_sitting() -> void: + if ( + _character_animation_player == null + or not _character_animation_player.has_animation( + CHARACTER_SITTING_ANIMATION + ) + ): + return + _sitting = not _sitting + if _sitting: + velocity = Vector3.ZERO + _character_animation_name = &"" + _update_character_animation() + + +func is_sitting() -> bool: + return _sitting + + func _unhandled_input(event: InputEvent) -> void: if not local_control_enabled or not _camera_input_enabled: return diff --git a/player/player.tscn b/player/player.tscn index 8cbc081..a337217 100644 --- a/player/player.tscn +++ b/player/player.tscn @@ -63,21 +63,24 @@ cast_shadow = 0 mesh = SubResource("GroundShadowMesh") [node name="CharacterRig" parent="Visuals" instance=ExtResource("13_character")] +rotation_degrees = Vector3(0, 180, 0) [node name="AnimationPlayer" parent="Visuals/CharacterRig"] autoplay = "idle" -[node name="FishingRod" type="Node3D" parent="Visuals"] -unique_name_in_owner = true -position = Vector3(0.16, 0.38, 0.02) -rotation = Vector3(-0.785398, 0, 0) +[node name="FishingRodAttachment" type="BoneAttachment3D" parent="Visuals/CharacterRig/CharacterRig/Skeleton3D"] +bone_name = "hand.R" -[node name="RodMesh" type="MeshInstance3D" parent="Visuals/FishingRod"] +[node name="FishingRod" type="Node3D" parent="Visuals/CharacterRig/CharacterRig/Skeleton3D/FishingRodAttachment"] +unique_name_in_owner = true +rotation = Vector3(-0.785398, -0.007863, 3.141593) + +[node name="RodMesh" type="MeshInstance3D" parent="Visuals/CharacterRig/CharacterRig/Skeleton3D/FishingRodAttachment/FishingRod"] position = Vector3(0, 0.42, 0) mesh = SubResource("FishingRodMesh") material_override = SubResource("FishingRodMaterial") -[node name="FishingRodTip" type="Marker3D" parent="Visuals/FishingRod"] +[node name="FishingRodTip" type="Marker3D" parent="Visuals/CharacterRig/CharacterRig/Skeleton3D/FishingRodAttachment/FishingRod"] unique_name_in_owner = true position = Vector3(0, 0.95, 0) diff --git a/player/player_visual_presenter.gd b/player/player_visual_presenter.gd index caddfe8..e75f791 100644 --- a/player/player_visual_presenter.gd +++ b/player/player_visual_presenter.gd @@ -7,12 +7,25 @@ const PlayerScene: PackedScene = preload("res://player/player.tscn") static func instantiate_visuals() -> Node3D: var source: Player = PlayerScene.instantiate() var visuals := source.get_node("Visuals") as Node3D + var source_rod_attachment: Node = visuals.find_child( + "FishingRodAttachment", true, false + ) + var source_rod_parent: Node = null + var source_rod_index: int = -1 + if source_rod_attachment != null: + source_rod_parent = source_rod_attachment.get_parent() + source_rod_index = source_rod_attachment.get_index() + source_rod_attachment.owner = null + source_rod_parent.remove_child(source_rod_attachment) var presentation := visuals.duplicate( Node.DUPLICATE_SIGNALS | Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS | Node.DUPLICATE_USE_INSTANTIATION ) as Node3D + if source_rod_attachment != null and source_rod_parent != null: + source_rod_parent.add_child(source_rod_attachment) + source_rod_parent.move_child(source_rod_attachment, source_rod_index) source.free() presentation.name = "PlayerVisuals" return presentation diff --git a/ui/emote_radial_menu.gd b/ui/emote_radial_menu.gd new file mode 100644 index 0000000..4d869e0 --- /dev/null +++ b/ui/emote_radial_menu.gd @@ -0,0 +1,110 @@ +class_name EmoteRadialMenu +extends Control + +signal emote_selected(emote_id: StringName) + +const BubbleButtonScene: PackedScene = preload( + "res://ui/components/bubble_menu/bubble_button.tscn" +) +const BubbleProfile: BubbleMenuProfile = preload( + "res://ui/components/bubble_menu/bubble_menu_profile.tres" +) +const SECTOR_COUNT: int = 8 +const SIT_SECTOR: int = 0 +const RING_RADIUS: float = 172.0 +const BUBBLE_SIZE: Vector2 = Vector2(92.0, 88.0) + +var _buttons: Array[BubbleButton] = [] +var _is_open: bool = false +var _selected_sector: int = SIT_SECTOR + + +func _ready() -> void: + visible = false + mouse_filter = Control.MOUSE_FILTER_IGNORE + for sector: int in SECTOR_COUNT: + var bubble: BubbleButton = BubbleButtonScene.instantiate() as BubbleButton + bubble.profile = BubbleProfile + bubble.focus_mode = Control.FOCUS_NONE + bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE + bubble.text = "Sit" if sector == SIT_SECTOR else "" + add_child(bubble) + _buttons.append(bubble) + _apply_selection_styles() + + +func handle_input(event: InputEvent, can_open: bool) -> bool: + var key_event: InputEventKey = event as InputEventKey + if key_event == null or key_event.echo: + return false + if key_event.physical_keycode != KEY_C: + return false + if key_event.pressed: + if _is_open or not can_open: + return _is_open + open_menu() + return true + if not _is_open: + return false + var selected: int = _selected_sector + close_menu() + if selected == SIT_SECTOR: + emote_selected.emit(&"sit") + return true + + +func open_menu() -> void: + _is_open = true + _selected_sector = SIT_SECTOR + visible = true + _layout_bubbles() + _apply_selection_styles() + + +func close_menu() -> void: + _is_open = false + visible = false + + +func is_open() -> bool: + return _is_open + + +func _process(_delta: float) -> void: + if not _is_open: + return + _layout_bubbles() + _update_mouse_selection() + + +func _layout_bubbles() -> void: + var center: Vector2 = size * 0.5 + for sector: int in SECTOR_COUNT: + var angle: float = -PI * 0.5 + TAU * float(sector) / float(SECTOR_COUNT) + var bubble_center: Vector2 = center + Vector2.from_angle(angle) * RING_RADIUS + var bubble: BubbleButton = _buttons[sector] + bubble.position = bubble_center - BUBBLE_SIZE * 0.5 + bubble.size = BUBBLE_SIZE + bubble.pivot_offset = BUBBLE_SIZE * 0.5 + + +func _update_mouse_selection() -> void: + var offset: Vector2 = get_local_mouse_position() - size * 0.5 + if offset.length() < 24.0: + return + var angle: float = fposmod(offset.angle() + PI * 0.5 + PI / 8.0, TAU) + var sector: int = int(floor(angle / (TAU / float(SECTOR_COUNT)))) + if sector == _selected_sector: + return + _selected_sector = sector + _apply_selection_styles() + + +func _apply_selection_styles() -> void: + for sector: int in _buttons.size(): + var bubble: BubbleButton = _buttons[sector] + bubble.apply_profile() + if sector == _selected_sector: + bubble.add_theme_stylebox_override( + "normal", BubbleProfile.make_hover_style() + ) diff --git a/ui/emote_radial_menu.gd.uid b/ui/emote_radial_menu.gd.uid new file mode 100644 index 0000000..1a1f6a0 --- /dev/null +++ b/ui/emote_radial_menu.gd.uid @@ -0,0 +1 @@ +uid://d3emoteradialmenu diff --git a/ui/emote_radial_menu.tscn b/ui/emote_radial_menu.tscn new file mode 100644 index 0000000..6960a74 --- /dev/null +++ b/ui/emote_radial_menu.tscn @@ -0,0 +1,14 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://ui/emote_radial_menu.gd" id="1_emote"] + +[node name="EmoteRadialMenu" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +z_index = 90 +script = ExtResource("1_emote") diff --git a/ui/game_ui.gd b/ui/game_ui.gd index eef3fd9..2b0fcc5 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -32,6 +32,7 @@ const PlayerCoolerCapacityType = preload( "res://progression/player_cooler_capacity.gd" ) const ChatUIType = preload("res://ui/chat_ui.gd") +const EmoteRadialMenuType = preload("res://ui/emote_radial_menu.gd") const PlayerSettingsManagerType = preload( "res://settings/player_settings_manager.gd" ) @@ -49,7 +50,8 @@ signal shop_backdrop_visibility_changed(is_visible: bool) @onready var _green_catch_progress: ProgressBar = %GreenCatchProgress @onready var _red_chase_progress: ProgressBar = %RedChaseProgress @onready var _barrier_markers: Control = %BarrierMarkers -@onready var _barrier_summary: Label = %BarrierSummary +@onready var _barrier_prompt_panel: PanelContainer = %BarrierPromptPanel +@onready var _barrier_prompt: Label = %BarrierPrompt @onready var _barrier_health: Label = %BarrierHealth @onready var _showcase_details: Label = %ShowcaseDetails @onready var _fishing_panel: PanelContainer = %FishingPanel @@ -62,6 +64,7 @@ signal shop_backdrop_visibility_changed(is_visible: bool) @onready var _shop_prompt: PanelContainer = %ShopPrompt @onready var _effect_status: Label = %EffectStatus @onready var _chat_ui: ChatUIType = %ChatUI +@onready var _emote_radial_menu: EmoteRadialMenuType = %EmoteRadialMenu @onready var _title_settings_panel: SettingsPanelType = ( $UIRoot/TitleScreen/ResponsiveTitleStage/TitlePresentationScaleRoot/SettingsPanel ) @@ -70,6 +73,7 @@ signal shop_backdrop_visibility_changed(is_visible: bool) ) var _showcase_active: bool = false +var _player: PlayerType var _player_menu_open: bool = false var _gameplay_ui_enabled: bool = false var _fishing_spot: FishingSpotType @@ -83,6 +87,7 @@ var _shop_interaction: ShopInteractionType func _ready() -> void: + _emote_radial_menu.emote_selected.connect(_on_emote_selected) _chat_ui.text_entry_ownership_changed.connect( _on_chat_text_entry_ownership_changed ) @@ -132,6 +137,7 @@ func setup( network_player_list: NetworkPlayerListService, settings_manager: PlayerSettingsManagerType, ) -> void: + _player = player _fishing_spot = fishing_spot _item_effects = item_effects _chat_ui.setup( @@ -202,6 +208,26 @@ func setup( _shop_interaction = shop_interaction +func _input(event: InputEvent) -> void: + if _emote_radial_menu == null: + return + var can_open: bool = ( + _gameplay_ui_enabled + and not _system_menu_open + and not _player_menu_open + and not _shop_open + and not _chat_input_open + and not _showcase_active + ) + if _emote_radial_menu.handle_input(event, can_open): + get_viewport().set_input_as_handled() + + +func _on_emote_selected(emote_id: StringName) -> void: + if emote_id == &"sit" and _player != null: + _player.toggle_sitting() + + func setup_data_and_identity( data_root: PlayerDataRoot, identity_backups: IdentityBackupService, @@ -359,6 +385,17 @@ func _on_fishing_status_changed(status: String) -> void: return if _fishing_spot != null and _fishing_spot.is_fighting(): return + var normalized_status: String = status.strip_edges().to_lower() + if normalized_status in [ + "fishing cancelled.", + "fishing cancelled", + "fishing failed.", + "fishing failed", + "fishing attempt ended.", + "fishing attempt ended", + ]: + _set_fishing_status("") + return _set_fishing_status(status) @@ -373,9 +410,6 @@ 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 = ( _gameplay_ui_enabled @@ -388,7 +422,7 @@ func _on_catch_display_changed( chase_progress: float, barrier_positions: PackedFloat32Array, barrier_health: PackedInt32Array, - barrier_max_health: PackedInt32Array, + _barrier_max_health: PackedInt32Array, active_barrier_index: int, visible: bool, ) -> void: @@ -401,12 +435,13 @@ func _on_catch_display_changed( _green_catch_progress.value = progress * 100.0 _red_chase_progress.value = maxf(chase_progress, 0.0) * 100.0 _catch_track.visible = encounter_visible - _barrier_summary.visible = encounter_visible - _barrier_health.visible = encounter_visible + _barrier_prompt_panel.visible = false + _barrier_prompt.visible = false + _barrier_health.visible = false if not encounter_visible: _barrier_health.visible = false + _barrier_prompt.visible = false _clear_barrier_markers() - _barrier_summary.text = "" _barrier_health.text = "" _refresh_fishing_panel_visibility() return @@ -418,45 +453,18 @@ func _on_catch_display_changed( barrier_health, active_barrier_index ) - var barrier_labels: PackedStringArray = [] - for barrier_index: int in range(barrier_positions.size()): - var defeated: bool = ( - barrier_index < barrier_health.size() - and barrier_health[barrier_index] <= 0 - ) - var marker: String = "✓" if defeated else "●" - if barrier_index == active_barrier_index: - marker = "[%s]" % marker - barrier_labels.append( - "%d%% %s" - % [roundi(barrier_positions[barrier_index] * 100.0), marker] - ) - _barrier_summary.text = "barriers: %s" % " ".join(barrier_labels) - if ( active_barrier_index >= 0 and active_barrier_index < barrier_health.size() - and active_barrier_index < barrier_max_health.size() ): - _barrier_health.text = ( - "barrier: %d / %d" - % [ - barrier_health[active_barrier_index], - barrier_max_health[active_barrier_index], - ] - ) + _barrier_prompt_panel.visible = true + _barrier_prompt.visible = true + _barrier_health.visible = true + _barrier_health.text = "%d" % barrier_health[active_barrier_index] else: - _barrier_health.text = "hold left click to reel" - - var chase_gap: float = progress - chase_progress - if chase_gap <= 0.05: - _barrier_health.text = ( - "%s%s" - % [ - _barrier_health.text, - "\nred is closing in!", - ] - ).strip_edges() + _barrier_prompt.visible = false + _barrier_health.visible = false + _barrier_health.text = "" _refresh_fishing_panel_visibility() @@ -508,15 +516,18 @@ func _on_showcase_changed( _set_fishing_status("") return _catch_track.visible = false - _barrier_summary.visible = false + _barrier_prompt_panel.visible = false + _barrier_prompt.visible = false _barrier_health.visible = false _clear_barrier_markers() _showcase_details.text = ( - "%.1f lb • %s\nleft click or escape to put away" + "%.1f lb • %s" % [weight_lb, rarity_name] ) _showcase_details.visible = true - _set_fishing_status("you caught a %s!" % fish_name) + _status_label.text = "You caught a %s!" % fish_name + _status_label.visible = true + _refresh_fishing_panel_visibility() func _on_player_menu_visibility_changed(is_open: bool) -> void: diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index 17e14a7..604f7f4 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=14 format=3] +[gd_scene load_steps=15 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"] @@ -9,27 +9,27 @@ [ext_resource type="PackedScene" path="res://ui/hotbar.tscn" id="7_hotbar"] [ext_resource type="PackedScene" path="res://ui/fishing_shop.tscn" id="8_shop"] [ext_resource type="Script" path="res://ui/chat_ui.gd" id="9_chat"] +[ext_resource type="PackedScene" path="res://ui/emote_radial_menu.tscn" id="10_emote"] [sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"] 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.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.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_fishing_panel"] +bg_color = Color(0.055, 0.105, 0.125, 0.96) +border_width_left = 0 +border_width_top = 0 +border_width_right = 0 +border_width_bottom = 0 +corner_radius_top_left = 12 +corner_radius_top_right = 12 +corner_radius_bottom_right = 12 +corner_radius_bottom_left = 12 [sub_resource type="StyleBoxFlat" id="StyleBox_transparent"] bg_color = Color(0, 0, 0, 0) @@ -64,23 +64,24 @@ anchor_left = 0.5 anchor_top = 1.0 anchor_right = 0.5 anchor_bottom = 1.0 -offset_left = -205.0 -offset_top = -216.0 -offset_right = 205.0 -offset_bottom = -128.0 +offset_left = -260.0 +offset_top = -150.0 +offset_right = 260.0 +offset_bottom = -68.0 grow_horizontal = 2 grow_vertical = 0 mouse_filter = 2 theme = ExtResource("3_theme") +theme_override_styles/panel = SubResource("StyleBox_fishing_panel") [node name="MarginContainer" type="MarginContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel"] -theme_override_constants/margin_left = 14 -theme_override_constants/margin_top = 8 -theme_override_constants/margin_right = 14 -theme_override_constants/margin_bottom = 8 +theme_override_constants/margin_left = 0 +theme_override_constants/margin_top = 0 +theme_override_constants/margin_right = 0 +theme_override_constants/margin_bottom = 0 [node name="VBoxContainer" type="VBoxContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer"] -theme_override_constants/separation = 4 +theme_override_constants/separation = 2 [node name="StatusLabel" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer"] unique_name_in_owner = true @@ -94,12 +95,25 @@ unique_name_in_owner = true visible = false horizontal_alignment = 1 -[node name="CatchTrack" type="Control" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer"] +[node name="CatchTrack" type="Control" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] unique_name_in_owner = true visible = false -custom_minimum_size = Vector2(0, 20) +z_index = 60 +layout_mode = 1 +anchors_preset = 7 +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = -420.0 +offset_top = -160.0 +offset_right = 420.0 +offset_bottom = -124.0 +grow_horizontal = 2 +grow_vertical = 0 +mouse_filter = 2 -[node name="GreenCatchProgress" type="ProgressBar" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer/CatchTrack"] +[node name="GreenCatchProgress" type="ProgressBar" parent="UIRoot/CanonicalStage/GameplayTransientHUD/CatchTrack"] unique_name_in_owner = true layout_mode = 1 anchors_preset = 15 @@ -113,7 +127,7 @@ theme_override_styles/fill = SubResource("StyleBox_catch_fill") value = 0.0 show_percentage = false -[node name="RedChaseProgress" type="ProgressBar" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer/CatchTrack"] +[node name="RedChaseProgress" type="ProgressBar" parent="UIRoot/CanonicalStage/GameplayTransientHUD/CatchTrack"] unique_name_in_owner = true layout_mode = 1 anchors_preset = 15 @@ -127,7 +141,7 @@ theme_override_styles/fill = SubResource("StyleBox_chase_fill") value = 0.0 show_percentage = false -[node name="BarrierMarkers" type="Control" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer/CatchTrack"] +[node name="BarrierMarkers" type="Control" parent="UIRoot/CanonicalStage/GameplayTransientHUD/CatchTrack"] unique_name_in_owner = true layout_mode = 1 anchors_preset = 15 @@ -137,15 +151,51 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 -[node name="BarrierSummary" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer"] +[node name="BarrierPromptPanel" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] unique_name_in_owner = true visible = false -horizontal_alignment = 1 +z_index = 61 +anchors_preset = 7 +anchor_left = 0.5 +anchor_top = 1.0 +anchor_right = 0.5 +anchor_bottom = 1.0 +offset_left = -170.0 +offset_top = -116.0 +offset_right = 170.0 +offset_bottom = -8.0 +grow_horizontal = 2 +grow_vertical = 0 +pivot_offset = Vector2(170, 55) +rotation = 0.025 +mouse_filter = 2 +theme = ExtResource("3_theme") +theme_override_styles/panel = SubResource("StyleBox_fishing_panel") -[node name="BarrierHealth" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer"] +[node name="Margin" type="MarginContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/BarrierPromptPanel"] +theme_override_constants/margin_left = 18 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 18 +theme_override_constants/margin_bottom = 4 + +[node name="Layout" type="VBoxContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/BarrierPromptPanel/Margin"] +theme_override_constants/separation = 0 + +[node name="BarrierPrompt" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/BarrierPromptPanel/Margin/Layout"] unique_name_in_owner = true visible = false +text = "MASH!" horizontal_alignment = 1 +theme_override_colors/font_color = Color(1, 0.84, 0.3, 1) +theme_override_font_sizes/font_size = 63 + +[node name="BarrierHealth" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/BarrierPromptPanel/Margin/Layout"] +unique_name_in_owner = true +visible = false +text = "0" +horizontal_alignment = 1 +theme_override_colors/font_color = Color(0.95, 0.98, 1, 1) +theme_override_font_sizes/font_size = 34 [node name="PlayerMenu" parent="UIRoot/CanonicalStage" instance=ExtResource("2_menu")] unique_name_in_owner = true @@ -153,6 +203,9 @@ unique_name_in_owner = true [node name="Hotbar" parent="UIRoot/CanonicalStage" instance=ExtResource("7_hotbar")] unique_name_in_owner = true +[node name="EmoteRadialMenu" parent="UIRoot/CanonicalStage" instance=ExtResource("10_emote")] +unique_name_in_owner = true + [node name="ShopPrompt" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] unique_name_in_owner = true visible = false