feat: add animated net gathering

This commit is contained in:
Alexander Sellite 2026-08-17 17:50:30 -04:00
parent b252b2f20c
commit f0ddcccaad
38 changed files with 1298 additions and 76 deletions

Binary file not shown.

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -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

View file

Binary file not shown.

View file

@ -21,6 +21,8 @@ independent team Woofmeow.
arms, ears and tails, and multiple world props and decorative assets, arms, ears and tails, and multiple world props and decorative assets,
including trees and bridges. The contributor requested to be credited as including trees and bridges. The contributor requested to be credited as
`chillnfill`. `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 - Tekgator: creator of the cooler-capacity, reel-speed, and barrier-power shop
icons. icons.
- kat: supplied the original recorded voice sample used for the animalese - 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. | | 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. | | 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. | | 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`. | | 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`. | | 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. | | 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, later written confirmation covering Project use, modification,
distribution, commercial release, and applicable sublicensing. 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 ### Fur customization channel-map record
Original channel-map artwork by Endeavour. Repository paths preserve the Original channel-map artwork by Endeavour. Repository paths preserve the

View file

@ -11,3 +11,4 @@ required_tool_id = &"crab_net"
surface_materials = Array[StringName]([&"sand"]) surface_materials = Array[StringName]([&"sand"])
minimum_surface_y = -0.8 minimum_surface_y = -0.8
population = 4 population = 4
sprite_pixel_size = 0.00025

View file

@ -17,7 +17,7 @@ scare_radius = 2.8
capture_radius = 0.7 capture_radius = 0.7
interaction_range = 2.6 interaction_range = 2.6
charge_duration = 2.0 charge_duration = 2.0
sprite_pixel_size = 0.001 sprite_pixel_size = 0.00025
sprite_tilt_degrees = -45.0 sprite_tilt_degrees = -45.0
capture_respawn_min_seconds = 480.0 capture_respawn_min_seconds = 480.0
capture_respawn_max_seconds = 720.0 capture_respawn_max_seconds = 720.0

View file

@ -11,3 +11,4 @@ required_tool_id = &"crab_net"
surface_materials = Array[StringName]([&"sand"]) surface_materials = Array[StringName]([&"sand"])
minimum_surface_y = -0.5 minimum_surface_y = -0.5
population = 4 population = 4
sprite_pixel_size = 0.00025

View file

@ -11,3 +11,4 @@ required_tool_id = &"crab_net"
surface_materials = Array[StringName]([&"sand"]) surface_materials = Array[StringName]([&"sand"])
minimum_surface_y = 0.08 minimum_surface_y = 0.08
population = 4 population = 4
sprite_pixel_size = 0.00025

View file

@ -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, 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 interaction_range: float = 2.6
@export_range(0.1, 10.0, 0.05) var charge_duration: float = 2.0 @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_range(-90.0, 90.0, 1.0) var sprite_tilt_degrees: float = -45.0
@export_category("Respawn Budget") @export_category("Respawn Budget")
@export_range(0.0, 3600.0, 1.0) var capture_respawn_min_seconds: float = 480.0 @export_range(0.0, 3600.0, 1.0) var capture_respawn_min_seconds: float = 480.0

View file

@ -10,7 +10,9 @@ const NetworkWorldSpawnServiceType = preload(
signal status_changed(message: String) signal status_changed(message: String)
@export_range(0.5, 5.0, 0.05) var marker_distance: float = 1.7 @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_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_height: float = 5.0
@export_range(0.5, 20.0, 0.5) var ray_depth: float = 8.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"): if not event.is_action("fish_primary"):
return return
if event.is_pressed(): 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 ( if (
_charging _charging
or not _gameplay_input_enabled or not _gameplay_input_enabled
@ -133,17 +142,20 @@ func _unhandled_input(event: InputEvent) -> void:
) )
_update_marker_target() _update_marker_target()
_update_target_entity() _update_target_entity()
_player.begin_net_draw_visual()
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
return return
if not _charging: if not _charging:
return return
var fully_charged: bool = _charge_elapsed >= _charge_duration var fully_charged: bool = _charge_elapsed >= _charge_duration
if ( var valid_capture: bool = (
fully_charged fully_charged
and _marker_has_surface and _marker_has_surface
and not _target_entity_id.is_empty() and not _target_entity_id.is_empty()
and _player.is_sneaking() and _player.is_sneaking()
): )
_player.play_net_strike_visual()
if valid_capture:
_service.finish_local_interaction( _service.finish_local_interaction(
_request_id, _request_id,
_target_entity_id, _target_entity_id,
@ -151,6 +163,7 @@ func _unhandled_input(event: InputEvent) -> void:
) )
else: else:
_service.cancel_local_interaction(_request_id) _service.cancel_local_interaction(_request_id)
_player.resolve_net_strike_visual(false)
_reset_charge_state() _reset_charge_state()
get_viewport().set_input_as_handled() get_viewport().set_input_as_handled()
@ -167,23 +180,82 @@ func _update_marker_target() -> void:
if facing.is_zero_approx(): if facing.is_zero_approx():
return return
facing = facing.normalized() 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_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_height,
query_center - Vector3.UP * ray_depth, 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, terrain_collision_mask,
[_player.get_rid()],
) )
query.collide_with_areas = false query.collide_with_areas = false
query.collide_with_bodies = true 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(): if hit.is_empty():
return return false
var position: Variant = hit.get("position") var position: Variant = hit.get("position")
if typeof(position) != TYPE_VECTOR3: var normal_value: Variant = hit.get("normal")
return if (
_marker_position = (position as Vector3) + Vector3.UP * 0.045 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_position = _marker_position
_marker.global_basis = marker_basis_for_surface(surface_normal, facing)
_marker_has_surface = true _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: func _update_target_entity() -> void:
@ -200,6 +272,8 @@ func _update_target_entity() -> void:
func _cancel_charge() -> void: func _cancel_charge() -> void:
if _charging and not _request_id.is_empty() and _service != null: if _charging and not _request_id.is_empty() and _service != null:
_service.cancel_local_interaction(_request_id) _service.cancel_local_interaction(_request_id)
if _charging and _player != null:
_player.cancel_net_action_visual()
_reset_charge_state() _reset_charge_state()
@ -213,6 +287,8 @@ func _reset_charge_state() -> void:
func _on_interaction_finished(accepted: bool, message: String) -> void: func _on_interaction_finished(accepted: bool, message: String) -> void:
if _player != null:
_player.resolve_net_strike_visual(accepted)
_reset_charge_state() _reset_charge_state()
if not accepted or not message.is_empty(): if not accepted or not message.is_empty():
status_changed.emit(message) status_changed.emit(message)

View file

@ -2,6 +2,8 @@ class_name WorldGatherable
extends Node3D extends Node3D
const GatherableDataType = preload("res://gathering/gatherable_data.gd") 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 entity_id: String = ""
var type_id: StringName var type_id: StringName
@ -26,6 +28,11 @@ func configure(
if data != null and data.catch_data != null: if data != null and data.catch_data != null:
_sprite.texture = data.catch_data.display_texture _sprite.texture = data.catch_data.display_texture
_sprite.pixel_size = data.sprite_pixel_size _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 _sprite.rotation_degrees.x = data.sprite_tilt_degrees
apply_network_state(position, yaw, true) apply_network_state(position, yaw, true)
@ -80,7 +87,7 @@ func _ensure_visual() -> void:
return return
_sprite = Sprite3D.new() _sprite = Sprite3D.new()
_sprite.name = "GatherableSprite" _sprite.name = "GatherableSprite"
_sprite.position.y = 0.22 _sprite.position.y = REFERENCE_SPRITE_HEIGHT
_sprite.shaded = false _sprite.shaded = false
_sprite.double_sided = true _sprite.double_sided = true
_sprite.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST _sprite.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST

View file

@ -9,6 +9,7 @@ const MAX_ACTION_ELAPSED_SECONDS: float = 86400.0
const LOCOMOTION_IDLE: StringName = &"idle" const LOCOMOTION_IDLE: StringName = &"idle"
const LOCOMOTION_WALKING: StringName = &"walking" const LOCOMOTION_WALKING: StringName = &"walking"
const LOCOMOTION_RUNNING: StringName = &"running" const LOCOMOTION_RUNNING: StringName = &"running"
const LOCOMOTION_SNEAKING: StringName = &"sneaking"
static func make_state( static func make_state(
@ -17,13 +18,17 @@ static func make_state(
action_id: StringName = &"", action_id: StringName = &"",
action_sequence: int = 0, action_sequence: int = 0,
action_elapsed: float = 0.0, action_elapsed: float = 0.0,
action_paused: bool = false,
) -> Dictionary: ) -> Dictionary:
return { return {
"format_version": FORMAT_VERSION, "format_version": FORMAT_VERSION,
"locomotion_id": String(locomotion_id), "locomotion_id": String(locomotion_id),
"grounded": grounded, "grounded": grounded,
"action": make_action_state( "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_id: StringName = &"",
action_sequence: int = 0, action_sequence: int = 0,
action_elapsed: float = 0.0, action_elapsed: float = 0.0,
paused: bool = false,
) -> Dictionary: ) -> Dictionary:
return { return {
"id": String(action_id), "id": String(action_id),
"sequence": action_sequence, "sequence": action_sequence,
"elapsed": action_elapsed, "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) not valid_state_id(action.get("id"), true)
or typeof(action.get("sequence")) != TYPE_INT or typeof(action.get("sequence")) != TYPE_INT
or typeof(action.get("elapsed")) not in [TYPE_FLOAT, TYPE_INT] or typeof(action.get("elapsed")) not in [TYPE_FLOAT, TYPE_INT]
or typeof(action.get("paused", false)) != TYPE_BOOL
): ):
return false return false
var sequence: int = int(action["sequence"]) var sequence: int = int(action["sequence"])

View file

@ -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"] [sub_resource type="CylinderMesh" id="FishingRodMesh"]
top_radius = 0.025 top_radius = 0.025
@ -11,6 +13,8 @@ roughness = 0.8
[node name="FishingRodAttachment" type="BoneAttachment3D"] [node name="FishingRodAttachment" type="BoneAttachment3D"]
bone_name = "rod_socket" bone_name = "rod_socket"
script = ExtResource("1_socket")
aligned_mount_path = NodePath("FishingRod")
[node name="FishingRod" type="Node3D" parent="."] [node name="FishingRod" type="Node3D" parent="."]

88
player/net_attachment.gd Normal file
View file

@ -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

View file

@ -0,0 +1 @@
uid://cw5vc02hoelgm

View file

@ -1,55 +1,20 @@
[gd_scene load_steps=7 format=3] [gd_scene load_steps=3 format=3]
[sub_resource type="StandardMaterial3D" id="HandleMaterial"] [ext_resource type="Script" path="res://player/net_attachment.gd" id="1_script"]
albedo_color = Color(0.25, 0.13, 0.055, 1) [ext_resource type="PackedScene" path="res://art/exported/items/equipment/net.glb" id="2_model"]
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
[node name="NetAttachment" type="BoneAttachment3D"] [node name="NetAttachment" type="BoneAttachment3D"]
bone_name = "rod_socket" 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 visible = false
[node name="Handle" type="MeshInstance3D" parent="CatchingNet"] [node name="ModelMount" type="Node3D" parent="CatchingNet"]
position = Vector3(0, 0.55, 0) position = Vector3(-0.000217, -0.098636, -0.020218)
mesh = SubResource("HandleMesh") rotation = Vector3(0, 3.1415927, 0)
[node name="Hoop" type="MeshInstance3D" parent="CatchingNet"] [node name="NetModel" parent="CatchingNet/ModelMount" unique_id=1627767240 instance=ExtResource("2_model")]
position = Vector3(0, 1.17, 0) unique_name_in_owner = true
mesh = SubResource("HoopMesh")
[node name="Bag" type="MeshInstance3D" parent="CatchingNet"]
position = Vector3(0, 1.0, 0)
mesh = SubResource("NetMesh")

View file

@ -32,6 +32,7 @@ const FishingRodAttachmentScene = preload(
"res://player/fishing_rod_attachment.tscn" "res://player/fishing_rod_attachment.tscn"
) )
const NetAttachmentScene = preload("res://player/net_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 FishingRodDataType = preload("res://items/fishing_rod_data.gd")
const HeldItemAttachmentScene = preload( const HeldItemAttachmentScene = preload(
"res://player/held_item_attachment.tscn" "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_WALKING_SHOW_ANIMATION: StringName = &"walking_show"
const CHARACTER_RUNNING_ANIMATION: StringName = &"running" const CHARACTER_RUNNING_ANIMATION: StringName = &"running"
const CHARACTER_RUNNING_SHOW_ANIMATION: StringName = &"running_show" 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_ANIMATION: StringName = &"casting"
const CHARACTER_CASTING_SIT_ANIMATION: StringName = &"casting_sit" const CHARACTER_CASTING_SIT_ANIMATION: StringName = &"casting_sit"
const CHARACTER_RELEASE_ANIMATION: StringName = &"release" 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_FISHING_SIT_ANIMATION: StringName = &"fishing_sit"
const CHARACTER_FIGHTING_ANIMATION: StringName = &"fighting" const CHARACTER_FIGHTING_ANIMATION: StringName = &"fighting"
const CHARACTER_FIGHTING_SIT_ANIMATION: StringName = &"fighting_sit" 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 # Add future networked emote animation IDs here. The protocol accepts unknown
# safe IDs so newer clients can extend it, but Player only presents actions # safe IDs so newer clients can extend it, but Player only presents actions
# explicitly approved by this catalog. # 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 { enum FishingVisualPhase {
NONE, NONE,
@ -98,6 +107,7 @@ enum LocomotionState {
IDLE, IDLE,
WALKING, WALKING,
RUNNING, RUNNING,
SNEAKING,
} }
const FIGHTING_EYES_ID: String = "alligator_eyes" const FIGHTING_EYES_ID: String = "alligator_eyes"
const BLINK_EYES_ID: String = "closed" 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.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_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_category("Camera")
@export var mouse_sensitivity: float = 0.005 @export var mouse_sensitivity: float = 0.005
@export var controller_camera_speed: float = 2.5 @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_id: StringName = &""
var _network_target_animation_action_sequence: int = 0 var _network_target_animation_action_sequence: int = 0
var _network_target_animation_action_elapsed: float = 0.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_ready: bool = false
var _network_snapshot_age: float = 0.0 var _network_snapshot_age: float = 0.0
var _network_snapshot_jitter: 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_id: StringName = &""
var _animation_action_sequence: int = 0 var _animation_action_sequence: int = 0
var _animation_action_elapsed: float = 0.0 var _animation_action_elapsed: float = 0.0
var _animation_action_paused: bool = false
var _presented_animation_action_id: StringName = &"" var _presented_animation_action_id: StringName = &""
var _presented_animation_action_sequence: int = -1 var _presented_animation_action_sequence: int = -1
var _presented_animation_action_paused: bool = false
var _character_animation_name: StringName = &"" var _character_animation_name: StringName = &""
var _sitting: bool = false var _sitting: bool = false
var _sit_after_landing: 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 _retract_animation_completed: bool = true
var _fishing_rod: Node3D var _fishing_rod: Node3D
var _catching_net: 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_tip: Marker3D
var _fishing_rod_model_mount: Node3D var _fishing_rod_model_mount: Node3D
var _fishing_rod_fallback_visual: GeometryInstance3D 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: func _apply_animation_action_state(action_state: Dictionary) -> void:
if not NetworkPlayerAnimationProtocol.validate_action_state(action_state): if not NetworkPlayerAnimationProtocol.validate_action_state(action_state):
return return
@ -597,10 +760,13 @@ func _apply_animation_action_state(action_state: Dictionary) -> void:
var action_changed: bool = ( var action_changed: bool = (
action_id != _animation_action_id action_id != _animation_action_id
or action_sequence != _animation_action_sequence or action_sequence != _animation_action_sequence
or bool(action_state.get("paused", false))
!= _animation_action_paused
) )
_animation_action_id = action_id _animation_action_id = action_id
_animation_action_sequence = action_sequence _animation_action_sequence = action_sequence
_animation_action_elapsed = float(action_state["elapsed"]) _animation_action_elapsed = float(action_state["elapsed"])
_animation_action_paused = bool(action_state.get("paused", false))
if action_changed: if action_changed:
_character_animation_name = &"" _character_animation_name = &""
_update_character_animation() _update_character_animation()
@ -637,11 +803,12 @@ func _initialize_catching_net() -> void:
if skeleton == null: if skeleton == null:
push_error("Player character skeleton is unavailable for the net.") push_error("Player character skeleton is unavailable for the net.")
return return
var attachment := NetAttachmentScene.instantiate() as BoneAttachment3D var attachment := NetAttachmentScene.instantiate() as NetAttachmentType
if attachment == null: if attachment == null:
push_error("Catching net attachment could not be instantiated.") push_error("Catching net attachment could not be instantiated.")
return return
skeleton.add_child(attachment) skeleton.add_child(attachment)
_catching_net_attachment = attachment
_catching_net = attachment.get_node("CatchingNet") as Node3D _catching_net = attachment.get_node("CatchingNet") as Node3D
if _catching_net != null: if _catching_net != null:
_catching_net.visible = false _catching_net.visible = false
@ -760,6 +927,8 @@ func _physics_process(delta: float) -> void:
move_direction.y = 0.0 move_direction.y = 0.0
var input_strength: float = minf(input_vector.length(), 1.0) var input_strength: float = minf(input_vector.length(), 1.0)
move_direction = move_direction.normalized() move_direction = move_direction.normalized()
if input_strength > 0.05:
release_net_strike_hold()
var speed: float = _get_network_aware_speed() var speed: float = _get_network_aware_speed()
if item_effects != null: if item_effects != null:
@ -779,13 +948,14 @@ func _physics_process(delta: float) -> void:
func _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 = minf(
_animation_action_elapsed + delta, _animation_action_elapsed + delta,
NetworkPlayerAnimationProtocol.MAX_ACTION_ELAPSED_SECONDS, NetworkPlayerAnimationProtocol.MAX_ACTION_ELAPSED_SECONDS,
) )
_update_blink(delta) _update_blink(delta)
_update_character_animation() _update_character_animation()
_update_net_strike_contact()
_update_sprint_dust(delta) _update_sprint_dust(delta)
# The hand bone supplies the attachment position, but its animated wrist # The hand bone supplies the attachment position, but its animated wrist
# rotation should not turn the flat fish/catch artwork edge-on. Keep each # 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_walking: bool = locomotion_state != LocomotionState.IDLE
var is_running: bool = locomotion_state == LocomotionState.RUNNING 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: Dictionary = _get_presented_animation_action()
var animation_action_id := StringName(str(animation_action.get("id", ""))) 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 = ( var animation_action_available: bool = (
not animation_action_id.is_empty() not animation_action_id.is_empty()
and supports_network_animation_action(animation_action_id) and supports_network_animation_action(animation_action_id)
@ -1075,6 +1254,19 @@ func _update_character_animation() -> void:
&"sitting", &"sitting",
&"idle_loop_sit", &"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: elif is_running and held_show_item_visible:
requested_animation = [ requested_animation = [
CHARACTER_RUNNING_SHOW_ANIMATION, CHARACTER_RUNNING_SHOW_ANIMATION,
@ -1124,11 +1316,14 @@ func _update_character_animation() -> void:
and ( and (
animation_action_id != _presented_animation_action_id animation_action_id != _presented_animation_action_id
or action_sequence != _presented_animation_action_sequence or action_sequence != _presented_animation_action_sequence
or animation_action_paused
!= _presented_animation_action_paused
) )
) )
if not action_selected: if not action_selected:
_presented_animation_action_id = &"" _presented_animation_action_id = &""
_presented_animation_action_sequence = -1 _presented_animation_action_sequence = -1
_presented_animation_action_paused = false
if _character_animation_name == next_animation and not action_changed: if _character_animation_name == next_animation and not action_changed:
return return
_character_animation_player.play(next_animation) _character_animation_player.play(next_animation)
@ -1136,22 +1331,58 @@ func _update_character_animation() -> void:
if action_selected: if action_selected:
_presented_animation_action_id = animation_action_id _presented_animation_action_id = animation_action_id
_presented_animation_action_sequence = action_sequence _presented_animation_action_sequence = action_sequence
_presented_animation_action_paused = animation_action_paused
var elapsed: float = float(animation_action.get("elapsed", 0.0)) var elapsed: float = float(animation_action.get("elapsed", 0.0))
var animation: Animation = _character_animation_player.get_animation( var animation: Animation = _character_animation_player.get_animation(
next_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( _character_animation_player.seek(
fposmod(elapsed, animation.length), true fposmod(elapsed, animation.length), true
) )
if animation_action_paused:
_character_animation_player.pause()
func _on_character_animation_finished(animation_name: StringName) -> void: 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 ( if (
local_control_enabled local_control_enabled
and not _animation_action_id.is_empty() and not _animation_action_id.is_empty()
and animation_name == _animation_action_id and animation_name == _animation_action_id
): ):
if animation_name in [
CHARACTER_CASTING_ANIMATION,
CHARACTER_NET_DRAW_ANIMATION,
]:
return
end_animation_action() end_animation_action()
return return
if animation_name in [ 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_id = &""
_network_target_animation_action_sequence = 0 _network_target_animation_action_sequence = 0
_network_target_animation_action_elapsed = 0.0 _network_target_animation_action_elapsed = 0.0
_network_target_animation_action_paused = false
_camera.current = false _camera.current = false
@ -1665,6 +1897,7 @@ func make_network_snapshot(peer_id: int) -> Dictionary:
_animation_action_id, _animation_action_id,
_animation_action_sequence, _animation_action_sequence,
_animation_action_elapsed, _animation_action_elapsed,
_animation_action_paused,
), ),
"sitting": _sitting, "sitting": _sitting,
"casting": _fishing_visual_phase == FishingVisualPhase.CASTING, "casting": _fishing_visual_phase == FishingVisualPhase.CASTING,
@ -1876,6 +2109,7 @@ func _make_animation_action_state() -> Dictionary:
_animation_action_id, _animation_action_id,
_animation_action_sequence, _animation_action_sequence,
_animation_action_elapsed, _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_id = StringName(str(action["id"]))
_network_target_animation_action_sequence = int(action["sequence"]) _network_target_animation_action_sequence = int(action["sequence"])
_network_target_animation_action_elapsed = float(action["elapsed"]) _network_target_animation_action_elapsed = float(action["elapsed"])
_network_target_animation_action_paused = bool(
action.get("paused", false)
)
func _get_authoritative_locomotion_state() -> LocomotionState: func _get_authoritative_locomotion_state() -> LocomotionState:
@ -1898,14 +2135,17 @@ func _get_authoritative_locomotion_state() -> LocomotionState:
and _is_movement_input_enabled() and _is_movement_input_enabled()
and not _water_recovery_active and not _water_recovery_active
and not _sitting 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 ( return (
LocomotionState.RUNNING LocomotionState.RUNNING
if _network_sprint if _network_sprint
else LocomotionState.WALKING else LocomotionState.WALKING
) )
return _locomotion_state_from_velocity() return _locomotion_state_from_velocity(is_sneaking())
func _get_authoritative_locomotion_id() -> StringName: func _get_authoritative_locomotion_id() -> StringName:
@ -1915,7 +2155,19 @@ func _get_authoritative_locomotion_id() -> StringName:
func _get_presented_locomotion_state() -> LocomotionState: func _get_presented_locomotion_state() -> LocomotionState:
if _network_interpolation_enabled and _network_snapshot_ready: if _network_interpolation_enabled and _network_snapshot_ready:
return _network_target_locomotion_state 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: 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_id,
_network_target_animation_action_sequence, _network_target_animation_action_sequence,
minf( 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, NetworkPlayerAnimationProtocol.MAX_ACTION_ELAPSED_SECONDS,
), ),
_network_target_animation_action_paused,
) )
return _make_animation_action_state() return _make_animation_action_state()
func _locomotion_id_from_state(state: LocomotionState) -> StringName: func _locomotion_id_from_state(state: LocomotionState) -> StringName:
match state: match state:
LocomotionState.SNEAKING:
return NetworkPlayerAnimationProtocol.LOCOMOTION_SNEAKING
LocomotionState.WALKING: LocomotionState.WALKING:
return NetworkPlayerAnimationProtocol.LOCOMOTION_WALKING return NetworkPlayerAnimationProtocol.LOCOMOTION_WALKING
LocomotionState.RUNNING: LocomotionState.RUNNING:
@ -1943,6 +2203,8 @@ func _locomotion_id_from_state(state: LocomotionState) -> StringName:
func _locomotion_state_from_id(state_id: StringName) -> LocomotionState: func _locomotion_state_from_id(state_id: StringName) -> LocomotionState:
match state_id: match state_id:
NetworkPlayerAnimationProtocol.LOCOMOTION_SNEAKING:
return LocomotionState.SNEAKING
NetworkPlayerAnimationProtocol.LOCOMOTION_WALKING: NetworkPlayerAnimationProtocol.LOCOMOTION_WALKING:
return LocomotionState.WALKING return LocomotionState.WALKING
NetworkPlayerAnimationProtocol.LOCOMOTION_RUNNING: 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 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 = ( var horizontal_speed_squared: float = (
velocity.x * velocity.x + velocity.z * velocity.z 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: func set_active_catching_net(should_show: bool) -> void:
_active_item_is_net = should_show _active_item_is_net = should_show
if not should_show:
cancel_net_action_visual()
if _catching_net == null: if _catching_net == null:
return return
if _showcase_rod_state_stored: if _showcase_rod_state_stored:
@ -2602,6 +2870,14 @@ func get_cast_origin_position() -> Vector3:
func begin_catch_showcase(fish_catch: FishCatchType) -> void: func begin_catch_showcase(fish_catch: FishCatchType) -> void:
if fish_catch == null or not fish_catch.is_valid(): if fish_catch == null or not fish_catch.is_valid():
return 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: if _pocket_visual_target == PocketVisualTarget.CATCH_SHOWCASE:
_cancel_pocket_visual() _cancel_pocket_visual()
_showcase_animation_active = true _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: func begin_remote_catch_showcase(fish_catch: FishCatchType) -> void:
if local_control_enabled or fish_catch == null or not fish_catch.is_valid(): if local_control_enabled or fish_catch == null or not fish_catch.is_valid():
return 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) end_catch_showcase(Callable(), true)
_showcase_animation_active = true _showcase_animation_active = true
set_fishing_visual(false) set_fishing_visual(false)
@ -2657,6 +2941,32 @@ func begin_remote_catch_showcase(fish_catch: FishCatchType) -> void:
_update_character_animation() _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( func end_catch_showcase(
restored_callback: Callable = Callable(), restored_callback: Callable = Callable(),
immediate: bool = false, immediate: bool = false,

View file

@ -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

View file

@ -0,0 +1 @@
uid://toolsocketalignment01

View file

@ -14,6 +14,7 @@ readonly -a QUICK_TESTS=(
"scripts/validate_animalese_samples.gd" "scripts/validate_animalese_samples.gd"
"tests/android_readiness_validation.gd" "tests/android_readiness_validation.gd"
"tests/camera_drag_validation.gd" "tests/camera_drag_validation.gd"
"tests/character_rig_validation.gd"
"tests/controller_focus_presentation_validation.gd" "tests/controller_focus_presentation_validation.gd"
"tests/controller_menu_accessibility_validation.gd" "tests/controller_menu_accessibility_validation.gd"
"tests/controller_mapping_validation.gd" "tests/controller_mapping_validation.gd"
@ -28,11 +29,14 @@ readonly -a QUICK_TESTS=(
"tests/fishing_audio_validation.gd" "tests/fishing_audio_validation.gd"
"tests/fishing_surface_validation.gd" "tests/fishing_surface_validation.gd"
"tests/fur_pattern_validation.gd" "tests/fur_pattern_validation.gd"
"tests/gathering_marker_surface_validation.gd"
"tests/inventory_storage_validation.gd" "tests/inventory_storage_validation.gd"
"tests/keyboard_mouse_mapping_validation.gd" "tests/keyboard_mouse_mapping_validation.gd"
"tests/logbook_validation.gd" "tests/logbook_validation.gd"
"tests/network_player_animation_protocol_validation.gd" "tests/network_player_animation_protocol_validation.gd"
"tests/net_attachment_validation.gd"
"tests/on_screen_keyboard_validation.gd" "tests/on_screen_keyboard_validation.gd"
"tests/player_gathering_animation_validation.gd"
"tests/player_experience_validation.gd" "tests/player_experience_validation.gd"
"tests/shoreline_ambience_validation.gd" "tests/shoreline_ambience_validation.gd"
"tests/surface_drawing_validation.gd" "tests/surface_drawing_validation.gd"

View file

@ -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

View file

@ -0,0 +1 @@
uid://di6r1gphajq8u

View file

@ -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

View file

@ -0,0 +1 @@
uid://ddsf88kuxgsqp

View file

@ -241,7 +241,25 @@ func _run_host() -> void:
await create_timer(2.5).timeout await create_timer(2.5).timeout
player.apply_authoritative_network_input(_movement_input(2, false)) player.apply_authoritative_network_input(_movement_input(2, false))
await create_timer(2.5).timeout 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 var disconnect_deadline: int = Time.get_ticks_msec() + 8000
while ( while (
Time.get_ticks_msec() < disconnect_deadline 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 sprint_dust := host_avatar.get_node("%SprintDust") as SprintDustTrail
var saw_running: bool = false var saw_running: bool = false
var saw_walking: 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_animation_advance: bool = false
var saw_dust: bool = false var saw_dust: bool = false
var previous_animation: StringName = &"" var previous_animation: StringName = &""
var previous_animation_position: float = -1.0 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: while Time.get_ticks_msec() < observation_deadline:
await process_frame await process_frame
var current_animation: StringName = animation_player.current_animation var current_animation: StringName = animation_player.current_animation
saw_running = saw_running or current_animation.begins_with("running") saw_running = saw_running or current_animation.begins_with("running")
saw_walking = saw_walking or current_animation.begins_with("walking") 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 = ( var current_position: float = (
animation_player.current_animation_position animation_player.current_animation_position
) )
@ -306,10 +332,23 @@ func _run_client() -> void:
previous_animation = current_animation previous_animation = current_animation
previous_animation_position = current_position previous_animation_position = current_position
saw_dust = saw_dust or sprint_dust.get_active_puff_count() > 0 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 break
assert(saw_running) assert(saw_running)
assert(saw_walking) assert(saw_walking)
assert(saw_sneaking)
assert(saw_idle_sneak)
assert(saw_net_draw)
assert(saw_net_strike)
assert(saw_animation_advance) assert(saw_animation_advance)
assert(saw_dust) assert(saw_dust)
print("Movement multiplayer client validation: PASS") print("Movement multiplayer client validation: PASS")
@ -325,6 +364,9 @@ func _movement_input(
sequence: int, sequence: int,
sprinting: bool, sprinting: bool,
moving: bool = true, moving: bool = true,
sneaking: bool = false,
action_id: StringName = &"",
action_sequence: int = 0,
) -> Dictionary: ) -> Dictionary:
return { return {
"sequence": sequence, "sequence": sequence,
@ -332,12 +374,13 @@ func _movement_input(
"camera_yaw": 0.0, "camera_yaw": 0.0,
"jump": false, "jump": false,
"sprint": sprinting, "sprint": sprinting,
"sneak": false, "sneak": sneaking,
"slow_walk": false, "slow_walk": false,
"sitting": false, "sitting": false,
"casting": false, "casting": false,
"animation_action": ( "animation_action": NetworkPlayerAnimationProtocol.make_action_state(
NetworkPlayerAnimationProtocol.make_action_state() action_id,
action_sequence,
), ),
} }

View file

@ -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)

View file

@ -0,0 +1 @@
uid://clnr8ofu1fqw5

View file

@ -15,11 +15,24 @@ func _initialize() -> void:
1.25, 1.25,
) )
assert(NetworkPlayerAnimationProtocol.validate_state(emote)) 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( var future_locomotion := NetworkPlayerAnimationProtocol.make_state(
&"swimming_fast", &"swimming_fast",
false, false,
) )
assert(NetworkPlayerAnimationProtocol.validate_state(future_locomotion)) 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) var extra_future_field: Dictionary = emote.duplicate(true)
extra_future_field["future_layer"] = {"id": "umbrella"} extra_future_field["future_layer"] = {"id": "umbrella"}
assert(NetworkPlayerAnimationProtocol.validate_state(extra_future_field)) assert(NetworkPlayerAnimationProtocol.validate_state(extra_future_field))
@ -45,5 +58,8 @@ func _initialize() -> void:
"elapsed": INF, "elapsed": INF,
} }
assert(not NetworkPlayerAnimationProtocol.validate_state(invalid_elapsed)) 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") print("Network player animation protocol validation: PASS")
quit() quit()

View file

@ -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()

View file

@ -0,0 +1 @@
uid://cba1j1dfxk5t4

View file

@ -4,6 +4,12 @@ const Gatherables: GatherableCatalog = preload(
"res://gathering/catalog/gatherable_catalog.tres" "res://gathering/catalog/gatherable_catalog.tres"
) )
const FishCatalog: FishPool = preload("res://fish/pools/fish_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: func _initialize() -> void:
@ -27,6 +33,12 @@ func _validate_catalog_statuses() -> void:
assert(brown.catch_data.logbook_section == FishData.LogbookSection.SHELLFISH) assert(brown.catch_data.logbook_section == FishData.LogbookSection.SHELLFISH)
assert(brown.population == 2) assert(brown.population == 2)
assert(is_equal_approx(brown.charge_duration, 2.0)) 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) _validate_quality_behavior(brown)
assert(is_equal_approx(brown.capture_respawn_min_seconds, 480.0)) assert(is_equal_approx(brown.capture_respawn_min_seconds, 480.0))
assert(is_equal_approx(brown.capture_respawn_max_seconds, 720.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(entry.is_valid())
assert(not entry.catch_data.active) assert(not entry.catch_data.active)
assert(not entry.is_available()) 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.collection_method == FishData.CollectionMethod.NET)
assert( assert(
entry.catch_data.logbook_section 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(captured_delay >= 480.0 and captured_delay <= 720.0)
assert(scared_delay >= 45.0 and scared_delay <= 90.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: func _validate_quality_behavior(entry: GatherableData) -> void:
var prior_speed: float = -1.0 var prior_speed: float = -1.0