diff --git a/art/exported/characters/base/netfishing_base_character.glb b/art/exported/characters/base/netfishing_base_character.glb index b7bc2f7..58eb7cd 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/exported/items/equipment/net.glb b/art/exported/items/equipment/net.glb new file mode 100644 index 0000000..db4fa34 Binary files /dev/null and b/art/exported/items/equipment/net.glb differ diff --git a/art/exported/items/equipment/net.glb.import b/art/exported/items/equipment/net.glb.import new file mode 100644 index 0000000..bd957ad --- /dev/null +++ b/art/exported/items/equipment/net.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://cvwxg6kh8ulqv" +path="res://.godot/imported/net.glb-cbbc53905aec6a50c4a3322ca7a93b1e.scn" + +[deps] + +source_file="res://art/exported/items/equipment/net.glb" +dest_files=["res://.godot/imported/net.glb-cbbc53905aec6a50c4a3322ca7a93b1e.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/art/exported/items/equipment/net_Material.png b/art/exported/items/equipment/net_Material.png new file mode 100644 index 0000000..bd5eff2 Binary files /dev/null and b/art/exported/items/equipment/net_Material.png differ diff --git a/art/exported/items/equipment/net_Material.png.import b/art/exported/items/equipment/net_Material.png.import new file mode 100644 index 0000000..0723973 --- /dev/null +++ b/art/exported/items/equipment/net_Material.png.import @@ -0,0 +1,45 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://binoqhg45vgbs" +path.s3tc="res://.godot/imported/net_Material.png-f4c4b0400561bc2751fe719b6b1a1e1a.s3tc.ctex" +path.etc2="res://.godot/imported/net_Material.png-f4c4b0400561bc2751fe719b6b1a1e1a.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} +generator_parameters={ +"md5": "ef2c2ae657c9460c90eb808ba5217f39" +} + +[deps] + +source_file="res://art/exported/items/equipment/net_Material.png" +dest_files=["res://.godot/imported/net_Material.png-f4c4b0400561bc2751fe719b6b1a1e1a.s3tc.ctex", "res://.godot/imported/net_Material.png-f4c4b0400561bc2751fe719b6b1a1e1a.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/art/exported/items/equipment/net_Plane Base Color.png b/art/exported/items/equipment/net_Plane Base Color.png new file mode 100644 index 0000000..795d0a3 Binary files /dev/null and b/art/exported/items/equipment/net_Plane Base Color.png differ diff --git a/art/exported/items/equipment/net_Plane Base Color.png.import b/art/exported/items/equipment/net_Plane Base Color.png.import new file mode 100644 index 0000000..61077be --- /dev/null +++ b/art/exported/items/equipment/net_Plane Base Color.png.import @@ -0,0 +1,45 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cdsabucjy5rpk" +path.s3tc="res://.godot/imported/net_Plane Base Color.png-3875ae3dd186ddf319c55aabb13e663f.s3tc.ctex" +path.etc2="res://.godot/imported/net_Plane Base Color.png-3875ae3dd186ddf319c55aabb13e663f.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} +generator_parameters={ +"md5": "a6001c381d4c8e16bd2d64ea74ccff82" +} + +[deps] + +source_file="res://art/exported/items/equipment/net_Plane Base Color.png" +dest_files=["res://.godot/imported/net_Plane Base Color.png-3875ae3dd186ddf319c55aabb13e663f.s3tc.ctex", "res://.godot/imported/net_Plane Base Color.png-3875ae3dd186ddf319c55aabb13e663f.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/art/source/characters/base/netfishing_character_source.blend b/art/source/characters/base/netfishing_character_source.blend index e75f254..edfb8e6 100644 Binary files a/art/source/characters/base/netfishing_character_source.blend and b/art/source/characters/base/netfishing_character_source.blend differ diff --git a/art/source/items/equipment/.gdignore b/art/source/items/equipment/.gdignore new file mode 100644 index 0000000..e69de29 diff --git a/art/source/items/equipment/net.blend b/art/source/items/equipment/net.blend new file mode 100644 index 0000000..0a7ba4c Binary files /dev/null and b/art/source/items/equipment/net.blend differ diff --git a/docs/ATTRIBUTION.md b/docs/ATTRIBUTION.md index 5255e74..5f9e387 100644 --- a/docs/ATTRIBUTION.md +++ b/docs/ATTRIBUTION.md @@ -21,6 +21,8 @@ independent team Woofmeow. arms, ears and tails, and multiple world props and decorative assets, including trees and bridges. The contributor requested to be credited as `chillnfill`. +- adamantris: contributor of the original hand-net model and texture artwork; + the contributor is credited as `adamantris`. - Tekgator: creator of the cooler-capacity, reel-speed, and barrier-power shop icons. - kat: supplied the original recorded voice sample used for the animalese @@ -137,6 +139,7 @@ locations only and must never appear in scenes or resources. | Fur customization channel maps | `art/exported/characters/patterns/` | Original 2D channel-map artwork by co-owner Endeavour. Exact file hashes are recorded below. | | Character base meshes and appendages | `art/source/characters/base/`, `art/exported/characters/base/`, and derived character resources | Original body and arm models and original ear and tail models by chillnfill. Any embedded original 2D artwork is by Endeavour. | | World and decorative prop models | `art/source/environment/`, `art/exported/environment/`, and derived world resources | Original prop and decorative models by chillnfill, including trees and bridges. Any embedded original 2D artwork is by Endeavour. | +| Hand-net model | `art/source/items/equipment/net.blend`, `art/exported/items/equipment/net.glb`, and adjacent exported textures | Original model and texture artwork by adamantris; repository adaptation and rigging by Voyager. | | Tuffy font | `ui/fonts/Tuffy_Bold.otf` | Public-domain dedication in `ui/fonts/Tuffy-LICENSE.txt`. | | Seattle Avenue font | `ui/fonts/seattle_avenue.otf` | Public-domain font by JLH Fonts; source record and hash in `ui/fonts/Seattle-Avenue-LICENSE.txt`. | | Title music | `audio/music/title/as_in_four_wolves.ogg` | Original music composed and owned by co-owner Voyager. | @@ -172,6 +175,21 @@ https://creativecommons.org/licenses/by/3.0/legalcode. later written confirmation covering Project use, modification, distribution, commercial release, and applicable sublicensing. +### adamantris hand-net contribution record + +- Requested credit: `adamantris`. +- Original contribution: hand-net model and texture artwork. +- Repository source: `art/source/items/equipment/net.blend`, SHA-256 + `4608993b2397681029c60eb6a7657e3db311ded99ebb5a461cdf4738c8858b39`. +- Repository export: `art/exported/items/equipment/net.glb`, SHA-256 + `d4c2d4903da3817ff9bb82e0404f5e51b74d299abc5bef28c7f6daf6371474fb`. +- Repository adaptation: reshaping and the generic `net_root`, `net_rim`, + `net_mid`, and `net_tip` deformation rig were prepared by Voyager for + reuse across gathering activities. +- Permission record: retain the original submission correspondence or a + written confirmation accepting `CONTRIBUTOR-TERMS.md` with the project + records. + ### Fur customization channel-map record Original channel-map artwork by Endeavour. Repository paths preserve the diff --git a/gathering/catalog/crab_blue.tres b/gathering/catalog/crab_blue.tres index 9e02a04..b2f1234 100644 --- a/gathering/catalog/crab_blue.tres +++ b/gathering/catalog/crab_blue.tres @@ -11,3 +11,4 @@ required_tool_id = &"crab_net" surface_materials = Array[StringName]([&"sand"]) minimum_surface_y = -0.8 population = 4 +sprite_pixel_size = 0.00025 diff --git a/gathering/catalog/crab_brown.tres b/gathering/catalog/crab_brown.tres index 13ac341..0c140a2 100644 --- a/gathering/catalog/crab_brown.tres +++ b/gathering/catalog/crab_brown.tres @@ -17,7 +17,7 @@ scare_radius = 2.8 capture_radius = 0.7 interaction_range = 2.6 charge_duration = 2.0 -sprite_pixel_size = 0.001 +sprite_pixel_size = 0.00025 sprite_tilt_degrees = -45.0 capture_respawn_min_seconds = 480.0 capture_respawn_max_seconds = 720.0 diff --git a/gathering/catalog/crab_dungeness.tres b/gathering/catalog/crab_dungeness.tres index e4af78f..699210f 100644 --- a/gathering/catalog/crab_dungeness.tres +++ b/gathering/catalog/crab_dungeness.tres @@ -11,3 +11,4 @@ required_tool_id = &"crab_net" surface_materials = Array[StringName]([&"sand"]) minimum_surface_y = -0.5 population = 4 +sprite_pixel_size = 0.00025 diff --git a/gathering/catalog/crab_ghost.tres b/gathering/catalog/crab_ghost.tres index 557419e..bfc164d 100644 --- a/gathering/catalog/crab_ghost.tres +++ b/gathering/catalog/crab_ghost.tres @@ -11,3 +11,4 @@ required_tool_id = &"crab_net" surface_materials = Array[StringName]([&"sand"]) minimum_surface_y = 0.08 population = 4 +sprite_pixel_size = 0.00025 diff --git a/gathering/gatherable_data.gd b/gathering/gatherable_data.gd index c5164d0..31b48c4 100644 --- a/gathering/gatherable_data.gd +++ b/gathering/gatherable_data.gd @@ -30,7 +30,7 @@ const FishQualityType = preload("res://fish/fish_quality.gd") @export_range(0.1, 5.0, 0.05) var capture_radius: float = 0.7 @export_range(0.1, 10.0, 0.05) var interaction_range: float = 2.6 @export_range(0.1, 10.0, 0.05) var charge_duration: float = 2.0 -@export_range(0.1, 5.0, 0.01) var sprite_pixel_size: float = 0.001 +@export_range(0.0001, 0.01, 0.00005) var sprite_pixel_size: float = 0.001 @export_range(-90.0, 90.0, 1.0) var sprite_tilt_degrees: float = -45.0 @export_category("Respawn Budget") @export_range(0.0, 3600.0, 1.0) var capture_respawn_min_seconds: float = 480.0 diff --git a/gathering/gathering_controller.gd b/gathering/gathering_controller.gd index 1a7649e..9968412 100644 --- a/gathering/gathering_controller.gd +++ b/gathering/gathering_controller.gd @@ -10,7 +10,9 @@ const NetworkWorldSpawnServiceType = preload( signal status_changed(message: String) @export_range(0.5, 5.0, 0.05) var marker_distance: float = 1.7 -@export_range(0.1, 2.0, 0.05) var marker_radius: float = 0.7 +@export_range(0.025, 2.0, 0.025) var marker_radius: float = 0.175 +@export_range(0.0, 1.0, 0.01) var marker_surface_offset: float = 0.045 +@export_range(0.0, 2.0, 0.05) var forward_probe_extra_distance: float = 0.6 @export_flags_3d_physics var terrain_collision_mask: int = 1 @export_range(0.5, 20.0, 0.5) var ray_height: float = 5.0 @export_range(0.5, 20.0, 0.5) var ray_depth: float = 8.0 @@ -112,6 +114,13 @@ func _unhandled_input(event: InputEvent) -> void: if not event.is_action("fish_primary"): return if event.is_pressed(): + if ( + _gameplay_input_enabled + and _player != null + and _player.release_net_strike_hold() + ): + get_viewport().set_input_as_handled() + return if ( _charging or not _gameplay_input_enabled @@ -133,17 +142,20 @@ func _unhandled_input(event: InputEvent) -> void: ) _update_marker_target() _update_target_entity() + _player.begin_net_draw_visual() get_viewport().set_input_as_handled() return if not _charging: return var fully_charged: bool = _charge_elapsed >= _charge_duration - if ( + var valid_capture: bool = ( fully_charged and _marker_has_surface and not _target_entity_id.is_empty() and _player.is_sneaking() - ): + ) + _player.play_net_strike_visual() + if valid_capture: _service.finish_local_interaction( _request_id, _target_entity_id, @@ -151,6 +163,7 @@ func _unhandled_input(event: InputEvent) -> void: ) else: _service.cancel_local_interaction(_request_id) + _player.resolve_net_strike_visual(false) _reset_charge_state() get_viewport().set_input_as_handled() @@ -167,23 +180,82 @@ func _update_marker_target() -> void: if facing.is_zero_approx(): return facing = facing.normalized() + var probe_origin: Vector3 = _player.get_body_center_position() + var probe_direction: Vector3 = _get_surface_probe_direction(facing) + var forward_hit: Dictionary = _cast_marker_ray( + probe_origin, + probe_origin + + probe_direction + * (marker_distance + forward_probe_extra_distance), + ) + if _apply_marker_surface(forward_hit, facing): + return var query_center: Vector3 = _player.global_position + facing * marker_distance - var query := PhysicsRayQueryParameters3D.create( + var ground_hit: Dictionary = _cast_marker_ray( query_center + Vector3.UP * ray_height, query_center - Vector3.UP * ray_depth, + ) + _apply_marker_surface(ground_hit, facing) + + +func _get_surface_probe_direction(facing: Vector3) -> Vector3: + var vertical_aim: float = 0.0 + var camera := _player.get_active_gameplay_camera() + if camera != null: + vertical_aim = clampf((-camera.global_basis.z).y, -0.75, 0.75) + return Vector3(facing.x, vertical_aim, facing.z).normalized() + + +func _cast_marker_ray(from: Vector3, to: Vector3) -> Dictionary: + var query := PhysicsRayQueryParameters3D.create( + from, + to, terrain_collision_mask, + [_player.get_rid()], ) query.collide_with_areas = false query.collide_with_bodies = true - var hit: Dictionary = get_world_3d().direct_space_state.intersect_ray(query) + return get_world_3d().direct_space_state.intersect_ray(query) + + +func _apply_marker_surface(hit: Dictionary, facing: Vector3) -> bool: if hit.is_empty(): - return + return false var position: Variant = hit.get("position") - if typeof(position) != TYPE_VECTOR3: - return - _marker_position = (position as Vector3) + Vector3.UP * 0.045 + var normal_value: Variant = hit.get("normal") + if ( + typeof(position) != TYPE_VECTOR3 + or typeof(normal_value) != TYPE_VECTOR3 + ): + return false + var surface_normal := normal_value as Vector3 + if not surface_normal.is_finite() or surface_normal.is_zero_approx(): + return false + surface_normal = surface_normal.normalized() + _marker_position = ( + (position as Vector3) + surface_normal * marker_surface_offset + ) _marker.global_position = _marker_position + _marker.global_basis = marker_basis_for_surface(surface_normal, facing) _marker_has_surface = true + return true + + +static func marker_basis_for_surface( + surface_normal: Vector3, + facing: Vector3, +) -> Basis: + var normal := surface_normal.normalized() + var reference := Vector3.UP + if absf(normal.dot(Vector3.UP)) > 0.95: + reference = facing.normalized() + if reference.is_zero_approx() or absf(normal.dot(reference)) > 0.95: + reference = Vector3.FORWARD + var x_axis := reference.cross(normal).normalized() + if x_axis.is_zero_approx(): + x_axis = Vector3.RIGHT + var z_axis := x_axis.cross(normal).normalized() + return Basis(x_axis, normal, z_axis).orthonormalized() func _update_target_entity() -> void: @@ -200,6 +272,8 @@ func _update_target_entity() -> void: func _cancel_charge() -> void: if _charging and not _request_id.is_empty() and _service != null: _service.cancel_local_interaction(_request_id) + if _charging and _player != null: + _player.cancel_net_action_visual() _reset_charge_state() @@ -213,6 +287,8 @@ func _reset_charge_state() -> void: func _on_interaction_finished(accepted: bool, message: String) -> void: + if _player != null: + _player.resolve_net_strike_visual(accepted) _reset_charge_state() if not accepted or not message.is_empty(): status_changed.emit(message) diff --git a/gathering/world_gatherable.gd b/gathering/world_gatherable.gd index 1dccc7a..6db2e9d 100644 --- a/gathering/world_gatherable.gd +++ b/gathering/world_gatherable.gd @@ -2,6 +2,8 @@ class_name WorldGatherable extends Node3D const GatherableDataType = preload("res://gathering/gatherable_data.gd") +const REFERENCE_SPRITE_PIXEL_SIZE: float = 0.001 +const REFERENCE_SPRITE_HEIGHT: float = 0.22 var entity_id: String = "" var type_id: StringName @@ -26,6 +28,11 @@ func configure( if data != null and data.catch_data != null: _sprite.texture = data.catch_data.display_texture _sprite.pixel_size = data.sprite_pixel_size + _sprite.position.y = ( + REFERENCE_SPRITE_HEIGHT + * data.sprite_pixel_size + / REFERENCE_SPRITE_PIXEL_SIZE + ) _sprite.rotation_degrees.x = data.sprite_tilt_degrees apply_network_state(position, yaw, true) @@ -80,7 +87,7 @@ func _ensure_visual() -> void: return _sprite = Sprite3D.new() _sprite.name = "GatherableSprite" - _sprite.position.y = 0.22 + _sprite.position.y = REFERENCE_SPRITE_HEIGHT _sprite.shaded = false _sprite.double_sided = true _sprite.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST diff --git a/network/network_player_animation_protocol.gd b/network/network_player_animation_protocol.gd index b27cb9d..40fe1c8 100644 --- a/network/network_player_animation_protocol.gd +++ b/network/network_player_animation_protocol.gd @@ -9,6 +9,7 @@ const MAX_ACTION_ELAPSED_SECONDS: float = 86400.0 const LOCOMOTION_IDLE: StringName = &"idle" const LOCOMOTION_WALKING: StringName = &"walking" const LOCOMOTION_RUNNING: StringName = &"running" +const LOCOMOTION_SNEAKING: StringName = &"sneaking" static func make_state( @@ -17,13 +18,17 @@ static func make_state( action_id: StringName = &"", action_sequence: int = 0, action_elapsed: float = 0.0, + action_paused: bool = false, ) -> Dictionary: return { "format_version": FORMAT_VERSION, "locomotion_id": String(locomotion_id), "grounded": grounded, "action": make_action_state( - action_id, action_sequence, action_elapsed + action_id, + action_sequence, + action_elapsed, + action_paused, ), } @@ -32,11 +37,13 @@ static func make_action_state( action_id: StringName = &"", action_sequence: int = 0, action_elapsed: float = 0.0, + paused: bool = false, ) -> Dictionary: return { "id": String(action_id), "sequence": action_sequence, "elapsed": action_elapsed, + "paused": paused, } @@ -61,6 +68,7 @@ static func validate_action_state(value: Variant) -> bool: not valid_state_id(action.get("id"), true) or typeof(action.get("sequence")) != TYPE_INT or typeof(action.get("elapsed")) not in [TYPE_FLOAT, TYPE_INT] + or typeof(action.get("paused", false)) != TYPE_BOOL ): return false var sequence: int = int(action["sequence"]) diff --git a/player/fishing_rod_attachment.tscn b/player/fishing_rod_attachment.tscn index f284825..d803e28 100644 --- a/player/fishing_rod_attachment.tscn +++ b/player/fishing_rod_attachment.tscn @@ -1,4 +1,6 @@ -[gd_scene load_steps=3 format=3] +[gd_scene load_steps=4 format=3] + +[ext_resource type="Script" path="res://player/tool_socket_attachment.gd" id="1_socket"] [sub_resource type="CylinderMesh" id="FishingRodMesh"] top_radius = 0.025 @@ -11,6 +13,8 @@ roughness = 0.8 [node name="FishingRodAttachment" type="BoneAttachment3D"] bone_name = "rod_socket" +script = ExtResource("1_socket") +aligned_mount_path = NodePath("FishingRod") [node name="FishingRod" type="Node3D" parent="."] diff --git a/player/net_attachment.gd b/player/net_attachment.gd new file mode 100644 index 0000000..3c7843a --- /dev/null +++ b/player/net_attachment.gd @@ -0,0 +1,88 @@ +extends "res://player/tool_socket_attachment.gd" + +const TextureSamplingPolicyScript := preload( + "res://main/texture_sampling_policy.gd" +) +const SPRING_ROOT_BONE: StringName = &"net_mid" +const SPRING_END_BONE: StringName = &"net_tip" +const IMPACT_BONE: StringName = &"net_rim" + +@export_range(0.0, 10.0, 0.05) var spring_stiffness: float = 1.8 +@export_range(0.0, 1.0, 0.01) var spring_drag: float = 0.68 +@export_range(0.0, 10.0, 0.05) var spring_gravity: float = 0.3 +@export_range(0.01, 0.5, 0.01) var spring_end_length: float = 0.1 + +@onready var _catching_net: Node3D = %CatchingNet +@onready var _net_model: Node3D = %NetModel + +var _spring: SpringBoneSimulator3D +var _spring_was_visible: bool = false +var _net_skeleton: Skeleton3D + + +func _ready() -> void: + super._ready() + TextureSamplingPolicyScript.enforce_subtree(_net_model) + _configure_spring() + _sync_spring_activity() + + +func _process(_delta: float) -> void: + _sync_spring_activity() + + +func _configure_spring() -> void: + _net_skeleton = _net_model.find_child( + "Skeleton3D", true, false + ) as Skeleton3D + if _net_skeleton == null: + push_error("Imported net model is missing its Skeleton3D.") + return + if _net_skeleton.find_bone(SPRING_ROOT_BONE) < 0: + push_error("Imported net model is missing bone %s." % SPRING_ROOT_BONE) + return + if _net_skeleton.find_bone(SPRING_END_BONE) < 0: + push_error("Imported net model is missing bone %s." % SPRING_END_BONE) + return + if _net_skeleton.find_bone(IMPACT_BONE) < 0: + push_error("Imported net model is missing bone %s." % IMPACT_BONE) + return + + _spring = SpringBoneSimulator3D.new() + _spring.name = "NetSpring" + _spring.set_setting_count(1) + _spring.set_root_bone_name(0, SPRING_ROOT_BONE) + _spring.set_end_bone_name(0, SPRING_END_BONE) + _spring.set_extend_end_bone(0, true) + _spring.set_end_bone_direction( + 0, SkeletonModifier3D.BONE_DIRECTION_FROM_PARENT + ) + _spring.set_end_bone_length(0, spring_end_length) + _spring.set_stiffness(0, spring_stiffness) + _spring.set_drag(0, spring_drag) + _spring.set_gravity(0, spring_gravity) + _spring.set_gravity_direction(0, Vector3.DOWN) + _net_skeleton.add_child(_spring) + + +func get_impact_world_position() -> Vector3: + if _net_skeleton == null: + return _catching_net.global_position + var impact_bone_index := _net_skeleton.find_bone(IMPACT_BONE) + if impact_bone_index < 0: + return _catching_net.global_position + return _net_skeleton.global_transform * ( + _net_skeleton.get_bone_global_pose(impact_bone_index).origin + ) + + +func _sync_spring_activity() -> void: + if _spring == null: + return + var is_visible := _catching_net.is_visible_in_tree() + if is_visible and not _spring_was_visible: + _spring.active = true + _spring.reset() + elif not is_visible: + _spring.active = false + _spring_was_visible = is_visible diff --git a/player/net_attachment.gd.uid b/player/net_attachment.gd.uid new file mode 100644 index 0000000..9bf2fd6 --- /dev/null +++ b/player/net_attachment.gd.uid @@ -0,0 +1 @@ +uid://cw5vc02hoelgm diff --git a/player/net_attachment.tscn b/player/net_attachment.tscn index 7c16fca..da49a88 100644 --- a/player/net_attachment.tscn +++ b/player/net_attachment.tscn @@ -1,55 +1,20 @@ -[gd_scene load_steps=7 format=3] +[gd_scene load_steps=3 format=3] -[sub_resource type="StandardMaterial3D" id="HandleMaterial"] -albedo_color = Color(0.25, 0.13, 0.055, 1) -roughness = 0.9 - -[sub_resource type="CylinderMesh" id="HandleMesh"] -material = SubResource("HandleMaterial") -top_radius = 0.025 -bottom_radius = 0.035 -height = 1.25 - -[sub_resource type="StandardMaterial3D" id="HoopMaterial"] -albedo_color = Color(0.34, 0.39, 0.4, 1) -metallic = 0.25 -roughness = 0.65 - -[sub_resource type="TorusMesh" id="HoopMesh"] -material = SubResource("HoopMaterial") -inner_radius = 0.24 -outer_radius = 0.29 -rings = 12 -ring_segments = 18 - -[sub_resource type="StandardMaterial3D" id="NetMaterial"] -transparency = 1 -albedo_color = Color(0.65, 0.78, 0.75, 0.42) -shading_mode = 0 -cull_mode = 2 - -[sub_resource type="CylinderMesh" id="NetMesh"] -material = SubResource("NetMaterial") -top_radius = 0.24 -bottom_radius = 0.08 -height = 0.34 -radial_segments = 12 -rings = 1 +[ext_resource type="Script" path="res://player/net_attachment.gd" id="1_script"] +[ext_resource type="PackedScene" path="res://art/exported/items/equipment/net.glb" id="2_model"] [node name="NetAttachment" type="BoneAttachment3D"] bone_name = "rod_socket" +script = ExtResource("1_script") +aligned_mount_path = NodePath("CatchingNet") -[node name="CatchingNet" type="Node3D" parent="."] +[node name="CatchingNet" type="Node3D" parent="." unique_id=947004727] +unique_name_in_owner = true visible = false -[node name="Handle" type="MeshInstance3D" parent="CatchingNet"] -position = Vector3(0, 0.55, 0) -mesh = SubResource("HandleMesh") +[node name="ModelMount" type="Node3D" parent="CatchingNet"] +position = Vector3(-0.000217, -0.098636, -0.020218) +rotation = Vector3(0, 3.1415927, 0) -[node name="Hoop" type="MeshInstance3D" parent="CatchingNet"] -position = Vector3(0, 1.17, 0) -mesh = SubResource("HoopMesh") - -[node name="Bag" type="MeshInstance3D" parent="CatchingNet"] -position = Vector3(0, 1.0, 0) -mesh = SubResource("NetMesh") +[node name="NetModel" parent="CatchingNet/ModelMount" unique_id=1627767240 instance=ExtResource("2_model")] +unique_name_in_owner = true diff --git a/player/player.gd b/player/player.gd index 7dc9dc5..5514b57 100644 --- a/player/player.gd +++ b/player/player.gd @@ -32,6 +32,7 @@ const FishingRodAttachmentScene = preload( "res://player/fishing_rod_attachment.tscn" ) const NetAttachmentScene = preload("res://player/net_attachment.tscn") +const NetAttachmentType = preload("res://player/net_attachment.gd") const FishingRodDataType = preload("res://items/fishing_rod_data.gd") const HeldItemAttachmentScene = preload( "res://player/held_item_attachment.tscn" @@ -51,6 +52,8 @@ const CHARACTER_WALKING_ANIMATION: StringName = &"walking" const CHARACTER_WALKING_SHOW_ANIMATION: StringName = &"walking_show" const CHARACTER_RUNNING_ANIMATION: StringName = &"running" const CHARACTER_RUNNING_SHOW_ANIMATION: StringName = &"running_show" +const CHARACTER_IDLE_SNEAK_ANIMATION: StringName = &"idle_sneak" +const CHARACTER_SNEAKING_ANIMATION: StringName = &"sneaking" const CHARACTER_CASTING_ANIMATION: StringName = &"casting" const CHARACTER_CASTING_SIT_ANIMATION: StringName = &"casting_sit" const CHARACTER_RELEASE_ANIMATION: StringName = &"release" @@ -73,10 +76,16 @@ const CHARACTER_FISHING_ANIMATION: StringName = &"fishing" const CHARACTER_FISHING_SIT_ANIMATION: StringName = &"fishing_sit" const CHARACTER_FIGHTING_ANIMATION: StringName = &"fighting" const CHARACTER_FIGHTING_SIT_ANIMATION: StringName = &"fighting_sit" +const CHARACTER_NET_DRAW_ANIMATION: StringName = &"draw" +const CHARACTER_NET_STRIKE_ANIMATION: StringName = &"strike" # Add future networked emote animation IDs here. The protocol accepts unknown # safe IDs so newer clients can extend it, but Player only presents actions # explicitly approved by this catalog. -const NETWORK_ANIMATION_ACTION_IDS: Array[StringName] = [] +const NETWORK_ANIMATION_ACTION_IDS: Array[StringName] = [ + CHARACTER_CASTING_ANIMATION, + CHARACTER_NET_DRAW_ANIMATION, + CHARACTER_NET_STRIKE_ANIMATION, +] enum FishingVisualPhase { NONE, @@ -98,6 +107,7 @@ enum LocomotionState { IDLE, WALKING, RUNNING, + SNEAKING, } const FIGHTING_EYES_ID: String = "alligator_eyes" const BLINK_EYES_ID: String = "closed" @@ -342,6 +352,10 @@ class ShowcaseCameraSnapshot: @export_range(0.5, 3.0, 0.05) var showcase_camera_target_height: float = 1.45 @export_range(0.01, 1.0, 0.01) var catch_presentation_base_scale: float = 0.10 +@export_category("Gathering") +@export_flags_3d_physics var net_impact_collision_mask: int = 1 +@export_range(0.02, 0.5, 0.01) var net_impact_radius: float = 0.16 + @export_category("Camera") @export var mouse_sensitivity: float = 0.005 @export var controller_camera_speed: float = 2.5 @@ -443,6 +457,7 @@ var _network_target_locomotion_state: LocomotionState = LocomotionState.IDLE var _network_target_animation_action_id: StringName = &"" var _network_target_animation_action_sequence: int = 0 var _network_target_animation_action_elapsed: float = 0.0 +var _network_target_animation_action_paused: bool = false var _network_snapshot_ready: bool = false var _network_snapshot_age: float = 0.0 var _network_snapshot_jitter: float = 0.0 @@ -451,8 +466,10 @@ var _local_network_jump_intent_sequence: int = -1 var _animation_action_id: StringName = &"" var _animation_action_sequence: int = 0 var _animation_action_elapsed: float = 0.0 +var _animation_action_paused: bool = false var _presented_animation_action_id: StringName = &"" var _presented_animation_action_sequence: int = -1 +var _presented_animation_action_paused: bool = false var _character_animation_name: StringName = &"" var _sitting: bool = false var _sit_after_landing: bool = false @@ -483,6 +500,17 @@ var _fishing_after_release_pending: bool = false var _retract_animation_completed: bool = true var _fishing_rod: Node3D var _catching_net: Node3D +var _catching_net_attachment: NetAttachmentType +var _net_strike_collision_armed: bool = false +var _net_strike_has_previous_sample: bool = false +var _net_strike_previous_position: Vector3 +var _net_strike_previous_animation_position: float = 0.0 +var _net_strike_contact_held: bool = false +var _net_strike_result_known: bool = false +var _net_strike_successful: bool = false +var _net_impact_shape := SphereShape3D.new() +var _pending_net_showcase_catch: FishCatchType +var _pending_net_showcase_remote: bool = false var _fishing_rod_tip: Marker3D var _fishing_rod_model_mount: Node3D var _fishing_rod_fallback_visual: GeometryInstance3D @@ -584,6 +612,141 @@ func end_animation_action() -> void: ) +func begin_net_draw_visual() -> bool: + return begin_animation_action( + CHARACTER_NET_DRAW_ANIMATION + if is_sneaking() + else CHARACTER_CASTING_ANIMATION + ) + + +func play_net_strike_visual() -> bool: + if begin_animation_action(CHARACTER_NET_STRIKE_ANIMATION): + _net_strike_collision_armed = local_control_enabled + _net_strike_has_previous_sample = false + _net_strike_contact_held = false + _net_strike_result_known = false + _net_strike_successful = false + return true + cancel_net_action_visual() + return false + + +func resolve_net_strike_visual(successful: bool) -> void: + if _animation_action_id != CHARACTER_NET_STRIKE_ANIMATION: + return + _net_strike_result_known = true + _net_strike_successful = successful + if successful: + _net_strike_collision_armed = false + if successful and _animation_action_paused: + _net_strike_contact_held = false + _set_animation_action_paused(false) + + +func is_net_strike_held() -> bool: + return ( + _animation_action_id == CHARACTER_NET_STRIKE_ANIMATION + and _net_strike_contact_held + ) + + +func release_net_strike_hold() -> bool: + if not is_net_strike_held(): + return false + cancel_net_action_visual() + return true + + +func cancel_net_action_visual() -> void: + _clear_net_strike_state() + if _animation_action_id in [ + CHARACTER_CASTING_ANIMATION, + CHARACTER_NET_DRAW_ANIMATION, + CHARACTER_NET_STRIKE_ANIMATION, + ]: + end_animation_action() + + +func _clear_net_strike_state() -> void: + _net_strike_collision_armed = false + _net_strike_has_previous_sample = false + _net_strike_contact_held = false + _net_strike_result_known = false + _net_strike_successful = false + if _animation_action_paused: + _set_animation_action_paused(false) + + +func _set_animation_action_paused(paused: bool) -> void: + if _animation_action_id.is_empty() or _animation_action_paused == paused: + return + _apply_animation_action_state( + NetworkPlayerAnimationProtocol.make_action_state( + _animation_action_id, + _animation_action_sequence, + _animation_action_elapsed, + paused, + ) + ) + + +func _update_net_strike_contact() -> void: + if ( + not local_control_enabled + or not _net_strike_collision_armed + or _animation_action_paused + or _animation_action_id != CHARACTER_NET_STRIKE_ANIMATION + or _catching_net_attachment == null + or _character_animation_player == null + or get_world_3d() == null + ): + return + var current_position: Vector3 = ( + _catching_net_attachment.get_impact_world_position() + ) + var current_animation_position: float = ( + _character_animation_player.current_animation_position + ) + if not _net_strike_has_previous_sample: + _net_strike_previous_position = current_position + _net_strike_previous_animation_position = current_animation_position + _net_strike_has_previous_sample = true + return + var motion := current_position - _net_strike_previous_position + if motion.length_squared() <= 0.000001: + _net_strike_previous_animation_position = current_animation_position + return + _net_impact_shape.radius = net_impact_radius + var query := PhysicsShapeQueryParameters3D.new() + query.shape = _net_impact_shape + query.transform = Transform3D( + Basis.IDENTITY, + _net_strike_previous_position, + ) + query.motion = motion + query.collision_mask = net_impact_collision_mask + query.collide_with_areas = false + query.collide_with_bodies = true + query.exclude = [get_rid()] + var fractions: PackedFloat32Array = ( + get_world_3d().direct_space_state.cast_motion(query) + ) + if fractions.size() >= 2 and float(fractions[1]) < 1.0: + var impact_fraction := clampf(float(fractions[0]), 0.0, 1.0) + _animation_action_elapsed = lerpf( + _net_strike_previous_animation_position, + current_animation_position, + impact_fraction, + ) + _net_strike_collision_armed = false + _net_strike_contact_held = true + _set_animation_action_paused(true) + return + _net_strike_previous_position = current_position + _net_strike_previous_animation_position = current_animation_position + + func _apply_animation_action_state(action_state: Dictionary) -> void: if not NetworkPlayerAnimationProtocol.validate_action_state(action_state): return @@ -597,10 +760,13 @@ func _apply_animation_action_state(action_state: Dictionary) -> void: var action_changed: bool = ( action_id != _animation_action_id or action_sequence != _animation_action_sequence + or bool(action_state.get("paused", false)) + != _animation_action_paused ) _animation_action_id = action_id _animation_action_sequence = action_sequence _animation_action_elapsed = float(action_state["elapsed"]) + _animation_action_paused = bool(action_state.get("paused", false)) if action_changed: _character_animation_name = &"" _update_character_animation() @@ -637,11 +803,12 @@ func _initialize_catching_net() -> void: if skeleton == null: push_error("Player character skeleton is unavailable for the net.") return - var attachment := NetAttachmentScene.instantiate() as BoneAttachment3D + var attachment := NetAttachmentScene.instantiate() as NetAttachmentType if attachment == null: push_error("Catching net attachment could not be instantiated.") return skeleton.add_child(attachment) + _catching_net_attachment = attachment _catching_net = attachment.get_node("CatchingNet") as Node3D if _catching_net != null: _catching_net.visible = false @@ -760,6 +927,8 @@ func _physics_process(delta: float) -> void: move_direction.y = 0.0 var input_strength: float = minf(input_vector.length(), 1.0) move_direction = move_direction.normalized() + if input_strength > 0.05: + release_net_strike_hold() var speed: float = _get_network_aware_speed() if item_effects != null: @@ -779,13 +948,14 @@ func _physics_process(delta: float) -> void: func _process(delta: float) -> void: - if not _animation_action_id.is_empty(): + if not _animation_action_id.is_empty() and not _animation_action_paused: _animation_action_elapsed = minf( _animation_action_elapsed + delta, NetworkPlayerAnimationProtocol.MAX_ACTION_ELAPSED_SECONDS, ) _update_blink(delta) _update_character_animation() + _update_net_strike_contact() _update_sprint_dust(delta) # The hand bone supplies the attachment position, but its animated wrist # rotation should not turn the flat fish/catch artwork edge-on. Keep each @@ -963,8 +1133,17 @@ func _update_character_animation() -> void: ) var is_walking: bool = locomotion_state != LocomotionState.IDLE var is_running: bool = locomotion_state == LocomotionState.RUNNING + var is_sneaking_pose: bool = ( + locomotion_state == LocomotionState.SNEAKING + ) + var is_moving_horizontally: bool = ( + _is_presented_moving_horizontally() + ) var animation_action: Dictionary = _get_presented_animation_action() var animation_action_id := StringName(str(animation_action.get("id", ""))) + var animation_action_paused: bool = bool( + animation_action.get("paused", false) + ) var animation_action_available: bool = ( not animation_action_id.is_empty() and supports_network_animation_action(animation_action_id) @@ -1075,6 +1254,19 @@ func _update_character_animation() -> void: &"sitting", &"idle_loop_sit", ] + elif is_sneaking_pose: + if is_moving_horizontally: + requested_animation = [ + CHARACTER_SNEAKING_ANIMATION, + CHARACTER_WALKING_ANIMATION, + &"walking_loop", + ] + else: + requested_animation = [ + CHARACTER_IDLE_SNEAK_ANIMATION, + CHARACTER_IDLE_ANIMATION, + &"idle_loop", + ] elif is_running and held_show_item_visible: requested_animation = [ CHARACTER_RUNNING_SHOW_ANIMATION, @@ -1124,11 +1316,14 @@ func _update_character_animation() -> void: and ( animation_action_id != _presented_animation_action_id or action_sequence != _presented_animation_action_sequence + or animation_action_paused + != _presented_animation_action_paused ) ) if not action_selected: _presented_animation_action_id = &"" _presented_animation_action_sequence = -1 + _presented_animation_action_paused = false if _character_animation_name == next_animation and not action_changed: return _character_animation_player.play(next_animation) @@ -1136,22 +1331,58 @@ func _update_character_animation() -> void: if action_selected: _presented_animation_action_id = animation_action_id _presented_animation_action_sequence = action_sequence + _presented_animation_action_paused = animation_action_paused var elapsed: float = float(animation_action.get("elapsed", 0.0)) var animation: Animation = _character_animation_player.get_animation( next_animation ) - if elapsed > 0.0 and animation != null and animation.length > 0.0: + if ( + (elapsed > 0.0 or animation_action_paused) + and animation != null + and animation.length > 0.0 + ): _character_animation_player.seek( fposmod(elapsed, animation.length), true ) + if animation_action_paused: + _character_animation_player.pause() func _on_character_animation_finished(animation_name: StringName) -> void: + if animation_name == CHARACTER_NET_STRIKE_ANIMATION: + if local_control_enabled: + if _net_strike_result_known and _net_strike_successful: + _clear_net_strike_state() + end_animation_action() + _begin_pending_net_showcase() + else: + var strike_animation := ( + _character_animation_player.get_animation( + CHARACTER_NET_STRIKE_ANIMATION + ) + ) + if strike_animation != null: + _animation_action_elapsed = maxf( + strike_animation.length - 0.001, + 0.0, + ) + _net_strike_collision_armed = false + _net_strike_contact_held = true + _set_animation_action_paused(true) + return + if _pending_net_showcase_remote: + _begin_pending_net_showcase() + return if ( local_control_enabled and not _animation_action_id.is_empty() and animation_name == _animation_action_id ): + if animation_name in [ + CHARACTER_CASTING_ANIMATION, + CHARACTER_NET_DRAW_ANIMATION, + ]: + return end_animation_action() return if animation_name in [ @@ -1551,6 +1782,7 @@ func configure_network_remote(authoritative_simulation: bool) -> void: _network_target_animation_action_id = &"" _network_target_animation_action_sequence = 0 _network_target_animation_action_elapsed = 0.0 + _network_target_animation_action_paused = false _camera.current = false @@ -1665,6 +1897,7 @@ func make_network_snapshot(peer_id: int) -> Dictionary: _animation_action_id, _animation_action_sequence, _animation_action_elapsed, + _animation_action_paused, ), "sitting": _sitting, "casting": _fishing_visual_phase == FishingVisualPhase.CASTING, @@ -1876,6 +2109,7 @@ func _make_animation_action_state() -> Dictionary: _animation_action_id, _animation_action_sequence, _animation_action_elapsed, + _animation_action_paused, ) @@ -1890,6 +2124,9 @@ func _apply_network_target_animation_state(state: Dictionary) -> void: _network_target_animation_action_id = StringName(str(action["id"])) _network_target_animation_action_sequence = int(action["sequence"]) _network_target_animation_action_elapsed = float(action["elapsed"]) + _network_target_animation_action_paused = bool( + action.get("paused", false) + ) func _get_authoritative_locomotion_state() -> LocomotionState: @@ -1898,14 +2135,17 @@ func _get_authoritative_locomotion_state() -> LocomotionState: and _is_movement_input_enabled() and not _water_recovery_active and not _sitting - and _network_axis.length_squared() > 0.0025 ): + if _network_sneak: + return LocomotionState.SNEAKING + if _network_axis.length_squared() <= 0.0025: + return LocomotionState.IDLE return ( LocomotionState.RUNNING if _network_sprint else LocomotionState.WALKING ) - return _locomotion_state_from_velocity() + return _locomotion_state_from_velocity(is_sneaking()) func _get_authoritative_locomotion_id() -> StringName: @@ -1915,7 +2155,19 @@ func _get_authoritative_locomotion_id() -> StringName: func _get_presented_locomotion_state() -> LocomotionState: if _network_interpolation_enabled and _network_snapshot_ready: return _network_target_locomotion_state - return _locomotion_state_from_velocity() + return _locomotion_state_from_velocity(is_sneaking()) + + +func _is_presented_moving_horizontally() -> bool: + var presented_velocity: Vector3 = ( + _network_target_velocity + if _network_interpolation_enabled and _network_snapshot_ready + else velocity + ) + return Vector2( + presented_velocity.x, + presented_velocity.z, + ).length_squared() > 0.0025 func _get_presented_animation_action() -> Dictionary: @@ -1924,15 +2176,23 @@ func _get_presented_animation_action() -> Dictionary: _network_target_animation_action_id, _network_target_animation_action_sequence, minf( - _network_target_animation_action_elapsed + _network_snapshot_age, + _network_target_animation_action_elapsed + + ( + 0.0 + if _network_target_animation_action_paused + else _network_snapshot_age + ), NetworkPlayerAnimationProtocol.MAX_ACTION_ELAPSED_SECONDS, ), + _network_target_animation_action_paused, ) return _make_animation_action_state() func _locomotion_id_from_state(state: LocomotionState) -> StringName: match state: + LocomotionState.SNEAKING: + return NetworkPlayerAnimationProtocol.LOCOMOTION_SNEAKING LocomotionState.WALKING: return NetworkPlayerAnimationProtocol.LOCOMOTION_WALKING LocomotionState.RUNNING: @@ -1943,6 +2203,8 @@ func _locomotion_id_from_state(state: LocomotionState) -> StringName: func _locomotion_state_from_id(state_id: StringName) -> LocomotionState: match state_id: + NetworkPlayerAnimationProtocol.LOCOMOTION_SNEAKING: + return LocomotionState.SNEAKING NetworkPlayerAnimationProtocol.LOCOMOTION_WALKING: return LocomotionState.WALKING NetworkPlayerAnimationProtocol.LOCOMOTION_RUNNING: @@ -1955,7 +2217,11 @@ static func supports_network_animation_action(action_id: StringName) -> bool: return action_id in NETWORK_ANIMATION_ACTION_IDS -func _locomotion_state_from_velocity() -> LocomotionState: +func _locomotion_state_from_velocity( + sneaking: bool = false, +) -> LocomotionState: + if sneaking: + return LocomotionState.SNEAKING var horizontal_speed_squared: float = ( velocity.x * velocity.x + velocity.z * velocity.z ) @@ -2234,6 +2500,8 @@ func set_active_fishing_rod( func set_active_catching_net(should_show: bool) -> void: _active_item_is_net = should_show + if not should_show: + cancel_net_action_visual() if _catching_net == null: return if _showcase_rod_state_stored: @@ -2602,6 +2870,14 @@ func get_cast_origin_position() -> Vector3: func begin_catch_showcase(fish_catch: FishCatchType) -> void: if fish_catch == null or not fish_catch.is_valid(): return + if _is_net_strike_visual_active(): + _pending_net_showcase_catch = fish_catch + _pending_net_showcase_remote = false + return + _begin_catch_showcase_now(fish_catch) + + +func _begin_catch_showcase_now(fish_catch: FishCatchType) -> void: if _pocket_visual_target == PocketVisualTarget.CATCH_SHOWCASE: _cancel_pocket_visual() _showcase_animation_active = true @@ -2636,6 +2912,14 @@ func begin_catch_showcase(fish_catch: FishCatchType) -> void: func begin_remote_catch_showcase(fish_catch: FishCatchType) -> void: if local_control_enabled or fish_catch == null or not fish_catch.is_valid(): return + if _is_net_strike_visual_active(): + _pending_net_showcase_catch = fish_catch + _pending_net_showcase_remote = true + return + _begin_remote_catch_showcase_now(fish_catch) + + +func _begin_remote_catch_showcase_now(fish_catch: FishCatchType) -> void: end_catch_showcase(Callable(), true) _showcase_animation_active = true set_fishing_visual(false) @@ -2657,6 +2941,32 @@ func begin_remote_catch_showcase(fish_catch: FishCatchType) -> void: _update_character_animation() +func _is_net_strike_visual_active() -> bool: + return ( + _animation_action_id == CHARACTER_NET_STRIKE_ANIMATION + or _network_target_animation_action_id + == CHARACTER_NET_STRIKE_ANIMATION + or ( + _character_animation_player != null + and _character_animation_player.current_animation + == CHARACTER_NET_STRIKE_ANIMATION + ) + ) + + +func _begin_pending_net_showcase() -> void: + var fish_catch := _pending_net_showcase_catch + var remote: bool = _pending_net_showcase_remote + _pending_net_showcase_catch = null + _pending_net_showcase_remote = false + if fish_catch == null or not fish_catch.is_valid(): + return + if remote: + _begin_remote_catch_showcase_now(fish_catch) + else: + _begin_catch_showcase_now(fish_catch) + + func end_catch_showcase( restored_callback: Callable = Callable(), immediate: bool = false, diff --git a/player/tool_socket_attachment.gd b/player/tool_socket_attachment.gd new file mode 100644 index 0000000..4bbd1af --- /dev/null +++ b/player/tool_socket_attachment.gd @@ -0,0 +1,57 @@ +class_name ToolSocketAttachment +extends BoneAttachment3D + +@export var direction_guide_bone: StringName = &"rod_socket.001" +@export_node_path("Node3D") var aligned_mount_path: NodePath + + +func _ready() -> void: + _align_mount_to_direction_guide() + + +func _align_mount_to_direction_guide() -> void: + var skeleton := get_parent() as Skeleton3D + var mount := get_node_or_null(aligned_mount_path) as Node3D + if skeleton == null or mount == null: + push_error("Tool socket alignment requires a Skeleton3D parent and mount.") + return + var socket_index := skeleton.find_bone(bone_name) + var guide_index := skeleton.find_bone(direction_guide_bone) + if socket_index < 0 or guide_index < 0: + push_error( + "Tool socket alignment is missing %s or %s." + % [bone_name, direction_guide_bone] + ) + return + if skeleton.get_bone_parent(guide_index) != socket_index: + push_error( + "Tool direction guide %s must be a child of %s." + % [direction_guide_bone, bone_name] + ) + return + var socket_rest := skeleton.get_bone_global_rest(socket_index) + var guide_rest := skeleton.get_bone_global_rest(guide_index) + var guide_in_socket := socket_rest.affine_inverse() * guide_rest + var rear_direction := guide_in_socket.basis.y.normalized() + # rod_socket already aims local +Y from the hand toward the working end. + # Its child guide is perpendicular to that axis and identifies which face + # of the held tool points rearward. Projecting it onto the roll plane keeps + # imperfect authored guides from changing the tool's lengthwise direction. + rear_direction -= Vector3.UP * rear_direction.dot(Vector3.UP) + if rear_direction.is_zero_approx(): + push_error("Tool direction guide has no usable direction.") + return + rear_direction = rear_direction.normalized() + var rear_roll := Vector3.BACK.signed_angle_to( + rear_direction, + Vector3.UP, + ) + var mount_transform := mount.transform + mount_transform.basis = ( + Basis(Vector3.UP, rear_roll).orthonormalized() + * mount_transform.basis + ) + # The guide starts at the socket tip, but the held tool must remain anchored + # at the socket origin in the hand. Only its authored direction is applied. + mount_transform.origin = mount.transform.origin + mount.transform = mount_transform diff --git a/player/tool_socket_attachment.gd.uid b/player/tool_socket_attachment.gd.uid new file mode 100644 index 0000000..7ddb71c --- /dev/null +++ b/player/tool_socket_attachment.gd.uid @@ -0,0 +1 @@ +uid://toolsocketalignment01 diff --git a/scripts/run_validations.sh b/scripts/run_validations.sh index 7fb248b..298c4f5 100755 --- a/scripts/run_validations.sh +++ b/scripts/run_validations.sh @@ -14,6 +14,7 @@ readonly -a QUICK_TESTS=( "scripts/validate_animalese_samples.gd" "tests/android_readiness_validation.gd" "tests/camera_drag_validation.gd" + "tests/character_rig_validation.gd" "tests/controller_focus_presentation_validation.gd" "tests/controller_menu_accessibility_validation.gd" "tests/controller_mapping_validation.gd" @@ -28,11 +29,14 @@ readonly -a QUICK_TESTS=( "tests/fishing_audio_validation.gd" "tests/fishing_surface_validation.gd" "tests/fur_pattern_validation.gd" + "tests/gathering_marker_surface_validation.gd" "tests/inventory_storage_validation.gd" "tests/keyboard_mouse_mapping_validation.gd" "tests/logbook_validation.gd" "tests/network_player_animation_protocol_validation.gd" + "tests/net_attachment_validation.gd" "tests/on_screen_keyboard_validation.gd" + "tests/player_gathering_animation_validation.gd" "tests/player_experience_validation.gd" "tests/shoreline_ambience_validation.gd" "tests/surface_drawing_validation.gd" diff --git a/tests/character_rig_validation.gd b/tests/character_rig_validation.gd new file mode 100644 index 0000000..c8fc57d --- /dev/null +++ b/tests/character_rig_validation.gd @@ -0,0 +1,139 @@ +extends SceneTree + + +const CHARACTER_SCENE: PackedScene = preload( + "res://art/exported/characters/base/netfishing_base_character.glb" +) + +const EXPECTED_ANIMATIONS: Array[StringName] = [ + &"casting", + &"casting_sit", + &"draw", + &"fighting", + &"fighting_sit", + &"fishing", + &"fishing_sit", + &"idle", + &"idle_show", + &"idle_sit", + &"idle_sit_show", + &"idle_sneak", + &"pocket_idle_idle", + &"pocket_idle_show", + &"pocket_show_idle", + &"pocket_show_show", + &"pocket_sit_idle_idle", + &"pocket_sit_idle_show", + &"pocket_sit_show_idle", + &"pocket_sit_show_show", + &"pocket_walking_idle_idle", + &"pocket_walking_idle_show", + &"pocket_walking_show_idle", + &"pocket_walking_show_show", + &"release", + &"release_sit", + &"retract", + &"retract_sit", + &"running", + &"running_show", + &"sneaking", + &"strike", + &"walking", + &"walking_show", +] + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + var character := CHARACTER_SCENE.instantiate() + root.add_child(character) + var skeleton := _find_first(character, "Skeleton3D") as Skeleton3D + var animation_player := _find_first( + character, + "AnimationPlayer", + ) as AnimationPlayer + assert(skeleton != null) + assert(animation_player != null) + + var socket_index := skeleton.find_bone("rod_socket") + assert(socket_index >= 0) + var socket_direction_index := skeleton.find_bone("rod_socket.001") + assert(socket_direction_index >= 0) + assert(skeleton.get_bone_parent(socket_direction_index) == socket_index) + var animation_names := animation_player.get_animation_list() + assert(animation_names.size() == EXPECTED_ANIMATIONS.size()) + for expected in EXPECTED_ANIMATIONS: + assert(expected in animation_names, "Missing animation: %s" % expected) + var animation := animation_player.get_animation(expected) + if expected in [&"casting", &"draw", &"idle_sneak", &"sneaking", &"strike"]: + var has_socket_track: bool = false + for track_index in animation.get_track_count(): + has_socket_track = has_socket_track or String( + animation.track_get_path(track_index) + ).contains("rod_socket") + assert( + has_socket_track, + "Animation %s lost its authored rod socket pose." % expected, + ) + + var arms := character.find_child( + "body_arms", + true, + false, + ) as MeshInstance3D + assert(arms != null) + var blended_arm_vertices := _validate_mesh_weights(arms) + assert(blended_arm_vertices > 0, "body_arms has no blended joint weights.") + var body := character.find_child( + "body_main", + true, + false, + ) as MeshInstance3D + assert(body != null) + _validate_mesh_weights(body) + + print( + "Character rig validation: PASS " + + "(animations=%d, blended arm vertices=%d)" + % [EXPECTED_ANIMATIONS.size(), blended_arm_vertices] + ) + character.queue_free() + quit() + + +func _validate_mesh_weights(mesh_instance: MeshInstance3D) -> int: + var blended_vertices := 0 + for surface_index in mesh_instance.mesh.get_surface_count(): + var arrays := mesh_instance.mesh.surface_get_arrays(surface_index) + var weights: PackedFloat32Array = arrays[Mesh.ARRAY_WEIGHTS] + var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX] + assert(not weights.is_empty()) + assert(not vertices.is_empty()) + var components_per_vertex := weights.size() / vertices.size() + assert(components_per_vertex == 4 or components_per_vertex == 8) + for offset in range(0, weights.size(), components_per_vertex): + var total := 0.0 + var influence_count := 0 + for component in range(components_per_vertex): + var weight := weights[offset + component] + total += weight + if weight > 0.000001: + influence_count += 1 + assert(absf(total - 1.0) <= 0.0001) + assert(influence_count <= 2) + if influence_count == 2: + blended_vertices += 1 + return blended_vertices + + +func _find_first(parent: Node, class_name_to_find: String) -> Node: + if parent.is_class(class_name_to_find): + return parent + for child in parent.get_children(): + var found := _find_first(child, class_name_to_find) + if found != null: + return found + return null diff --git a/tests/character_rig_validation.gd.uid b/tests/character_rig_validation.gd.uid new file mode 100644 index 0000000..f7bda09 --- /dev/null +++ b/tests/character_rig_validation.gd.uid @@ -0,0 +1 @@ +uid://di6r1gphajq8u diff --git a/tests/gathering_marker_surface_validation.gd b/tests/gathering_marker_surface_validation.gd new file mode 100644 index 0000000..1ae48a5 --- /dev/null +++ b/tests/gathering_marker_surface_validation.gd @@ -0,0 +1,66 @@ +extends SceneTree + +const PlayerScene: PackedScene = preload("res://player/player.tscn") +const GatheringControllerType = preload( + "res://gathering/gathering_controller.gd" +) + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + var player := PlayerScene.instantiate() as Player + root.add_child(player) + player.set_process(false) + player.set_physics_process(false) + + var controller := GatheringControllerType.new() + root.add_child(controller) + controller.set("_player", player) + + var wall := _make_surface( + Vector3(2.0, 2.0, 0.1), + Vector3(0.0, 0.9, -1.5), + ) + root.add_child(wall) + await physics_frame + controller.call("_update_marker_target") + var marker := controller.get("_marker") as MeshInstance3D + assert(marker != null) + assert(bool(controller.get("_marker_has_surface"))) + assert(marker.global_basis.y.dot(Vector3.BACK) > 0.99) + + wall.queue_free() + await process_frame + var floor := _make_surface( + Vector3(10.0, 0.2, 10.0), + Vector3(0.0, -0.1, -1.7), + ) + root.add_child(floor) + await physics_frame + controller.call("_update_marker_target") + assert(bool(controller.get("_marker_has_surface"))) + assert(marker.global_basis.y.dot(Vector3.UP) > 0.99) + assert(is_equal_approx(absf(marker.global_basis.determinant()), 1.0)) + + floor.queue_free() + controller.queue_free() + player.queue_free() + await process_frame + print("Gathering marker surface validation: PASS") + quit() + + +func _make_surface(size: Vector3, position: Vector3) -> StaticBody3D: + var body := StaticBody3D.new() + body.position = position + body.collision_layer = 1 + body.collision_mask = 0 + var collision := CollisionShape3D.new() + var shape := BoxShape3D.new() + shape.size = size + collision.shape = shape + body.add_child(collision) + return body diff --git a/tests/gathering_marker_surface_validation.gd.uid b/tests/gathering_marker_surface_validation.gd.uid new file mode 100644 index 0000000..044defc --- /dev/null +++ b/tests/gathering_marker_surface_validation.gd.uid @@ -0,0 +1 @@ +uid://ddsf88kuxgsqp diff --git a/tests/movement_multiplayer_validation.gd b/tests/movement_multiplayer_validation.gd index 274544a..aec3e35 100644 --- a/tests/movement_multiplayer_validation.gd +++ b/tests/movement_multiplayer_validation.gd @@ -241,7 +241,25 @@ func _run_host() -> void: await create_timer(2.5).timeout player.apply_authoritative_network_input(_movement_input(2, false)) await create_timer(2.5).timeout - player.apply_authoritative_network_input(_movement_input(3, false, false)) + player.apply_authoritative_network_input( + _movement_input(3, false, true, true) + ) + await create_timer(2.0).timeout + player.apply_authoritative_network_input( + _movement_input(4, false, false, true) + ) + await create_timer(2.0).timeout + player.apply_authoritative_network_input( + _movement_input(5, false, false, true, &"draw", 1) + ) + await create_timer(1.0).timeout + player.apply_authoritative_network_input( + _movement_input(6, false, false, true, &"strike", 2) + ) + await create_timer(0.5).timeout + player.apply_authoritative_network_input( + _movement_input(7, false, false, false, &"", 3) + ) var disconnect_deadline: int = Time.get_ticks_msec() + 8000 while ( Time.get_ticks_msec() < disconnect_deadline @@ -284,16 +302,24 @@ func _run_client() -> void: var sprint_dust := host_avatar.get_node("%SprintDust") as SprintDustTrail var saw_running: bool = false var saw_walking: bool = false + var saw_sneaking: bool = false + var saw_idle_sneak: bool = false + var saw_net_draw: bool = false + var saw_net_strike: bool = false var saw_animation_advance: bool = false var saw_dust: bool = false var previous_animation: StringName = &"" var previous_animation_position: float = -1.0 - var observation_deadline: int = Time.get_ticks_msec() + 7000 + var observation_deadline: int = Time.get_ticks_msec() + 14000 while Time.get_ticks_msec() < observation_deadline: await process_frame var current_animation: StringName = animation_player.current_animation saw_running = saw_running or current_animation.begins_with("running") saw_walking = saw_walking or current_animation.begins_with("walking") + saw_sneaking = saw_sneaking or current_animation == &"sneaking" + saw_idle_sneak = saw_idle_sneak or current_animation == &"idle_sneak" + saw_net_draw = saw_net_draw or current_animation == &"draw" + saw_net_strike = saw_net_strike or current_animation == &"strike" var current_position: float = ( animation_player.current_animation_position ) @@ -306,10 +332,23 @@ func _run_client() -> void: previous_animation = current_animation previous_animation_position = current_position saw_dust = saw_dust or sprint_dust.get_active_puff_count() > 0 - if saw_running and saw_walking and saw_animation_advance and saw_dust: + if ( + saw_running + and saw_walking + and saw_sneaking + and saw_idle_sneak + and saw_net_draw + and saw_net_strike + and saw_animation_advance + and saw_dust + ): break assert(saw_running) assert(saw_walking) + assert(saw_sneaking) + assert(saw_idle_sneak) + assert(saw_net_draw) + assert(saw_net_strike) assert(saw_animation_advance) assert(saw_dust) print("Movement multiplayer client validation: PASS") @@ -325,6 +364,9 @@ func _movement_input( sequence: int, sprinting: bool, moving: bool = true, + sneaking: bool = false, + action_id: StringName = &"", + action_sequence: int = 0, ) -> Dictionary: return { "sequence": sequence, @@ -332,12 +374,13 @@ func _movement_input( "camera_yaw": 0.0, "jump": false, "sprint": sprinting, - "sneak": false, + "sneak": sneaking, "slow_walk": false, "sitting": false, "casting": false, - "animation_action": ( - NetworkPlayerAnimationProtocol.make_action_state() + "animation_action": NetworkPlayerAnimationProtocol.make_action_state( + action_id, + action_sequence, ), } diff --git a/tests/net_attachment_validation.gd b/tests/net_attachment_validation.gd new file mode 100644 index 0000000..2ace76f --- /dev/null +++ b/tests/net_attachment_validation.gd @@ -0,0 +1,157 @@ +extends SceneTree + +const NetAttachmentScene: PackedScene = preload( + "res://player/net_attachment.tscn" +) +const FishingRodAttachmentScene: PackedScene = preload( + "res://player/fishing_rod_attachment.tscn" +) + +var failures: Array[String] = [] + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + var host_skeleton := Skeleton3D.new() + host_skeleton.name = "HostSkeleton" + host_skeleton.add_bone("rod_socket") + host_skeleton.add_bone("rod_socket.001") + host_skeleton.set_bone_parent(1, 0) + host_skeleton.set_bone_rest( + 1, + Transform3D( + Basis(Quaternion(Vector3.UP, Vector3.RIGHT)), + Vector3(0.0, 0.25, 0.0), + ), + ) + root.add_child(host_skeleton) + + var attachment := NetAttachmentScene.instantiate() as BoneAttachment3D + host_skeleton.add_child(attachment) + await process_frame + await process_frame + + _check( + attachment.bone_name == "rod_socket", + "Attachment moved off rod_socket.", + ) + var catching_net := attachment.get_node_or_null("CatchingNet") as Node3D + _check(catching_net != null, "CatchingNet presentation root is missing.") + if catching_net == null: + _finish(host_skeleton) + return + _check(not catching_net.visible, "Net must start hidden.") + _check( + catching_net.position.is_zero_approx(), + "Net direction alignment moved its hand anchor.", + ) + _check( + catching_net.basis.y.is_equal_approx(Vector3.UP), + "Net working axis no longer follows rod_socket.", + ) + _check( + catching_net.basis.z.is_equal_approx(Vector3.RIGHT), + "Net rear face did not follow the socket direction guide.", + ) + + var model := catching_net.get_node_or_null("ModelMount/NetModel") as Node3D + _check(model != null, "Imported net model is missing.") + if model == null: + _finish(host_skeleton) + return + var skeleton := model.find_child("Skeleton3D", true, false) as Skeleton3D + _check(skeleton != null, "Imported net Skeleton3D is missing.") + if skeleton == null: + _finish(host_skeleton) + return + _check( + skeleton.find_bone("net_root") >= 0, + "Imported net_root bone is missing.", + ) + _check( + skeleton.find_bone("net_rim") >= 0, + "Imported net_rim bone is missing.", + ) + _check( + skeleton.find_bone("net_mid") >= 0, + "Imported net_mid bone is missing.", + ) + _check( + skeleton.find_bone("net_tip") >= 0, + "Imported net_tip bone is missing.", + ) + + var spring := skeleton.get_node_or_null("NetSpring") as SpringBoneSimulator3D + _check(spring != null, "Net spring simulator was not created.") + if spring != null: + _check(spring.get_setting_count() == 1, "Net spring settings changed.") + _check( + spring.get_root_bone_name(0) == &"net_mid", + "Net spring root must remain net_mid.", + ) + _check( + spring.get_end_bone_name(0) == &"net_tip", + "Net spring end must remain net_tip.", + ) + _check(not spring.active, "Hidden net spring must be suspended.") + catching_net.visible = true + await process_frame + await process_frame + _check(spring.active, "Visible net spring must be active.") + _check(spring.get_joint_count(0) == 2, "Net spring chain is incomplete.") + catching_net.visible = false + await process_frame + _check(not spring.active, "Hidden net spring did not suspend.") + + for child: Node in model.find_children("*", "MeshInstance3D", true, false): + var mesh_instance := child as MeshInstance3D + if mesh_instance.mesh == null: + continue + for surface_index: int in mesh_instance.mesh.get_surface_count(): + var material := mesh_instance.mesh.surface_get_material(surface_index) + if material is BaseMaterial3D: + _check( + (material as BaseMaterial3D).texture_filter + == BaseMaterial3D.TEXTURE_FILTER_NEAREST, + "Net material does not use nearest texture sampling.", + ) + + var rod_attachment := ( + FishingRodAttachmentScene.instantiate() as BoneAttachment3D + ) + host_skeleton.add_child(rod_attachment) + await process_frame + var fishing_rod := rod_attachment.get_node("FishingRod") as Node3D + _check( + fishing_rod.position.is_zero_approx(), + "Rod direction alignment moved its hand anchor.", + ) + _check( + fishing_rod.basis.y.is_equal_approx(Vector3.UP), + "Rod working axis no longer follows rod_socket.", + ) + _check( + fishing_rod.basis.z.is_equal_approx(Vector3.RIGHT), + "Rod rear face did not follow the socket direction guide.", + ) + + _finish(host_skeleton) + + +func _finish(host_skeleton: Skeleton3D) -> void: + host_skeleton.queue_free() + if failures.is_empty(): + print("Net attachment validation: PASS") + quit(0) + return + for failure: String in failures: + printerr("Net attachment validation: ", failure) + quit(1) + + +func _check(condition: bool, message: String) -> void: + if not condition: + failures.append(message) diff --git a/tests/net_attachment_validation.gd.uid b/tests/net_attachment_validation.gd.uid new file mode 100644 index 0000000..5f968d1 --- /dev/null +++ b/tests/net_attachment_validation.gd.uid @@ -0,0 +1 @@ +uid://clnr8ofu1fqw5 diff --git a/tests/network_player_animation_protocol_validation.gd b/tests/network_player_animation_protocol_validation.gd index 8c3474c..66a3ec3 100644 --- a/tests/network_player_animation_protocol_validation.gd +++ b/tests/network_player_animation_protocol_validation.gd @@ -15,11 +15,24 @@ func _initialize() -> void: 1.25, ) assert(NetworkPlayerAnimationProtocol.validate_state(emote)) + var paused_action := NetworkPlayerAnimationProtocol.make_action_state( + &"strike", + 18, + 0.72, + true, + ) + assert(NetworkPlayerAnimationProtocol.validate_action_state(paused_action)) + assert(bool(paused_action["paused"])) var future_locomotion := NetworkPlayerAnimationProtocol.make_state( &"swimming_fast", false, ) assert(NetworkPlayerAnimationProtocol.validate_state(future_locomotion)) + var sneaking := NetworkPlayerAnimationProtocol.make_state( + NetworkPlayerAnimationProtocol.LOCOMOTION_SNEAKING, + true, + ) + assert(NetworkPlayerAnimationProtocol.validate_state(sneaking)) var extra_future_field: Dictionary = emote.duplicate(true) extra_future_field["future_layer"] = {"id": "umbrella"} assert(NetworkPlayerAnimationProtocol.validate_state(extra_future_field)) @@ -45,5 +58,8 @@ func _initialize() -> void: "elapsed": INF, } assert(not NetworkPlayerAnimationProtocol.validate_state(invalid_elapsed)) + var invalid_paused: Dictionary = emote.duplicate(true) + invalid_paused["action"]["paused"] = "yes" + assert(not NetworkPlayerAnimationProtocol.validate_state(invalid_paused)) print("Network player animation protocol validation: PASS") quit() diff --git a/tests/player_gathering_animation_validation.gd b/tests/player_gathering_animation_validation.gd new file mode 100644 index 0000000..c0f5576 --- /dev/null +++ b/tests/player_gathering_animation_validation.gd @@ -0,0 +1,93 @@ +extends SceneTree + + +const PlayerScene: PackedScene = preload("res://player/player.tscn") +const CrabBrown: FishData = preload( + "res://fish/species/crab_brown/crab_brown.tres" +) + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + var player := PlayerScene.instantiate() as Player + root.add_child(player) + await process_frame + player.set_process(false) + player.set_physics_process(false) + var animation_player := player.get_node( + "Visuals/CharacterRig/AnimationPlayer" + ) as AnimationPlayer + assert(animation_player != null) + for required_animation: StringName in [ + &"draw", + &"idle_sneak", + &"sneaking", + &"strike", + ]: + assert(animation_player.has_animation(required_animation)) + + Input.action_press(&"sneak") + player.velocity = Vector3.ZERO + player.call("_update_character_animation") + assert(animation_player.current_animation == &"idle_sneak") + + player.velocity = Vector3(player.sneak_speed, 0.0, 0.0) + player.call("_update_character_animation") + assert(animation_player.current_animation == &"sneaking") + + player.velocity = Vector3.ZERO + assert(player.begin_net_draw_visual()) + assert(animation_player.current_animation == &"draw") + player.call("_on_character_animation_finished", &"draw") + assert(StringName(player.get("_animation_action_id")) == &"draw") + assert(animation_player.current_animation == &"draw") + + assert(player.play_net_strike_visual()) + assert(animation_player.current_animation == &"strike") + player.call("_on_character_animation_finished", &"strike") + assert(StringName(player.get("_animation_action_id")) == &"strike") + assert(bool(player.get("_animation_action_paused"))) + assert(player.is_net_strike_held()) + assert(player.release_net_strike_hold()) + assert(StringName(player.get("_animation_action_id")).is_empty()) + assert(animation_player.current_animation == &"idle_sneak") + + Input.action_release(&"sneak") + assert(player.begin_net_draw_visual()) + assert(animation_player.current_animation == &"casting") + player.cancel_net_action_visual() + assert(StringName(player.get("_animation_action_id")).is_empty()) + assert(animation_player.current_animation == &"idle") + Input.action_press(&"sneak") + + assert(player.play_net_strike_visual()) + player.resolve_net_strike_visual(true) + var crab_catch := FishCatch.new() + crab_catch.fish = CrabBrown + crab_catch.fish_id = CrabBrown.id + crab_catch.weight_lb = CrabBrown.get_minimum_weight() + crab_catch.display_scale = CrabBrown.get_display_scale_for_weight( + crab_catch.weight_lb + ) + crab_catch.sale_value = CrabBrown.get_sale_value_for_weight( + crab_catch.weight_lb + ) + crab_catch.ensure_identity() + player.begin_catch_showcase(crab_catch) + assert(not bool(player.get("_showcase_animation_active"))) + assert(player.get("_pending_net_showcase_catch") == crab_catch) + player.call("_on_character_animation_finished", &"strike") + assert(StringName(player.get("_animation_action_id")).is_empty()) + assert(bool(player.get("_showcase_animation_active"))) + var catch_display := player.get("_catch_display") as Node3D + assert(catch_display != null and catch_display.visible) + player.end_catch_showcase(Callable(), true) + + Input.action_release(&"sneak") + player.queue_free() + await process_frame + print("Player gathering animation validation: PASS") + quit() diff --git a/tests/player_gathering_animation_validation.gd.uid b/tests/player_gathering_animation_validation.gd.uid new file mode 100644 index 0000000..276cd27 --- /dev/null +++ b/tests/player_gathering_animation_validation.gd.uid @@ -0,0 +1 @@ +uid://cba1j1dfxk5t4 diff --git a/tests/world_spawn_protocol_validation.gd b/tests/world_spawn_protocol_validation.gd index c9de51c..fd00423 100644 --- a/tests/world_spawn_protocol_validation.gd +++ b/tests/world_spawn_protocol_validation.gd @@ -4,6 +4,12 @@ const Gatherables: GatherableCatalog = preload( "res://gathering/catalog/gatherable_catalog.tres" ) const FishCatalog: FishPool = preload("res://fish/pools/fish_catalog.tres") +const GatheringControllerType = preload( + "res://gathering/gathering_controller.gd" +) + +const REDUCED_CRAB_PIXEL_SIZE: float = 0.00025 +const REDUCED_CATCH_RING_RADIUS: float = 0.175 func _initialize() -> void: @@ -27,6 +33,12 @@ func _validate_catalog_statuses() -> void: assert(brown.catch_data.logbook_section == FishData.LogbookSection.SHELLFISH) assert(brown.population == 2) assert(is_equal_approx(brown.charge_duration, 2.0)) + assert( + is_equal_approx( + brown.sprite_pixel_size, + REDUCED_CRAB_PIXEL_SIZE, + ) + ) _validate_quality_behavior(brown) assert(is_equal_approx(brown.capture_respawn_min_seconds, 480.0)) assert(is_equal_approx(brown.capture_respawn_max_seconds, 720.0)) @@ -47,6 +59,12 @@ func _validate_catalog_statuses() -> void: assert(entry.is_valid()) assert(not entry.catch_data.active) assert(not entry.is_available()) + assert( + is_equal_approx( + entry.sprite_pixel_size, + REDUCED_CRAB_PIXEL_SIZE, + ) + ) assert(entry.catch_data.collection_method == FishData.CollectionMethod.NET) assert( entry.catch_data.logbook_section @@ -63,6 +81,15 @@ func _validate_catalog_statuses() -> void: assert(captured_delay >= 480.0 and captured_delay <= 720.0) assert(scared_delay >= 45.0 and scared_delay <= 90.0) + var gathering_controller := GatheringControllerType.new() + assert( + is_equal_approx( + gathering_controller.marker_radius, + REDUCED_CATCH_RING_RADIUS, + ) + ) + gathering_controller.free() + func _validate_quality_behavior(entry: GatherableData) -> void: var prior_speed: float = -1.0