feat: add animated net gathering
This commit is contained in:
parent
b252b2f20c
commit
f0ddcccaad
38 changed files with 1298 additions and 76 deletions
Binary file not shown.
BIN
art/exported/items/equipment/net.glb
Normal file
BIN
art/exported/items/equipment/net.glb
Normal file
Binary file not shown.
45
art/exported/items/equipment/net.glb.import
Normal file
45
art/exported/items/equipment/net.glb.import
Normal 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
|
||||
BIN
art/exported/items/equipment/net_Material.png
Normal file
BIN
art/exported/items/equipment/net_Material.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 276 KiB |
45
art/exported/items/equipment/net_Material.png.import
Normal file
45
art/exported/items/equipment/net_Material.png.import
Normal 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
|
||||
BIN
art/exported/items/equipment/net_Plane Base Color.png
Normal file
BIN
art/exported/items/equipment/net_Plane Base Color.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
45
art/exported/items/equipment/net_Plane Base Color.png.import
Normal file
45
art/exported/items/equipment/net_Plane Base Color.png.import
Normal 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
|
||||
Binary file not shown.
0
art/source/items/equipment/.gdignore
Normal file
0
art/source/items/equipment/.gdignore
Normal file
BIN
art/source/items/equipment/net.blend
Normal file
BIN
art/source/items/equipment/net.blend
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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="."]
|
||||
|
||||
|
|
|
|||
88
player/net_attachment.gd
Normal file
88
player/net_attachment.gd
Normal 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
|
||||
1
player/net_attachment.gd.uid
Normal file
1
player/net_attachment.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cw5vc02hoelgm
|
||||
|
|
@ -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
|
||||
|
|
|
|||
328
player/player.gd
328
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,
|
||||
|
|
|
|||
57
player/tool_socket_attachment.gd
Normal file
57
player/tool_socket_attachment.gd
Normal 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
|
||||
1
player/tool_socket_attachment.gd.uid
Normal file
1
player/tool_socket_attachment.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://toolsocketalignment01
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
139
tests/character_rig_validation.gd
Normal file
139
tests/character_rig_validation.gd
Normal 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
|
||||
1
tests/character_rig_validation.gd.uid
Normal file
1
tests/character_rig_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://di6r1gphajq8u
|
||||
66
tests/gathering_marker_surface_validation.gd
Normal file
66
tests/gathering_marker_surface_validation.gd
Normal 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
|
||||
1
tests/gathering_marker_surface_validation.gd.uid
Normal file
1
tests/gathering_marker_surface_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ddsf88kuxgsqp
|
||||
|
|
@ -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,
|
||||
),
|
||||
}
|
||||
|
||||
|
|
|
|||
157
tests/net_attachment_validation.gd
Normal file
157
tests/net_attachment_validation.gd
Normal 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)
|
||||
1
tests/net_attachment_validation.gd.uid
Normal file
1
tests/net_attachment_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://clnr8ofu1fqw5
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
93
tests/player_gathering_animation_validation.gd
Normal file
93
tests/player_gathering_animation_validation.gd
Normal 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()
|
||||
1
tests/player_gathering_animation_validation.gd.uid
Normal file
1
tests/player_gathering_animation_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cba1j1dfxk5t4
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue